Skip to main content

axon_frontend/
parser.rs

1//! AXON Parser — recursive descent, fail-fast.
2//!
3//! Direct port of axon/compiler/parser.py.
4//!
5//! Tier 1 constructs (persona, context, anchor, memory, tool, type,
6//! flow, step, intent, run, epistemic, if, for, let, return) are
7//! fully parsed into typed AST nodes.
8//!
9//! Tier 2+ constructs are parsed structurally (balanced braces) into
10//! `GenericDeclaration` / `GenericFlowStep`.
11
12use crate::ast::*;
13use crate::tokens::{is_declaration_keyword, Token, TokenType, Trivia, TriviaKind};
14
15// Comment token kinds the lexer now emits (Fase 14.a). The parser
16// filters these out of its working stream — they are materialised into
17// a parallel `Trivia` array indexed by effective-token position, then
18// attached to `Program.declaration_trivia[i]` once each declaration's
19// span is known.
20const fn is_comment_token(tt: &TokenType) -> bool {
21    matches!(
22        tt,
23        TokenType::LineComment
24            | TokenType::BlockComment
25            | TokenType::DocLineComment
26            | TokenType::DocBlockComment
27            | TokenType::InnerDocLineComment
28            | TokenType::InnerDocBlockComment
29    )
30}
31
32const fn token_to_trivia_kind(tt: &TokenType) -> Option<TriviaKind> {
33    match tt {
34        TokenType::LineComment => Some(TriviaKind::Line),
35        TokenType::BlockComment => Some(TriviaKind::Block),
36        TokenType::DocLineComment => Some(TriviaKind::DocLine),
37        TokenType::DocBlockComment => Some(TriviaKind::DocBlock),
38        TokenType::InnerDocLineComment => Some(TriviaKind::InnerDocLine),
39        TokenType::InnerDocBlockComment => Some(TriviaKind::InnerDocBlock),
40        _ => None,
41    }
42}
43
44/// Fase 14.b — write `leading_trivia` and `trailing_trivia` into the
45/// per-struct fields of a `Declaration` variant.
46///
47/// Mirrors what the Python parser does automatically via its
48/// `_with_trivia` decorator on every `_parse_*` method. In Rust we
49/// do it once at the top of the parse loop so the spread to every
50/// variant is in a single place.
51fn attach_trivia_to_decl(decl: &mut Declaration, leading: Vec<Trivia>, trailing: Vec<Trivia>) {
52    match decl {
53        Declaration::Import(n) => {
54            n.leading_trivia = leading;
55            n.trailing_trivia = trailing;
56        }
57        Declaration::Persona(n) => {
58            n.leading_trivia = leading;
59            n.trailing_trivia = trailing;
60        }
61        Declaration::Context(n) => {
62            n.leading_trivia = leading;
63            n.trailing_trivia = trailing;
64        }
65        Declaration::Anchor(n) => {
66            n.leading_trivia = leading;
67            n.trailing_trivia = trailing;
68        }
69        Declaration::Memory(n) => {
70            n.leading_trivia = leading;
71            n.trailing_trivia = trailing;
72        }
73        Declaration::Tool(n) => {
74            n.leading_trivia = leading;
75            n.trailing_trivia = trailing;
76        }
77        Declaration::Type(n) => {
78            n.leading_trivia = leading;
79            n.trailing_trivia = trailing;
80        }
81        Declaration::Flow(n) => {
82            n.leading_trivia = leading;
83            n.trailing_trivia = trailing;
84        }
85        Declaration::Intent(n) => {
86            n.leading_trivia = leading;
87            n.trailing_trivia = trailing;
88        }
89        Declaration::Run(n) => {
90            n.leading_trivia = leading;
91            n.trailing_trivia = trailing;
92        }
93        Declaration::Epistemic(n) => {
94            n.leading_trivia = leading;
95            n.trailing_trivia = trailing;
96        }
97        Declaration::Let(n) => {
98            n.leading_trivia = leading;
99            n.trailing_trivia = trailing;
100        }
101        Declaration::LambdaData(n) => {
102            n.leading_trivia = leading;
103            n.trailing_trivia = trailing;
104        }
105        Declaration::Agent(n) => {
106            n.leading_trivia = leading;
107            n.trailing_trivia = trailing;
108        }
109        Declaration::Shield(n) => {
110            n.leading_trivia = leading;
111            n.trailing_trivia = trailing;
112        }
113        Declaration::Pix(n) => {
114            n.leading_trivia = leading;
115            n.trailing_trivia = trailing;
116        }
117        Declaration::Psyche(n) => {
118            n.leading_trivia = leading;
119            n.trailing_trivia = trailing;
120        }
121        Declaration::Corpus(n) => {
122            n.leading_trivia = leading;
123            n.trailing_trivia = trailing;
124        }
125        Declaration::Dataspace(n) => {
126            n.leading_trivia = leading;
127            n.trailing_trivia = trailing;
128        }
129        Declaration::Ots(n) => {
130            n.leading_trivia = leading;
131            n.trailing_trivia = trailing;
132        }
133        Declaration::Mandate(n) => {
134            n.leading_trivia = leading;
135            n.trailing_trivia = trailing;
136        }
137        Declaration::Compute(n) => {
138            n.leading_trivia = leading;
139            n.trailing_trivia = trailing;
140        }
141        Declaration::Daemon(n) => {
142            n.leading_trivia = leading;
143            n.trailing_trivia = trailing;
144        }
145        Declaration::Extension(n) => {
146            n.leading_trivia = leading;
147            n.trailing_trivia = trailing;
148        }
149        Declaration::AxonStore(n) => {
150            n.leading_trivia = leading;
151            n.trailing_trivia = trailing;
152        }
153        Declaration::AxonEndpoint(n) => {
154            n.leading_trivia = leading;
155            n.trailing_trivia = trailing;
156        }
157        Declaration::Resource(n) => {
158            n.leading_trivia = leading;
159            n.trailing_trivia = trailing;
160        }
161        Declaration::Fabric(n) => {
162            n.leading_trivia = leading;
163            n.trailing_trivia = trailing;
164        }
165        Declaration::Manifest(n) => {
166            n.leading_trivia = leading;
167            n.trailing_trivia = trailing;
168        }
169        Declaration::Observe(n) => {
170            n.leading_trivia = leading;
171            n.trailing_trivia = trailing;
172        }
173        Declaration::Reconcile(n) => {
174            n.leading_trivia = leading;
175            n.trailing_trivia = trailing;
176        }
177        Declaration::Lease(n) => {
178            n.leading_trivia = leading;
179            n.trailing_trivia = trailing;
180        }
181        Declaration::Ensemble(n) => {
182            n.leading_trivia = leading;
183            n.trailing_trivia = trailing;
184        }
185        Declaration::Session(n) => {
186            n.leading_trivia = leading;
187            n.trailing_trivia = trailing;
188        }
189        Declaration::Topology(n) => {
190            n.leading_trivia = leading;
191            n.trailing_trivia = trailing;
192        }
193        Declaration::Immune(n) => {
194            n.leading_trivia = leading;
195            n.trailing_trivia = trailing;
196        }
197        Declaration::Reflex(n) => {
198            n.leading_trivia = leading;
199            n.trailing_trivia = trailing;
200        }
201        Declaration::Heal(n) => {
202            n.leading_trivia = leading;
203            n.trailing_trivia = trailing;
204        }
205        Declaration::Component(n) => {
206            n.leading_trivia = leading;
207            n.trailing_trivia = trailing;
208        }
209        Declaration::View(n) => {
210            n.leading_trivia = leading;
211            n.trailing_trivia = trailing;
212        }
213        Declaration::Channel(n) => {
214            n.leading_trivia = leading;
215            n.trailing_trivia = trailing;
216        }
217        Declaration::Socket(n) => {
218            n.leading_trivia = leading;
219            n.trailing_trivia = trailing;
220        }
221        Declaration::Generic(n) => {
222            n.leading_trivia = leading;
223            n.trailing_trivia = trailing;
224        }
225    }
226}
227
228// ── Public error type ────────────────────────────────────────────────────────
229
230/// §Fase 28.d — Source-context constants. D4 ratified 2026-05-10:
231/// 2 lines before + 2 lines after the error line. Mirror of the
232/// Python-side `_SOURCE_CONTEXT_LINES_BEFORE` / `_AFTER` so the
233/// rustc-style block has identical shape across stacks.
234pub const SOURCE_CONTEXT_LINES_BEFORE: usize = 2;
235pub const SOURCE_CONTEXT_LINES_AFTER: usize = 2;
236
237/// §Fase 28.d — Rustc-style source-context block for a parse error.
238///
239/// Holds a reference to the source text plus the line/column the
240/// error points at. Rendering is lazy — call ``render()`` to format
241/// the block (line numbers + caret + 2 lines before + 2 after).
242///
243/// Pure and deterministic: no ANSI colors, no terminal-width
244/// detection. Output shape is byte-identical to the Python
245/// `SourceSnippet.render()` on the same input — that's the cross-
246/// stack drift gate (28.i).
247#[derive(Debug, Clone)]
248pub struct SourceSnippet {
249    pub source: String,
250    pub line: u32,
251    pub column: u32,
252    pub filename: String,
253    pub context_before: usize,
254    pub context_after: usize,
255}
256
257impl SourceSnippet {
258    /// Construct with the default 2/2 context window.
259    pub fn new(source: String, line: u32, column: u32, filename: String) -> Self {
260        Self {
261            source,
262            line,
263            column,
264            filename,
265            context_before: SOURCE_CONTEXT_LINES_BEFORE,
266            context_after: SOURCE_CONTEXT_LINES_AFTER,
267        }
268    }
269
270    /// Format the snippet as a multi-line rustc-style block.
271    ///
272    /// Empty source → empty string. Out-of-range line → empty
273    /// string. Caret column is clamped to `[1, line_len + 1]`.
274    /// Output shape matches Python `SourceSnippet.render` byte-
275    /// identically per D7.
276    #[must_use]
277    pub fn render(&self) -> String {
278        if self.source.is_empty() || self.line < 1 {
279            return String::new();
280        }
281        let raw: Vec<&str> = self.source.split('\n').collect();
282        // Match Python's str.splitlines() trailing-newline shape:
283        // strip an empty trailing entry produced by a final '\n'.
284        let lines: Vec<&str> = if raw.last() == Some(&"") {
285            raw[..raw.len() - 1].to_vec()
286        } else {
287            raw
288        };
289        if lines.is_empty() || self.line as usize > lines.len() {
290            return String::new();
291        }
292
293        let line_idx = self.line as usize;
294        let start = line_idx.saturating_sub(self.context_before).max(1);
295        let end = (line_idx + self.context_after).min(lines.len());
296
297        let gutter = end.to_string().len();
298        let empty_gutter = " ".repeat(gutter);
299
300        let mut out: Vec<String> = Vec::with_capacity(end - start + 4);
301        out.push(format!(
302            "{empty_gutter} --> {}:{}:{}",
303            self.filename, self.line, self.column
304        ));
305        out.push(format!("{empty_gutter} |"));
306        for n in start..=end {
307            let line_text = lines[n - 1];
308            out.push(format!("{n:>gutter$} | {line_text}", gutter = gutter));
309            if n == line_idx {
310                let line_len = line_text.chars().count();
311                let col = (self.column as usize).clamp(1, line_len + 1);
312                out.push(format!(
313                    "{empty_gutter} | {pad}^",
314                    pad = " ".repeat(col - 1)
315                ));
316            }
317        }
318        out.join("\n")
319    }
320}
321
322#[derive(Debug, Clone, Default)]
323pub struct ParseError {
324    pub message: String,
325    pub line: u32,
326    pub column: u32,
327    /// §Fase 28.d — Optional rustc-style source-context block.
328    /// `None` preserves the legacy single-line shape; populated by
329    /// `Parser::with_source` callers (and by `parse_with_recovery`
330    /// / `parse` when a source has been attached to the parser).
331    /// Existing struct-literal call sites use the `..Default::default()`
332    /// idiom (default = None) to stay terse.
333    pub source_snippet: Option<SourceSnippet>,
334}
335
336impl ParseError {
337    /// §Fase 28.d — Attach a `SourceSnippet` derived from raw source
338    /// text and filename. Returns `self` so the call can be chained
339    /// at the construction site. No-op when `line == 0`. Idempotent.
340    #[must_use]
341    pub fn attach_source(mut self, source: &str, filename: &str) -> Self {
342        if self.line >= 1 {
343            self.source_snippet = Some(SourceSnippet::new(
344                source.to_string(),
345                self.line,
346                self.column,
347                filename.to_string(),
348            ));
349        }
350        self
351    }
352}
353
354impl std::fmt::Display for ParseError {
355    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356        write!(f, "[line {}:{}] {}", self.line, self.column, self.message)?;
357        if let Some(snippet) = &self.source_snippet {
358            let block = snippet.render();
359            if !block.is_empty() {
360                write!(f, "\n{block}")?;
361            }
362        }
363        Ok(())
364    }
365}
366
367impl std::error::Error for ParseError {}
368
369// ── §Fase 28.c — Public recovery result ──────────────────────────────────────
370//
371// Mirror of Python's `axon.compiler.parser.ParseResult` (Fase 28.b).
372// The rationale, sync semantics, and test contract are documented in
373// `docs/fase/fase_28_adopter_diagnostic_robustness.md`. The Rust frontend
374// must produce structurally identical error lists to the Python parser
375// when handed the same source — that is the cross-stack drift gate
376// (D7 ratified 2026-05-10: byte-identical error lists).
377//
378// `program` holds whatever declarations the parser was able to parse
379// successfully. `errors` holds every recovered error in source order.
380// A clean parse returns `errors.is_empty()`; the existing fail-fast
381// `parse()` API is preserved verbatim per D9.
382
383/// Outcome of `Parser::parse_with_recovery` — partial program plus the
384/// list of every error the parser recovered from. See module docs for
385/// the panic-mode + sync-point recovery semantics.
386#[derive(Debug)]
387pub struct ParseResult {
388    pub program: Program,
389    pub errors: Vec<ParseError>,
390}
391
392impl ParseResult {
393    /// True iff at least one parse error was recovered. Callers that
394    /// want to short-circuit on failure should check this rather than
395    /// relying on `program.declarations.is_empty()` (the parser may
396    /// have salvaged some declarations even with errors present).
397    #[inline]
398    #[must_use]
399    pub fn has_errors(&self) -> bool {
400        !self.errors.is_empty()
401    }
402
403    /// Inverse of `has_errors`. Convenience for the "happy path" check
404    /// in tests + adopter integrations.
405    #[inline]
406    #[must_use]
407    pub fn is_clean(&self) -> bool {
408        self.errors.is_empty()
409    }
410}
411
412/// §Fase 28.c — Top-level declaration keywords used as resync points
413/// during error recovery (D2 ratified 2026-05-10). Mirrors the
414/// `_TOP_LEVEL_DECLARATION_KEYWORDS` frozenset on the Python side.
415///
416/// Distinct from `tokens::is_declaration_keyword` because that helper
417/// is used by the structural declaration counter and intentionally
418/// excludes some grammar-only tokens (Know/Believe/Speculate/Doubt,
419/// Ingest, Ots) that DO begin a top-level declaration in
420/// `parse_declaration` and therefore must be valid sync points.
421///
422/// Adding a new top-level dispatch arm in `parse_declaration` MUST
423/// add the corresponding token here so the recovery walker can
424/// re-sync correctly.
425#[inline]
426const fn is_top_level_decl_kw_for_recovery(tt: &TokenType) -> bool {
427    matches!(
428        tt,
429        TokenType::Import
430            | TokenType::Persona
431            | TokenType::Context
432            | TokenType::Anchor
433            | TokenType::Memory
434            | TokenType::Tool
435            | TokenType::Type
436            | TokenType::Flow
437            | TokenType::Intent
438            | TokenType::Run
439            | TokenType::Let
440            | TokenType::Know
441            | TokenType::Believe
442            | TokenType::Speculate
443            | TokenType::Doubt
444            | TokenType::Lambda
445            | TokenType::Agent
446            | TokenType::Shield
447            | TokenType::Pix
448            | TokenType::Psyche
449            | TokenType::Corpus
450            | TokenType::Dataspace
451            | TokenType::Ots
452            | TokenType::Mandate
453            | TokenType::Compute
454            | TokenType::Daemon
455            | TokenType::AxonStore
456            | TokenType::AxonEndpoint
457            | TokenType::Resource
458            | TokenType::Fabric
459            | TokenType::Manifest
460            | TokenType::Observe
461            | TokenType::Reconcile
462            | TokenType::Lease
463            | TokenType::Ensemble
464            | TokenType::Session
465            | TokenType::Topology
466            | TokenType::Immune
467            | TokenType::Reflex
468            | TokenType::Heal
469            | TokenType::Component
470            | TokenType::View
471            | TokenType::Channel
472            | TokenType::Ingest
473            | TokenType::Persist
474            | TokenType::Retrieve
475            | TokenType::Mutate
476            | TokenType::Purge
477            | TokenType::Transact
478            | TokenType::Mcp
479    )
480}
481
482// ── §Fase 30.b — axonendpoint transport + keepalive closed enums ────────────
483//
484// D2 ratified 2026-05-10: `transport` is a closed enum
485// {json, sse, ndjson}. D6 ratified: `keepalive` is a closed enum
486// {5s, 15s, 30s, 60s}. Both mirror the Python frontend's
487// `_AXONENDPOINT_TRANSPORT_VALUES` / `_AXONENDPOINT_KEEPALIVE_VALUES`
488// frozensets in `axon/compiler/parser.py`. Cross-stack drift gate
489// (30.b fixture) asserts byte-identical parse for every entry.
490
491/// Adopter-facing acceptable values for `transport:` field.
492/// Used by both the parser (validation + smart-suggest) and the
493/// type-checker (30.c) so adopter tooling sees one canonical list.
494pub const AXONENDPOINT_TRANSPORT_VALUES: &[&str] = &["json", "sse", "ndjson"];
495
496/// §Fase 33.z.k.b (v1.28.0) — Closed-catalog SSE wire-format
497/// dialects. Selected via the parametrized grammar
498/// `transport: sse(<dialect>)`; bare `transport: sse` resolves to
499/// the Q1 default per the flow's algebraic-effect predicate
500/// (openai for tool-streaming flows; axon for type-annotation-only).
501///
502/// Vertical-grounded scope (Q3 revised 2026-05-14): five dialects
503/// cover ~99% of LLM-streaming adopter expectations.
504///   - `axon`      — current W3C named events
505///                   (event: axon.token / event: axon.complete).
506///                   D6 backwards-compat baseline; indefinitely
507///                   supported as a first-class option.
508///   - `openai`    — `data: {"choices":[{"delta":{...}}]}` frames
509///                   terminated by `data: [DONE]`. OpenAI Chat
510///                   Completions streaming wire verbatim.
511///   - `kimi`      — Moonshot Kimi (kimi.moonshot.cn) — uses the
512///                   OpenAI-compatible Chat Completions wire format
513///                   verbatim (same chunk shape, same `data: [DONE]`
514///                   sentinel). First-class entry so adopters
515///                   declare intent explicitly; under the hood the
516///                   wire is identical to `openai`.
517///   - `glm`       — Zhipu ChatGLM (open.bigmodel.cn) — same as
518///                   kimi, uses OpenAI-compat wire. First-class
519///                   entry for adopter clarity.
520///   - `anthropic` — `event: content_block_delta` frames terminated
521///                   by `event: message_stop`. Adopter SDKs
522///                   targeting Anthropic Claude consume this shape
523///                   verbatim.
524///
525/// Why kimi + glm as first-class entries (Q3 revision rationale):
526/// Bemarking AI's primary adopter pipelines through Kimi K2.x +
527/// Zhipu GLM-4.x. While the wire IS byte-identical to OpenAI's
528/// Chat Completions streaming, declaring `transport: sse(kimi)` /
529/// `transport: sse(glm)` lets the audit trail + observability
530/// surfaces correlate adopter intent against the underlying
531/// provider — without the adopter having to know that "kimi
532/// happens to be OpenAI-compat on the wire today". The runtime
533/// dispatches kimi + glm to the same `OpenAIDialectAdapter` so
534/// the wire shape stays canonical-OpenAI-bytes.
535///
536/// Open-set adapter pluggability (downstream crates registering
537/// custom dialects) remains explicitly out of scope per the
538/// Axon-for-Axon discipline.
539pub const AXONENDPOINT_TRANSPORT_DIALECTS: &[&str] =
540    &["axon", "openai", "kimi", "glm", "anthropic"];
541
542/// Adopter-facing acceptable values for `keepalive:` field.
543pub const AXONENDPOINT_KEEPALIVE_VALUES: &[&str] = &["5s", "15s", "30s", "60s"];
544
545/// §Fase 32.b D3 — Closed method enum for `method:` field. Adopter-
546/// declarable methods only; HEAD/OPTIONS/CONNECT/TRACE are
547/// runtime-managed (CORS preflight, etc.) and never declared from
548/// source. Closed enum refuses interpretation drift; smart-suggest
549/// catches near-misses at parse time.
550pub const AXONENDPOINT_METHOD_VALUES: &[&str] = &["GET", "POST", "PUT", "DELETE", "PATCH"];
551
552/// §Fase 36.d (D2) — Closed catalog for the `axonendpoint backend:`
553/// declaration. The set is `CANONICAL_PROVIDERS ∪ {auto, stub}`:
554///
555///   - the seven canonical LLM providers — `anthropic`, `gemini`,
556///     `glm`, `kimi`, `ollama`, `openai`, `openrouter` — a concrete,
557///     declared backend that rung 2 of the Fase 36 D1 resolution
558///     ladder fires immediately;
559///   - `auto` — transparent: declaring it is equivalent to omitting
560///     `backend:` entirely (the route resolves down the ladder —
561///     server default → environment-available providers);
562///   - `stub` — the no-op backend, reachable ONLY by an explicit,
563///     written declaration (D5: a silent degradation to `stub` is
564///     forbidden; an explicit opt-in is not).
565///
566/// `axon-frontend` carries zero runtime deps and therefore cannot
567/// import `axon::backends::CANONICAL_PROVIDERS`; this list is a
568/// hand-maintained mirror. The axon-rs drift gate
569/// (`tests/fase36_d_backend_catalog_drift.rs`) asserts the two stay
570/// byte-identical — adding a provider in one place without the other
571/// fails CI.
572pub const AXONENDPOINT_BACKEND_VALUES: &[&str] = &[
573    "anthropic",
574    "auto",
575    "gemini",
576    "glm",
577    "kimi",
578    "ollama",
579    "openai",
580    "openrouter",
581    "stub",
582];
583
584#[inline]
585fn axonendpoint_is_valid_transport(s: &str) -> bool {
586    AXONENDPOINT_TRANSPORT_VALUES.iter().any(|&v| v == s)
587}
588
589#[inline]
590fn axonendpoint_is_valid_method(s: &str) -> bool {
591    AXONENDPOINT_METHOD_VALUES.iter().any(|&v| v == s)
592}
593
594#[inline]
595fn axonendpoint_is_valid_backend(s: &str) -> bool {
596    AXONENDPOINT_BACKEND_VALUES.iter().any(|&v| v == s)
597}
598
599#[inline]
600fn axonendpoint_is_valid_keepalive(s: &str) -> bool {
601    AXONENDPOINT_KEEPALIVE_VALUES.iter().any(|&v| v == s)
602}
603
604/// §Fase 37.y (D2) — Closed type catalog for query parameters.
605///
606/// Query values arrive over HTTP as URL-encoded strings; the catalog
607/// is the set of types axon will validate / coerce them into for the
608/// Request Binding Contract. Hand-curated, intentionally small:
609///   - `Text` — the raw string (always succeeds)
610///   - `Int` — `i64` parseable
611///   - `Float` — `f64` parseable, finite
612///   - `Bool` — case-insensitive `{true, false, 1, 0, yes, no, on, off}`
613///   - `Uuid` — RFC 4122 textual form
614///
615/// Extending the catalog is a future axon-T?nn surface; v1.38.5 ships
616/// the 5 types covering ~95% of REST query patterns. Lists / dates /
617/// datetimes / enums are honest deferrals (see §7 of the plan vivo).
618pub const AXONENDPOINT_QUERY_PARAM_TYPES: &[&str] =
619    &["Text", "Int", "Float", "Bool", "Uuid"];
620
621/// `true` iff `s` is one of the §Fase 37.y (D2) query-param catalog
622/// entries — exact case-sensitive match (axon types are PascalCase).
623#[inline]
624pub(crate) fn axonendpoint_is_valid_query_param_type(s: &str) -> bool {
625    AXONENDPOINT_QUERY_PARAM_TYPES.iter().any(|&v| v == s)
626}
627
628/// §Fase 37.y (D1) — Extract `{name}` placeholder names from an
629/// `axonendpoint` `path:` string, in left-to-right declaration order.
630///
631/// Recognized placeholder grammar (single-segment, no nested braces):
632/// `{NAME}` where `NAME` matches `[A-Za-z_][A-Za-z0-9_]*`. Anything
633/// inside braces that does NOT match the identifier shape is silently
634/// IGNORED — it's either an adopter typo (caught later by axum at
635/// route registration) or a literal brace in the URL pattern.
636///
637/// Returns `Err(duplicate_name)` when the same `{name}` appears more
638/// than once in the path — HTTP route patterns reject duplicates
639/// structurally (`axum` would panic at registration), so surfacing
640/// the error at parse time is the right place.
641///
642/// Pure + total: never panics; deterministic over its single string
643/// argument. Hand-rolled scanner (no regex dep at parser layer).
644///
645/// # Examples
646///
647/// - `"/api/users"` → `Ok(vec![])`
648/// - `"/api/users/{id}"` → `Ok(vec!["id"])`
649/// - `"/api/tenants/{tenant_id}/secrets/{secret_name}"`
650///   → `Ok(vec!["tenant_id", "secret_name"])`
651/// - `"/api/users/{id}/posts/{id}"` → `Err("id")` (duplicate)
652/// - `"/api/{not valid}"` → `Ok(vec![])` (malformed brace content
653///   silently ignored; axum surfaces the error at registration)
654pub(crate) fn extract_path_param_names(path: &str) -> Result<Vec<String>, String> {
655    let mut out: Vec<String> = Vec::new();
656    let bytes = path.as_bytes();
657    let mut i = 0;
658    while i < bytes.len() {
659        if bytes[i] != b'{' {
660            i += 1;
661            continue;
662        }
663        // Find the matching close brace; if none, the open brace is
664        // a literal — leave it alone.
665        let start = i + 1;
666        let mut end = start;
667        while end < bytes.len() && bytes[end] != b'}' {
668            end += 1;
669        }
670        if end == bytes.len() {
671            // Unterminated — give up; downstream parser/runtime
672            // surface the malformed path elsewhere.
673            break;
674        }
675        let raw = &path[start..end];
676        // Validate identifier shape: [A-Za-z_][A-Za-z0-9_]*
677        let valid = !raw.is_empty()
678            && raw.bytes().enumerate().all(|(idx, b)| {
679                if idx == 0 {
680                    b.is_ascii_alphabetic() || b == b'_'
681                } else {
682                    b.is_ascii_alphanumeric() || b == b'_'
683                }
684            });
685        if valid {
686            let name = raw.to_string();
687            if out.iter().any(|existing| existing == &name) {
688                return Err(name);
689            }
690            out.push(name);
691        }
692        i = end + 1;
693    }
694    Ok(out)
695}
696
697/// §Fase 32.g (D8) — Closed capability-slug grammar. Validates a
698/// `requires:` slug per `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$`.
699///
700/// Hand-rolled (no regex dep at parser layer) — each segment must
701/// match `[a-z][a-z0-9_]*` and segments are joined by single dots.
702/// Public so the runtime mirror (`axon::auth_scope`) reuses the same
703/// predicate without duplicating the rule.
704///
705/// Examples valid: `admin`, `legal.read`, `hipaa.phi.read`,
706/// `bank.officer.senior`, `a`, `a_b`, `a1`.
707/// Examples invalid: empty, `Admin` (uppercase), `1admin` (digit
708/// first), `bank-officer` (hyphen), `bank..a` (empty segment),
709/// `.admin`, `admin.`, `admin..` .
710pub fn is_valid_capability_slug(slug: &str) -> bool {
711    if slug.is_empty() {
712        return false;
713    }
714    for segment in slug.split('.') {
715        if !is_valid_slug_segment(segment) {
716            return false;
717        }
718    }
719    true
720}
721
722fn is_valid_slug_segment(seg: &str) -> bool {
723    let mut chars = seg.chars();
724    let first = match chars.next() {
725        Some(c) => c,
726        None => return false,
727    };
728    if !first.is_ascii_lowercase() {
729        return false;
730    }
731    chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
732}
733
734// ════════════════════════════════════════════════════════════════════
735//  §Fase 37.y (D1) — `extract_path_param_names` unit tests
736// ════════════════════════════════════════════════════════════════════
737
738// ════════════════════════════════════════════════════════════════════
739//  §Fase 37.y (D2) — `axonendpoint_is_valid_query_param_type` + the
740//  inline `query: { … }` parser, end-to-end through the lexer.
741// ════════════════════════════════════════════════════════════════════
742
743#[cfg(test)]
744mod query_param_catalog_tests {
745    use super::{axonendpoint_is_valid_query_param_type, AXONENDPOINT_QUERY_PARAM_TYPES};
746
747    #[test]
748    fn accepts_every_catalog_entry() {
749        for ty in AXONENDPOINT_QUERY_PARAM_TYPES {
750            assert!(
751                axonendpoint_is_valid_query_param_type(ty),
752                "catalog entry `{ty}` must validate"
753            );
754        }
755    }
756
757    #[test]
758    fn rejects_off_catalog_types() {
759        for off in &[
760            "Timestamp",    // not in v1.38.5 — list/dates deferred
761            "Date",
762            "DateTime",
763            "List<Text>",   // multi-value query params deferred (§7)
764            "Jsonb",        // store-only types not query-applicable
765            "Bytea",
766            "text",         // lowercase rejected (axon types are PascalCase)
767            "TEXT",
768            "Number",       // not in axon's type catalog at all
769            "",             // empty
770            " ",            // whitespace
771        ] {
772            assert!(
773                !axonendpoint_is_valid_query_param_type(off),
774                "off-catalog `{off}` must reject"
775            );
776        }
777    }
778
779    #[test]
780    fn catalog_size_matches_design() {
781        // The plan vivo D2 states a closed 5-type catalog. A future
782        // axon-T?nn surface may extend it; that requires updating BOTH
783        // the catalog AND the plan vivo §7 honest-scope note.
784        assert_eq!(AXONENDPOINT_QUERY_PARAM_TYPES.len(), 5);
785    }
786}
787
788#[cfg(test)]
789mod query_param_parser_tests {
790    use crate::lexer::Lexer;
791    use crate::parser::Parser;
792
793    fn parse_endpoint_source(src: &str) -> Result<crate::ast::AxonEndpointDefinition, String> {
794        let tokens = Lexer::new(src, "test.axon")
795            .tokenize()
796            .map_err(|e| format!("lex: {}", e.message))?;
797        let mut parser = Parser::new(tokens);
798        let program = parser.parse().map_err(|e| format!("parse: {}", e.message))?;
799        program
800            .declarations
801            .into_iter()
802            .find_map(|d| match d {
803                crate::ast::Declaration::AxonEndpoint(e) => Some(e),
804                _ => None,
805            })
806            .ok_or_else(|| "no axonendpoint in program".to_string())
807    }
808
809    #[test]
810    fn endpoint_with_no_query_block_keeps_empty_vec() {
811        let src = r#"
812            axonendpoint write_secret {
813                method: POST
814                path: "/api/users"
815                body: SecretWriteRequest
816                execute: WriteSecret
817            }
818        "#;
819        let ep = parse_endpoint_source(src).expect("parses");
820        assert!(
821            ep.query_params.is_empty(),
822            "D5 — no `query:` block ⇒ empty query_params"
823        );
824    }
825
826    #[test]
827    fn single_query_param_required() {
828        let src = r#"
829            axonendpoint list_users {
830                method: GET
831                path: "/api/users"
832                query: { status: Text }
833                execute: ListUsers
834            }
835        "#;
836        let ep = parse_endpoint_source(src).expect("parses");
837        assert_eq!(ep.query_params.len(), 1);
838        assert_eq!(ep.query_params[0].name, "status");
839        assert_eq!(ep.query_params[0].type_expr.name, "Text");
840        assert!(!ep.query_params[0].type_expr.optional);
841    }
842
843    #[test]
844    fn optional_query_param_via_question_suffix() {
845        let src = r#"
846            axonendpoint list_users {
847                method: GET
848                path: "/api/users"
849                query: { limit: Int? }
850                execute: ListUsers
851            }
852        "#;
853        let ep = parse_endpoint_source(src).expect("parses");
854        assert_eq!(ep.query_params.len(), 1);
855        assert_eq!(ep.query_params[0].name, "limit");
856        assert_eq!(ep.query_params[0].type_expr.name, "Int");
857        assert!(
858            ep.query_params[0].type_expr.optional,
859            "`?` suffix sets optional"
860        );
861    }
862
863    #[test]
864    fn multiple_query_params_preserve_declaration_order() {
865        let src = r#"
866            axonendpoint search {
867                method: GET
868                path: "/api/search"
869                query: { q: Text, page: Int?, limit: Int?, exact: Bool? }
870                execute: Search
871            }
872        "#;
873        let ep = parse_endpoint_source(src).expect("parses");
874        let names: Vec<&str> = ep.query_params.iter().map(|f| f.name.as_str()).collect();
875        assert_eq!(names, vec!["q", "page", "limit", "exact"]);
876        let types: Vec<&str> = ep
877            .query_params
878            .iter()
879            .map(|f| f.type_expr.name.as_str())
880            .collect();
881        assert_eq!(types, vec!["Text", "Int", "Int", "Bool"]);
882        let optionals: Vec<bool> = ep
883            .query_params
884            .iter()
885            .map(|f| f.type_expr.optional)
886            .collect();
887        assert_eq!(optionals, vec![false, true, true, true]);
888    }
889
890    #[test]
891    fn duplicate_query_param_is_parse_error() {
892        let src = r#"
893            axonendpoint bad {
894                method: GET
895                path: "/api/x"
896                query: { name: Text, name: Int? }
897                execute: Bad
898            }
899        "#;
900        let err = parse_endpoint_source(src).expect_err("must fail");
901        assert!(
902            err.contains("duplicate query param 'name'"),
903            "error must name the duplicate. Got: {err}"
904        );
905    }
906
907    #[test]
908    fn off_catalog_type_with_smart_suggest_hint() {
909        // `Strng` is one edit away from `Text` (would suggest `Text`?
910        // Actually edit distance to `Text` is 4; to `Int` is 5. Likely
911        // no smart suggestion within distance 2. The error still names
912        // the catalog explicitly.)
913        let src = r#"
914            axonendpoint bad {
915                method: GET
916                path: "/api/x"
917                query: { value: Strng }
918                execute: Bad
919            }
920        "#;
921        let err = parse_endpoint_source(src).expect_err("must fail");
922        assert!(
923            err.contains("unsupported type 'Strng'"),
924            "error must name the bad type. Got: {err}"
925        );
926        assert!(
927            err.contains("Expected one of: Text | Int | Float | Bool | Uuid"),
928            "error must list the closed catalog. Got: {err}"
929        );
930    }
931
932    #[test]
933    fn close_typo_gets_did_you_mean_hint() {
934        // `Txt` → edit distance 1 from `Text` → smart-suggest should
935        // surface the hint.
936        let src = r#"
937            axonendpoint bad {
938                method: GET
939                path: "/api/x"
940                query: { value: Txt }
941                execute: Bad
942            }
943        "#;
944        let err = parse_endpoint_source(src).expect_err("must fail");
945        assert!(
946            err.contains("Did you mean") && err.contains("`Text`"),
947            "smart-suggest must hint `Text`. Got: {err}"
948        );
949    }
950
951    #[test]
952    fn every_catalog_type_parses_cleanly() {
953        // Round-trip smoke for all 5 catalog entries.
954        for ty in &["Text", "Int", "Float", "Bool", "Uuid"] {
955            let src = format!(
956                r#"
957                    axonendpoint x {{
958                        method: GET
959                        path: "/api/x"
960                        query: {{ v: {ty} }}
961                        execute: X
962                    }}
963                "#
964            );
965            let ep = parse_endpoint_source(&src)
966                .unwrap_or_else(|e| panic!("`{ty}` should parse: {e}"));
967            assert_eq!(ep.query_params[0].type_expr.name, *ty);
968        }
969    }
970
971    #[test]
972    fn comma_optional_between_params() {
973        // The plan vivo design accepts both comma-separated and
974        // whitespace-separated query params (existing parser style is
975        // forgiving). Whitespace-only:
976        let src = r#"
977            axonendpoint x {
978                method: GET
979                path: "/api/x"
980                query: { a: Text b: Int? }
981                execute: X
982            }
983        "#;
984        let ep = parse_endpoint_source(src).expect("parses without commas");
985        assert_eq!(ep.query_params.len(), 2);
986    }
987
988    // ─── Robustness hardening (37.y.2 100% robust closure) ──────────
989
990    #[test]
991    fn double_query_block_is_parse_error() {
992        // An adopter who copy-pastes the `query:` block twice should
993        // see a clear parse error, not a silent merge that produces
994        // an unexpectedly-augmented endpoint with both blocks fused.
995        let src = r#"
996            axonendpoint x {
997                method: GET
998                path: "/api/x"
999                query: { a: Text }
1000                query: { b: Int? }
1001                execute: X
1002            }
1003        "#;
1004        let err = parse_endpoint_source(src).expect_err("must fail");
1005        assert!(
1006            err.contains("declares `query: { … }` more than once"),
1007            "error must call out the duplicate block. Got: {err}"
1008        );
1009        assert!(
1010            err.contains("combine all params into a single block"),
1011            "error must hint the canonical fix. Got: {err}"
1012        );
1013    }
1014
1015    #[test]
1016    fn optional_generic_type_is_parse_error_with_canonical_hint() {
1017        // `Optional<Text>` is the wrong way to declare an optional
1018        // query param. The canonical syntax is `Text?` (the `?`
1019        // suffix). The error must surface this with a literal example.
1020        let src = r#"
1021            axonendpoint x {
1022                method: GET
1023                path: "/api/x"
1024                query: { value: Optional<Text> }
1025                execute: X
1026            }
1027        "#;
1028        let err = parse_endpoint_source(src).expect_err("must fail");
1029        assert!(
1030            err.contains("generic type `Optional<Text>`"),
1031            "error must name the generic type literally. Got: {err}"
1032        );
1033        assert!(
1034            err.contains("Use `Text?` (the `?` suffix)"),
1035            "error must hint the canonical `Text?` syntax. Got: {err}"
1036        );
1037    }
1038
1039    #[test]
1040    fn list_generic_type_is_parse_error_with_deferral_hint() {
1041        // Multi-value query params (`?tag=a&tag=b`) are honest-
1042        // deferred per the plan vivo §7. Adopters who write
1043        // `List<Text>` should see a clear error explaining the
1044        // deferral, not a confusing "type `List` not in catalog".
1045        let src = r#"
1046            axonendpoint x {
1047                method: GET
1048                path: "/api/x"
1049                query: { tags: List<Text> }
1050                execute: X
1051            }
1052        "#;
1053        let err = parse_endpoint_source(src).expect_err("must fail");
1054        assert!(
1055            err.contains("generic type `List<Text>`"),
1056            "error must name the generic type. Got: {err}"
1057        );
1058        assert!(
1059            err.contains("Multi-value query params")
1060                && err.contains("honest-deferred"),
1061            "error must mention the multi-value deferral. Got: {err}"
1062        );
1063    }
1064
1065    #[test]
1066    fn other_generic_types_caught_generically() {
1067        // Generic types beyond `Optional` and `List` get the
1068        // generic-rejection message without a canonical-syntax hint
1069        // (the catalog list is the canonical guidance).
1070        let src = r#"
1071            axonendpoint x {
1072                method: GET
1073                path: "/api/x"
1074                query: { value: Stream<Int> }
1075                execute: X
1076            }
1077        "#;
1078        let err = parse_endpoint_source(src).expect_err("must fail");
1079        assert!(
1080            err.contains("generic type `Stream<Int>`"),
1081            "error must name the generic type. Got: {err}"
1082        );
1083        assert!(
1084            err.contains("Text | Int | Float | Bool | Uuid"),
1085            "error must list the closed catalog. Got: {err}"
1086        );
1087    }
1088
1089    #[test]
1090    fn uuid_optional_parses_cleanly() {
1091        // Hardening companion — `Uuid?` is in the catalog AND
1092        // optional. The two features compose without surprise.
1093        let src = r#"
1094            axonendpoint find {
1095                method: GET
1096                path: "/api/x"
1097                query: { after: Uuid? }
1098                execute: Find
1099            }
1100        "#;
1101        let ep = parse_endpoint_source(src).expect("parses");
1102        assert_eq!(ep.query_params.len(), 1);
1103        assert_eq!(ep.query_params[0].name, "after");
1104        assert_eq!(ep.query_params[0].type_expr.name, "Uuid");
1105        assert!(ep.query_params[0].type_expr.optional);
1106        assert_eq!(ep.query_params[0].type_expr.generic_param, "");
1107    }
1108
1109    #[test]
1110    fn empty_query_block_yields_empty_vec() {
1111        // `query: { }` is grammatically valid but semantically a
1112        // no-op (equivalent to omitting the block). Don't error;
1113        // just record an empty Vec.
1114        let src = r#"
1115            axonendpoint x {
1116                method: GET
1117                path: "/api/x"
1118                query: { }
1119                execute: X
1120            }
1121        "#;
1122        let ep = parse_endpoint_source(src).expect("empty block parses");
1123        assert!(ep.query_params.is_empty());
1124    }
1125
1126    #[test]
1127    fn kivi_secret_write_path_plus_query() {
1128        // Combined path-param + query-param test: an endpoint that
1129        // takes IDs in the URL AND optional filters in the query
1130        // string. This is the natural REST shape Fase 37.y serves.
1131        let src = r#"
1132            axonendpoint write_secret {
1133                method: POST
1134                path: "/api/tenants/{tenant_id}/secrets/{secret_name}"
1135                query: { dry_run: Bool?, overwrite: Bool? }
1136                body: SecretWriteRequest
1137                execute: WriteSecret
1138            }
1139        "#;
1140        let ep = parse_endpoint_source(src).expect("parses");
1141        // Path params populated (from 37.y.1):
1142        assert_eq!(ep.path_params, vec!["tenant_id", "secret_name"]);
1143        // Query params populated (from this sub-fase 37.y.2):
1144        assert_eq!(ep.query_params.len(), 2);
1145        assert_eq!(ep.query_params[0].name, "dry_run");
1146        assert_eq!(ep.query_params[0].type_expr.name, "Bool");
1147        assert!(ep.query_params[0].type_expr.optional);
1148        assert_eq!(ep.query_params[1].name, "overwrite");
1149        // Body still works:
1150        assert_eq!(ep.body_type, "SecretWriteRequest");
1151    }
1152}
1153
1154#[cfg(test)]
1155mod path_param_extraction_tests {
1156    use super::extract_path_param_names;
1157
1158    #[test]
1159    fn empty_path_no_placeholders() {
1160        assert_eq!(extract_path_param_names("/api/users"), Ok(vec![]));
1161        assert_eq!(extract_path_param_names("/"), Ok(vec![]));
1162        assert_eq!(extract_path_param_names(""), Ok(vec![]));
1163    }
1164
1165    #[test]
1166    fn single_placeholder() {
1167        assert_eq!(
1168            extract_path_param_names("/api/users/{id}"),
1169            Ok(vec!["id".to_string()])
1170        );
1171    }
1172
1173    #[test]
1174    fn multiple_placeholders_in_declaration_order() {
1175        assert_eq!(
1176            extract_path_param_names(
1177                "/api/tenants/{tenant_id}/secrets/{secret_name}"
1178            ),
1179            Ok(vec![
1180                "tenant_id".to_string(),
1181                "secret_name".to_string(),
1182            ])
1183        );
1184    }
1185
1186    #[test]
1187    fn kivi_chat_history_path_pattern() {
1188        // The exact pattern the kivi adopter reported (2026-05-20):
1189        // POST /api/tenants/{tenant_id}/secrets/{secret_name}
1190        // Both names extracted in source order.
1191        let names = extract_path_param_names(
1192            "/api/tenants/{tenant_id}/secrets/{secret_name}",
1193        );
1194        assert_eq!(
1195            names,
1196            Ok(vec![
1197                "tenant_id".to_string(),
1198                "secret_name".to_string(),
1199            ])
1200        );
1201    }
1202
1203    #[test]
1204    fn duplicate_placeholder_returns_err() {
1205        assert_eq!(
1206            extract_path_param_names("/api/users/{id}/posts/{id}"),
1207            Err("id".to_string())
1208        );
1209    }
1210
1211    #[test]
1212    fn underscore_and_numeric_in_name() {
1213        assert_eq!(
1214            extract_path_param_names("/api/{user_id}/items/{item_2}"),
1215            Ok(vec!["user_id".to_string(), "item_2".to_string()])
1216        );
1217    }
1218
1219    #[test]
1220    fn leading_underscore_accepted() {
1221        // Identifiers in HTTP paths often start with letters but the
1222        // grammar permits leading underscore (parity with Rust identifier
1223        // rules). The flow parameter name on the binding side has to
1224        // match exactly, so adopters with `_internal_id` in the path
1225        // can pair it with a same-named flow param.
1226        assert_eq!(
1227            extract_path_param_names("/api/{_internal}"),
1228            Ok(vec!["_internal".to_string()])
1229        );
1230    }
1231
1232    #[test]
1233    fn malformed_placeholder_silently_ignored() {
1234        // Content inside `{...}` that does not match the identifier
1235        // grammar is skipped at this layer. axum surfaces the route
1236        // registration failure if the literal text is invalid.
1237        assert_eq!(
1238            extract_path_param_names("/api/{not valid}"),
1239            Ok(vec![])
1240        );
1241        // Empty braces — same: skip silently.
1242        assert_eq!(extract_path_param_names("/api/{}"), Ok(vec![]));
1243        // Mixed: malformed brace skipped, valid placeholder kept.
1244        assert_eq!(
1245            extract_path_param_names("/api/{tenant id}/users/{id}"),
1246            Ok(vec!["id".to_string()])
1247        );
1248    }
1249
1250    #[test]
1251    fn unterminated_brace_returns_clean() {
1252        // Open brace with no close brace — give up without panicking.
1253        // (axum surfaces the malformed-route error at registration.)
1254        assert_eq!(extract_path_param_names("/api/{id"), Ok(vec![]));
1255    }
1256
1257    #[test]
1258    fn placeholders_at_path_boundaries() {
1259        // Placeholder as the very first segment AND the very last
1260        // segment — both should be extracted.
1261        assert_eq!(
1262            extract_path_param_names("{prefix}/api/users/{id}"),
1263            Ok(vec!["prefix".to_string(), "id".to_string()])
1264        );
1265        assert_eq!(
1266            extract_path_param_names("/api/{id}"),
1267            Ok(vec!["id".to_string()])
1268        );
1269    }
1270
1271    #[test]
1272    fn deduplication_detects_non_adjacent_duplicates() {
1273        // The duplicate-detection sweep is global, not just adjacent.
1274        assert_eq!(
1275            extract_path_param_names(
1276                "/api/orgs/{org_id}/teams/{team_id}/repos/{org_id}"
1277            ),
1278            Err("org_id".to_string())
1279        );
1280    }
1281
1282    #[test]
1283    fn never_panics_on_arbitrary_input() {
1284        // Light fuzz: a handful of weird inputs return cleanly.
1285        for input in &[
1286            "{",
1287            "}",
1288            "{}",
1289            "{{}}",
1290            "{{{",
1291            "/api/{}/{id}",
1292            "////",
1293            "\u{1F4A1}",        // emoji (lightbulb)
1294            "\u{0000}",         // null byte
1295        ] {
1296            let _ = extract_path_param_names(input); // must not panic
1297        }
1298    }
1299}
1300
1301#[cfg(test)]
1302mod capability_slug_tests {
1303    use super::is_valid_capability_slug;
1304
1305    #[test]
1306    fn accepts_canonical_examples() {
1307        assert!(is_valid_capability_slug("admin"));
1308        assert!(is_valid_capability_slug("legal.read"));
1309        assert!(is_valid_capability_slug("hipaa.phi.read"));
1310        assert!(is_valid_capability_slug("bank.officer.senior"));
1311        assert!(is_valid_capability_slug("a"));
1312        assert!(is_valid_capability_slug("a_b"));
1313        assert!(is_valid_capability_slug("a1"));
1314        assert!(is_valid_capability_slug("a.b1_c"));
1315    }
1316
1317    #[test]
1318    fn rejects_empty_string() {
1319        assert!(!is_valid_capability_slug(""));
1320    }
1321
1322    #[test]
1323    fn rejects_uppercase() {
1324        assert!(!is_valid_capability_slug("Admin"));
1325        assert!(!is_valid_capability_slug("admin.READ"));
1326    }
1327
1328    #[test]
1329    fn rejects_digit_first() {
1330        assert!(!is_valid_capability_slug("1admin"));
1331        assert!(!is_valid_capability_slug("admin.1read"));
1332    }
1333
1334    #[test]
1335    fn rejects_hyphen() {
1336        assert!(!is_valid_capability_slug("bank-officer"));
1337    }
1338
1339    #[test]
1340    fn rejects_empty_segments() {
1341        assert!(!is_valid_capability_slug("bank..a"));
1342        assert!(!is_valid_capability_slug(".admin"));
1343        assert!(!is_valid_capability_slug("admin."));
1344    }
1345
1346    #[test]
1347    fn rejects_special_chars() {
1348        assert!(!is_valid_capability_slug("admin@read"));
1349        assert!(!is_valid_capability_slug("admin/read"));
1350        assert!(!is_valid_capability_slug("admin read"));
1351    }
1352}
1353
1354// ── Parser ───────────────────────────────────────────────────────────────────
1355
1356pub struct Parser {
1357    tokens: Vec<Token>,
1358    pos: usize,
1359    /// Fase 14.a — leading trivia parallel array, indexed by the
1360    /// effective-token position. `leading_trivia[i]` is the comment
1361    /// trivia that appeared between the previous effective token (or
1362    /// file start) and `tokens[i]`.
1363    leading_trivia: Vec<Vec<Trivia>>,
1364    /// Fase 14.a — trailing trivia parallel array. `trailing_trivia[i]`
1365    /// is the comment trivia on the same line as `tokens[i]`, before
1366    /// the next effective token. Populated by the constructor.
1367    trailing_trivia: Vec<Vec<Trivia>>,
1368    /// Fase 17.a — side-channel for tagging let value_kind. Set by
1369    /// `parse_let_atom` / `parse_let_value_expr` as they descend; read
1370    /// at the end of `parse_let` and stored on the LetStatement.
1371    last_let_value_kind: String,
1372    /// Fase 19.e — loop nesting depth for break/continue scope check.
1373    /// Incremented at the start of `parse_for_in`, decremented after.
1374    /// `parse_break`/`parse_continue` raise ParseError when this is
1375    /// zero (the keyword has no meaning outside a loop body).
1376    loop_depth: u32,
1377    /// §Fase 28.d — Optional source text + filename for the rustc-
1378    /// style source-context block on `ParseError`. Set via the
1379    /// fluent `Parser::with_source` builder; default `None` keeps
1380    /// existing callers (`Parser::new(tokens).parse()`) emitting
1381    /// the legacy single-line shape.
1382    source: Option<String>,
1383    filename: String,
1384}
1385
1386impl Parser {
1387    pub fn new(raw_tokens: Vec<Token>) -> Self {
1388        // ── Fase 14.a — split the raw token stream into:
1389        //   - effective tokens the grammar consumes (cursor advances
1390        //     over these as before),
1391        //   - parallel `leading_trivia` / `trailing_trivia` arrays
1392        //     indexed by effective-token position.
1393        // Comments on a fresh line attach as leading trivia of the
1394        // next effective token; comments on the same line as an
1395        // effective token attach as trailing trivia of that token.
1396        // Roslyn/Swift convention.
1397        let mut effective: Vec<Token> = Vec::with_capacity(raw_tokens.len());
1398        let mut leading: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1399        let mut trailing: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1400
1401        let mut pending_leading: Vec<Trivia> = Vec::new();
1402        let mut last_effective_line: i64 = -1;
1403        for tok in raw_tokens {
1404            if is_comment_token(&tok.ttype) {
1405                let kind = token_to_trivia_kind(&tok.ttype)
1406                    .expect("comment token must map to a trivia kind");
1407                let triv = Trivia {
1408                    kind,
1409                    text: tok.value,
1410                    line: tok.line,
1411                    column: tok.column,
1412                };
1413                if !effective.is_empty() && (tok.line as i64) == last_effective_line {
1414                    trailing.last_mut().unwrap().push(triv);
1415                } else {
1416                    pending_leading.push(triv);
1417                }
1418            } else {
1419                last_effective_line = tok.line as i64;
1420                effective.push(tok);
1421                leading.push(std::mem::take(&mut pending_leading));
1422                trailing.push(Vec::new());
1423            }
1424        }
1425
1426        Parser {
1427            tokens: effective,
1428            pos: 0,
1429            leading_trivia: leading,
1430            trailing_trivia: trailing,
1431            last_let_value_kind: "literal".to_string(),
1432            loop_depth: 0,
1433            source: None,
1434            filename: "<source>".to_string(),
1435        }
1436    }
1437
1438    /// §Fase 28.d — Fluent attach of source text + filename for
1439    /// rustc-style source-context blocks on emitted `ParseError`s.
1440    /// Returns `self` so it chains with `.parse_with_recovery()`:
1441    ///
1442    /// ```ignore
1443    /// let result = Parser::new(tokens)
1444    ///     .with_source(src, "foo.axon")
1445    ///     .parse_with_recovery();
1446    /// ```
1447    ///
1448    /// No-op of any other behaviour — pure metadata attach.
1449    #[must_use]
1450    pub fn with_source(mut self, source: &str, filename: &str) -> Self {
1451        self.source = Some(source.to_string());
1452        self.filename = filename.to_string();
1453        self
1454    }
1455
1456    // ── public API ───────────────────────────────────────────────
1457
1458    pub fn parse(&mut self) -> Result<Program, ParseError> {
1459        let mut program = Program {
1460            declarations: Vec::new(),
1461            declaration_trivia: Vec::new(),
1462            loc: Loc { line: 1, column: 1 },
1463        };
1464        while !self.check(TokenType::Eof) {
1465            // Capture trivia around the declaration. `start_pos` is
1466            // the effective-token position of the declaration's first
1467            // token; that position carries the leading trivia. After
1468            // parsing, `pos - 1` is the last token consumed; that
1469            // position carries the trailing trivia.
1470            let start_pos = self.pos;
1471            let mut decl = match self.parse_declaration() {
1472                Ok(d) => d,
1473                Err(e) => return Err(self.attach_source_to_error(e)),
1474            };
1475            let end_pos = self.pos.saturating_sub(1);
1476            let leading = self
1477                .leading_trivia
1478                .get(start_pos)
1479                .cloned()
1480                .unwrap_or_default();
1481            let trailing = self
1482                .trailing_trivia
1483                .get(end_pos)
1484                .cloned()
1485                .unwrap_or_default();
1486            // Fase 14.b — also copy trivia into the per-struct fields on
1487            // the declaration so consumers can read `flow.leading_trivia`
1488            // directly without going through `program.declaration_trivia[i]`.
1489            // The side-channel is preserved for backward compat with
1490            // 14.a callers and as a flat enumeration source.
1491            attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1492            program.declarations.push(decl);
1493            program
1494                .declaration_trivia
1495                .push(DeclarationTrivia { leading, trailing });
1496        }
1497        Ok(program)
1498    }
1499
1500    // ── §Fase 28.c — recovery-mode parse ─────────────────────────
1501    //
1502    // Mirror of Python's `Parser.parse_with_recovery` from
1503    // `axon/compiler/parser.py`. Wraps `parse_declaration` in a
1504    // try/recover loop: on any `ParseError` the error is appended to
1505    // the list and the cursor advances to the next sync point, then
1506    // parsing resumes. The two stacks must produce structurally
1507    // identical error lists on the same input — that is the cross-
1508    // stack drift gate (D7). See the test module
1509    // `tests::fase28_recovery_tests` and Python-side
1510    // `tests/test_fase28_parser_recovery.py`.
1511
1512    /// Recovery-mode parse. Collects every parse error in source
1513    /// order; the existing `parse()` API remains fail-fast (D9).
1514    ///
1515    /// # Recovery contract (D2)
1516    ///
1517    /// On `ParseError`:
1518    ///   1. Push the error onto `errors`.
1519    ///   2. If the cursor is already on a top-level declaration
1520    ///      keyword (and brace-depth ≤ 0), do not consume — the
1521    ///      caller should retry the declaration parse from here.
1522    ///      Otherwise advance one token to make progress, then
1523    ///      walk to the next sync point.
1524    ///   3. Resume the outer loop.
1525    ///
1526    /// Sync points: top-level declaration keyword at brace-depth ≤ 0,
1527    /// or EOF. Negative depths are treated identically to ≤ 0 — the
1528    /// walker keeps walking through over-balanced `}` rather than
1529    /// pretending a closing brace is itself a sync point (which would
1530    /// emit a ghost "Unexpected token at top level" error in the
1531    /// outer loop).
1532    pub fn parse_with_recovery(&mut self) -> ParseResult {
1533        let mut program = Program {
1534            declarations: Vec::new(),
1535            declaration_trivia: Vec::new(),
1536            loc: Loc { line: 1, column: 1 },
1537        };
1538        let mut errors: Vec<ParseError> = Vec::new();
1539
1540        while !self.check(TokenType::Eof) {
1541            let start_pos = self.pos;
1542            match self.parse_declaration() {
1543                Ok(mut decl) => {
1544                    let end_pos = self.pos.saturating_sub(1);
1545                    let leading = self
1546                        .leading_trivia
1547                        .get(start_pos)
1548                        .cloned()
1549                        .unwrap_or_default();
1550                    let trailing = self
1551                        .trailing_trivia
1552                        .get(end_pos)
1553                        .cloned()
1554                        .unwrap_or_default();
1555                    attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1556                    program.declarations.push(decl);
1557                    program
1558                        .declaration_trivia
1559                        .push(DeclarationTrivia { leading, trailing });
1560                }
1561                Err(err) => {
1562                    // §Fase 28.d — attach source-context block when a
1563                    // source has been provided via `with_source(...)`;
1564                    // otherwise the error keeps its single-line shape.
1565                    errors.push(self.attach_source_to_error(err));
1566                    // Make progress. If parse_declaration returned
1567                    // immediately on the same token (e.g. unknown
1568                    // top-level token), we MUST advance at least one
1569                    // token to avoid an infinite loop.
1570                    if self.pos == start_pos && !self.check(TokenType::Eof) {
1571                        self.advance();
1572                    }
1573                    self.advance_to_sync_point();
1574                }
1575            }
1576        }
1577
1578        ParseResult { program, errors }
1579    }
1580
1581    /// §Fase 28.d — Decorate a `ParseError` with a `SourceSnippet`
1582    /// when the parser has source context attached, otherwise return
1583    /// the error unchanged. Idempotent: if the error already carries
1584    /// a snippet, this overwrites it with the parser's source.
1585    fn attach_source_to_error(&self, err: ParseError) -> ParseError {
1586        match &self.source {
1587            Some(src) if err.line >= 1 => err.attach_source(src, &self.filename),
1588            _ => err,
1589        }
1590    }
1591
1592    /// §Fase 28.c — Walk the cursor forward until the next sync
1593    /// point (top-level declaration keyword at brace-depth ≤ 0) or
1594    /// EOF. Used by `parse_with_recovery` to skip the malformed
1595    /// remainder of a failed declaration.
1596    fn advance_to_sync_point(&mut self) {
1597        let mut depth: i32 = 0;
1598        while !self.check(TokenType::Eof) {
1599            let tt = self.current().ttype.clone();
1600            // Sync at top-level keywords when depth ≤ 0. We do not
1601            // consume the keyword — the outer loop will dispatch on
1602            // it.
1603            if is_top_level_decl_kw_for_recovery(&tt) && depth <= 0 {
1604                return;
1605            }
1606            if matches!(tt, TokenType::LBrace) {
1607                depth += 1;
1608            } else if matches!(tt, TokenType::RBrace) {
1609                depth -= 1;
1610            }
1611            self.advance();
1612        }
1613    }
1614
1615    // ── token helpers ────────────────────────────────────────────
1616
1617    fn current(&self) -> &Token {
1618        if self.pos >= self.tokens.len() {
1619            self.tokens.last().unwrap() // EOF sentinel
1620        } else {
1621            &self.tokens[self.pos]
1622        }
1623    }
1624
1625    fn advance(&mut self) -> &Token {
1626        let idx = self.pos;
1627        if self.pos < self.tokens.len() {
1628            self.pos += 1;
1629        }
1630        &self.tokens[idx]
1631    }
1632
1633    fn check(&self, tt: TokenType) -> bool {
1634        self.current().ttype == tt
1635    }
1636
1637    fn consume(&mut self, expected: TokenType) -> Result<Token, ParseError> {
1638        let tok = self.current().clone();
1639        if tok.ttype != expected {
1640            return Err(ParseError {
1641                message: format!(
1642                    "Expected {:?}, found {:?}('{}')",
1643                    expected, tok.ttype, tok.value
1644                ),
1645                line: tok.line,
1646                column: tok.column,
1647                            ..Default::default()
1648            });
1649        }
1650        self.pos += 1;
1651        Ok(tok)
1652    }
1653
1654    /// §Fase 41.b — build a `ParseError` at the current token's location.
1655    fn error(&self, message: &str) -> ParseError {
1656        let tok = self.current();
1657        ParseError { message: message.to_string(), line: tok.line, column: tok.column, ..Default::default() }
1658    }
1659
1660    /// Consume any identifier or keyword-used-as-value.
1661    fn consume_any_ident_or_kw(&mut self) -> Result<Token, ParseError> {
1662        let tok = self.current().clone();
1663        match tok.ttype {
1664            TokenType::Identifier
1665            | TokenType::Bool
1666            | TokenType::StringLit
1667            | TokenType::Integer
1668            | TokenType::Float => {
1669                self.pos += 1;
1670                Ok(tok)
1671            }
1672            _ => {
1673                // Allow any keyword token whose value is alphabetic
1674                if !tok.value.is_empty()
1675                    && tok.value.chars().all(|c| c.is_alphanumeric() || c == '_')
1676                    && tok.ttype != TokenType::Eof
1677                {
1678                    self.pos += 1;
1679                    Ok(tok)
1680                } else {
1681                    Err(ParseError {
1682                        message: format!(
1683                            "Expected identifier or keyword value, found {:?}('{}')",
1684                            tok.ttype, tok.value
1685                        ),
1686                        line: tok.line,
1687                        column: tok.column,
1688                                            ..Default::default()
1689                    })
1690                }
1691            }
1692        }
1693    }
1694
1695    fn consume_number(&mut self) -> Result<f64, ParseError> {
1696        let tok = self.current().clone();
1697        match tok.ttype {
1698            TokenType::Float | TokenType::Integer => {
1699                self.pos += 1;
1700                tok.value.parse::<f64>().map_err(|_| ParseError {
1701                    message: format!("Invalid number '{}'", tok.value),
1702                    line: tok.line,
1703                    column: tok.column,
1704                                    ..Default::default()
1705                })
1706            }
1707            _ => Err(ParseError {
1708                message: format!("Expected number, found {:?}('{}')", tok.ttype, tok.value),
1709                line: tok.line,
1710                column: tok.column,
1711                            ..Default::default()
1712            }),
1713        }
1714    }
1715
1716    fn parse_bool(&mut self) -> Result<bool, ParseError> {
1717        let tok = self.consume(TokenType::Bool)?;
1718        Ok(tok.value == "true")
1719    }
1720
1721    fn loc_of(&self, tok: &Token) -> Loc {
1722        Loc {
1723            line: tok.line,
1724            column: tok.column,
1725        }
1726    }
1727
1728    fn check_comparison(&self) -> bool {
1729        matches!(
1730            self.current().ttype,
1731            TokenType::Lt
1732                | TokenType::Gt
1733                | TokenType::Lte
1734                | TokenType::Gte
1735                | TokenType::Eq
1736                | TokenType::Neq
1737        )
1738    }
1739
1740    fn check_run_modifier(&self) -> bool {
1741        matches!(
1742            self.current().ttype,
1743            TokenType::As
1744                | TokenType::Within
1745                | TokenType::ConstrainedBy
1746                | TokenType::OnFailure
1747                | TokenType::OutputTo
1748                | TokenType::Effort
1749        )
1750    }
1751
1752    // ── list helpers ─────────────────────────────────────────────
1753
1754    fn parse_string_list(&mut self) -> Result<Vec<String>, ParseError> {
1755        self.consume(TokenType::LBracket)?;
1756        let mut items = Vec::new();
1757        items.push(self.consume(TokenType::StringLit)?.value);
1758        while self.check(TokenType::Comma) {
1759            self.advance();
1760            items.push(self.consume(TokenType::StringLit)?.value);
1761        }
1762        self.consume(TokenType::RBracket)?;
1763        Ok(items)
1764    }
1765
1766    fn parse_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1767        let mut names = Vec::new();
1768        names.push(self.consume(TokenType::Identifier)?.value);
1769        while self.check(TokenType::Comma) {
1770            self.advance();
1771            names.push(self.consume(TokenType::Identifier)?.value);
1772        }
1773        Ok(names)
1774    }
1775
1776    fn parse_bracketed_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
1777        self.consume(TokenType::LBracket)?;
1778        let items = self.parse_extended_identifier_list()?;
1779        self.consume(TokenType::RBracket)?;
1780        Ok(items)
1781    }
1782
1783    fn parse_extended_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1784        let mut items = Vec::new();
1785        items.push(self.consume_any_ident_or_kw()?.value);
1786        while self.check(TokenType::Comma) {
1787            self.advance();
1788            items.push(self.consume_any_ident_or_kw()?.value);
1789        }
1790        Ok(items)
1791    }
1792
1793    fn parse_dotted_identifier(&mut self) -> Result<String, ParseError> {
1794        let mut parts = vec![self.consume_any_ident_or_kw()?.value];
1795        while self.check(TokenType::Dot) {
1796            self.advance();
1797            parts.push(self.consume_any_ident_or_kw()?.value);
1798        }
1799        Ok(parts.join("."))
1800    }
1801
1802    fn parse_expression_string(&mut self) -> Result<String, ParseError> {
1803        if self.check(TokenType::LBracket) {
1804            let items = self.parse_bracketed_dot_identifiers()?;
1805            return Ok(format!("[{}]", items.join(", ")));
1806        }
1807        self.parse_dotted_identifier()
1808    }
1809
1810    fn parse_bracketed_dot_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
1811        self.consume(TokenType::LBracket)?;
1812        let mut items = vec![self.parse_dotted_identifier()?];
1813        while self.check(TokenType::Comma) {
1814            self.advance();
1815            items.push(self.parse_dotted_identifier()?);
1816        }
1817        self.consume(TokenType::RBracket)?;
1818        Ok(items)
1819    }
1820
1821    fn parse_argument_list(&mut self) -> Result<Vec<String>, ParseError> {
1822        let mut args = Vec::new();
1823        while !self.check(TokenType::RParen) {
1824            let tok = self.current().clone();
1825            match tok.ttype {
1826                TokenType::StringLit | TokenType::Integer | TokenType::Float => {
1827                    self.advance();
1828                    args.push(tok.value);
1829                }
1830                TokenType::Identifier => {
1831                    self.advance();
1832                    let mut val = tok.value;
1833                    if self.check(TokenType::Dot) {
1834                        self.advance();
1835                        val.push('.');
1836                        val.push_str(&self.consume_any_ident_or_kw()?.value);
1837                    }
1838                    args.push(val);
1839                }
1840                _ => {
1841                    self.advance();
1842                    let key = tok.value;
1843                    if self.check(TokenType::Colon) {
1844                        self.advance();
1845                        let v = self.advance().value.clone();
1846                        args.push(format!("{key}:{v}"));
1847                    } else {
1848                        args.push(key);
1849                    }
1850                }
1851            }
1852            if self.check(TokenType::Comma) {
1853                self.advance();
1854            }
1855        }
1856        Ok(args)
1857    }
1858
1859    /// Skip a single value or balanced bracketed/braced block (unknown field).
1860    fn skip_value(&mut self) {
1861        match self.current().ttype {
1862            TokenType::LBracket => {
1863                self.advance();
1864                let mut depth = 1u32;
1865                while depth > 0 && !self.check(TokenType::Eof) {
1866                    if self.check(TokenType::LBracket) {
1867                        depth += 1;
1868                    } else if self.check(TokenType::RBracket) {
1869                        depth -= 1;
1870                    }
1871                    self.advance();
1872                }
1873            }
1874            TokenType::LBrace => {
1875                self.advance();
1876                let mut depth = 1u32;
1877                while depth > 0 && !self.check(TokenType::Eof) {
1878                    if self.check(TokenType::LBrace) {
1879                        depth += 1;
1880                    } else if self.check(TokenType::RBrace) {
1881                        depth -= 1;
1882                    }
1883                    self.advance();
1884                }
1885            }
1886            TokenType::Lt => {
1887                // effect row: <io, network, ...>
1888                self.advance();
1889                let mut depth = 1u32;
1890                while depth > 0 && !self.check(TokenType::Eof) {
1891                    if self.check(TokenType::Lt) {
1892                        depth += 1;
1893                    } else if self.check(TokenType::Gt) {
1894                        depth -= 1;
1895                    }
1896                    self.advance();
1897                }
1898            }
1899            _ => {
1900                self.advance();
1901                while self.check(TokenType::Dot) {
1902                    self.advance();
1903                    self.advance();
1904                }
1905            }
1906        }
1907    }
1908
1909    /// Skip a balanced `{ ... }` block including its braces.
1910    fn skip_braced_block(&mut self) -> Result<(), ParseError> {
1911        self.consume(TokenType::LBrace)?;
1912        let mut depth = 1u32;
1913        while depth > 0 {
1914            if self.check(TokenType::Eof) {
1915                let tok = self.current();
1916                return Err(ParseError {
1917                    message: "Unterminated block — expected '}'".to_string(),
1918                    line: tok.line,
1919                    column: tok.column,
1920                                    ..Default::default()
1921                });
1922            }
1923            if self.check(TokenType::LBrace) {
1924                depth += 1;
1925            } else if self.check(TokenType::RBrace) {
1926                depth -= 1;
1927            }
1928            self.advance();
1929        }
1930        Ok(())
1931    }
1932
1933    fn at_declaration_start(&self) -> bool {
1934        is_declaration_keyword(&self.current().ttype) || self.check(TokenType::Eof)
1935    }
1936
1937    // ── top-level dispatch ───────────────────────────────────────
1938
1939    fn parse_declaration(&mut self) -> Result<Declaration, ParseError> {
1940        let tok = self.current().clone();
1941
1942        match tok.ttype {
1943            TokenType::Import => self.parse_import().map(Declaration::Import),
1944            TokenType::Persona => self.parse_persona().map(Declaration::Persona),
1945            TokenType::Context => self.parse_context().map(Declaration::Context),
1946            TokenType::Anchor => self.parse_anchor().map(Declaration::Anchor),
1947            TokenType::Memory => self.parse_memory().map(Declaration::Memory),
1948            TokenType::Tool => self.parse_tool().map(Declaration::Tool),
1949            TokenType::Type => self.parse_type_def().map(Declaration::Type),
1950            TokenType::Flow => self.parse_flow().map(Declaration::Flow),
1951            TokenType::Intent => self.parse_intent().map(Declaration::Intent),
1952            TokenType::Run => self.parse_run().map(Declaration::Run),
1953            TokenType::Let => self.parse_let().map(Declaration::Let),
1954            TokenType::Know | TokenType::Believe | TokenType::Speculate | TokenType::Doubt => {
1955                self.parse_epistemic_block().map(Declaration::Epistemic)
1956            }
1957            TokenType::Lambda => self.parse_lambda_data().map(Declaration::LambdaData),
1958
1959            // ── Tier 2 declarations (full AST) ──────────────────
1960            TokenType::Agent => self.parse_agent().map(Declaration::Agent),
1961            TokenType::Shield => self.parse_shield().map(Declaration::Shield),
1962            TokenType::Pix => self.parse_pix().map(Declaration::Pix),
1963            TokenType::Psyche => self.parse_psyche().map(Declaration::Psyche),
1964            TokenType::Corpus => self.parse_corpus().map(Declaration::Corpus),
1965            TokenType::Dataspace => self.parse_dataspace().map(Declaration::Dataspace),
1966            TokenType::Ots => self.parse_ots().map(Declaration::Ots),
1967            TokenType::Mandate => self.parse_mandate().map(Declaration::Mandate),
1968            TokenType::Compute => self.parse_compute().map(Declaration::Compute),
1969            TokenType::Daemon => self.parse_daemon().map(Declaration::Daemon),
1970            TokenType::Extension => self.parse_extension().map(Declaration::Extension),
1971            TokenType::AxonStore => self.parse_axonstore().map(Declaration::AxonStore),
1972            TokenType::AxonEndpoint => self.parse_axonendpoint().map(Declaration::AxonEndpoint),
1973
1974            // ── §λ-L-E Fase 1 — I/O cognitivo ───────────────────
1975            TokenType::Resource => self.parse_resource().map(Declaration::Resource),
1976            TokenType::Fabric => self.parse_fabric().map(Declaration::Fabric),
1977            TokenType::Manifest => self.parse_manifest().map(Declaration::Manifest),
1978            TokenType::Observe => self.parse_observe().map(Declaration::Observe),
1979
1980            // ── §λ-L-E Fase 3 — Control cognitivo ───────────────
1981            TokenType::Reconcile => self.parse_reconcile().map(Declaration::Reconcile),
1982            TokenType::Lease => self.parse_lease().map(Declaration::Lease),
1983            TokenType::Ensemble => self.parse_ensemble().map(Declaration::Ensemble),
1984
1985            // ── §λ-L-E Fase 4 — Topology + π-calculus sessions ─
1986            TokenType::Session => self.parse_session_definition().map(Declaration::Session),
1987            TokenType::Topology => self.parse_topology().map(Declaration::Topology),
1988
1989            // ── §Fase 41.b — typed WebSocket transport ─────────
1990            TokenType::Socket => self.parse_socket().map(Declaration::Socket),
1991
1992            // ── §λ-L-E Fase 5 — Cognitive immune system ─────────
1993            TokenType::Immune => self.parse_immune().map(Declaration::Immune),
1994            TokenType::Reflex => self.parse_reflex().map(Declaration::Reflex),
1995            TokenType::Heal => self.parse_heal().map(Declaration::Heal),
1996
1997            // ── §λ-L-E Fase 9 — UI cognitiva ────────────────────
1998            TokenType::Component => self.parse_component().map(Declaration::Component),
1999            TokenType::View => self.parse_view().map(Declaration::View),
2000
2001            // ── §λ-L-E Fase 13 — Mobile typed channels ──────────
2002            TokenType::Channel => self.parse_channel().map(Declaration::Channel),
2003
2004            // ── Tier 3+ structural fallback ─────────────────────
2005            // Store operations: keyword target { ... } or keyword target ...
2006            TokenType::Ingest
2007            | TokenType::Persist
2008            | TokenType::Retrieve
2009            | TokenType::Mutate
2010            | TokenType::Purge
2011            | TokenType::Transact => self.parse_generic_declaration(),
2012
2013            // MCP declaration
2014            TokenType::Mcp => self.parse_generic_declaration(),
2015
2016            _ => {
2017                // §Fase 28.e — append "Did you mean X?" hint when the
2018                // unknown token looks like a typo'd top-level keyword
2019                // (Levenshtein ≤ 2). D3, D11 ratified 2026-05-10.
2020                let hint = crate::smart_suggest::suggest_for(
2021                    &tok.value,
2022                    crate::smart_suggest::TOP_LEVEL_KEYWORD_NAMES,
2023                );
2024                let base = format!(
2025                    "Unexpected token at top level: '{}' — expected declaration \
2026                     (persona, context, anchor, flow, run, ...)",
2027                    tok.value
2028                );
2029                let message = if hint.is_empty() {
2030                    base
2031                } else {
2032                    format!("{base}. {hint}")
2033                };
2034                Err(ParseError {
2035                    message,
2036                    line: tok.line,
2037                    column: tok.column,
2038                    ..Default::default()
2039                })
2040            }
2041        }
2042    }
2043
2044    // ── IMPORT ───────────────────────────────────────────────────
2045
2046    fn parse_import(&mut self) -> Result<ImportNode, ParseError> {
2047        let tok = self.consume(TokenType::Import)?;
2048        let loc = self.loc_of(&tok);
2049
2050        let mut path_parts = Vec::new();
2051
2052        // Optional @ scope
2053        if self.check(TokenType::At) {
2054            self.advance();
2055            let first = self.consume(TokenType::Identifier)?;
2056            path_parts.push(format!("@{}", first.value));
2057        } else {
2058            let first = self.consume(TokenType::Identifier)?;
2059            path_parts.push(first.value);
2060        }
2061
2062        while self.check(TokenType::Dot) {
2063            self.advance();
2064            if self.check(TokenType::LBrace) {
2065                break;
2066            }
2067            let part = self.consume(TokenType::Identifier)?;
2068            path_parts.push(part.value);
2069        }
2070
2071        let mut names = Vec::new();
2072        if self.check(TokenType::LBrace) {
2073            self.advance();
2074            names = self.parse_identifier_list()?;
2075            self.consume(TokenType::RBrace)?;
2076        }
2077
2078        // Skip optional APX policy (with apx { ... })
2079        if self.current().value == "with" {
2080            self.advance();
2081            self.advance(); // consume "apx"
2082            if self.check(TokenType::LBrace) {
2083                self.skip_braced_block()?;
2084            }
2085        }
2086
2087        Ok(ImportNode {
2088            module_path: path_parts,
2089            names,
2090            loc,
2091            leading_trivia: Vec::new(),
2092            trailing_trivia: Vec::new(),
2093        })
2094    }
2095
2096    // ── PERSONA ──────────────────────────────────────────────────
2097
2098    fn parse_persona(&mut self) -> Result<PersonaDefinition, ParseError> {
2099        let tok = self.consume(TokenType::Persona)?;
2100        let loc = self.loc_of(&tok);
2101        let name = self.consume(TokenType::Identifier)?.value;
2102        self.consume(TokenType::LBrace)?;
2103
2104        let mut node = PersonaDefinition {
2105            name,
2106            domain: Vec::new(),
2107            tone: String::new(),
2108            confidence_threshold: None,
2109            cite_sources: None,
2110            refuse_if: Vec::new(),
2111            language: String::new(),
2112            description: String::new(),
2113            loc,
2114            leading_trivia: Vec::new(),
2115            trailing_trivia: Vec::new(),
2116        };
2117
2118        while !self.check(TokenType::RBrace) {
2119            let field_name = self.current().value.clone();
2120            self.advance();
2121            self.consume(TokenType::Colon)?;
2122
2123            match field_name.as_str() {
2124                "domain" => node.domain = self.parse_string_list()?,
2125                "tone" => node.tone = self.consume_any_ident_or_kw()?.value,
2126                "confidence_threshold" => node.confidence_threshold = Some(self.consume_number()?),
2127                "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2128                "refuse_if" => node.refuse_if = self.parse_bracketed_identifiers()?,
2129                "language" => node.language = self.consume(TokenType::StringLit)?.value,
2130                "description" => node.description = self.consume(TokenType::StringLit)?.value,
2131                _ => self.skip_value(),
2132            }
2133        }
2134        self.consume(TokenType::RBrace)?;
2135        Ok(node)
2136    }
2137
2138    // ── CONTEXT ──────────────────────────────────────────────────
2139
2140    fn parse_context(&mut self) -> Result<ContextDefinition, ParseError> {
2141        let tok = self.consume(TokenType::Context)?;
2142        let loc = self.loc_of(&tok);
2143        let name = self.consume(TokenType::Identifier)?.value;
2144        self.consume(TokenType::LBrace)?;
2145
2146        let mut node = ContextDefinition {
2147            name,
2148            memory_scope: String::new(),
2149            language: String::new(),
2150            depth: String::new(),
2151            max_tokens: None,
2152            temperature: None,
2153            cite_sources: None,
2154            loc,
2155            leading_trivia: Vec::new(),
2156            trailing_trivia: Vec::new(),
2157        };
2158
2159        while !self.check(TokenType::RBrace) {
2160            let field_name = self.current().value.clone();
2161            self.advance();
2162            self.consume(TokenType::Colon)?;
2163
2164            match field_name.as_str() {
2165                "memory" => node.memory_scope = self.consume_any_ident_or_kw()?.value,
2166                "language" => node.language = self.consume(TokenType::StringLit)?.value,
2167                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
2168                "max_tokens" => {
2169                    node.max_tokens = Some(
2170                        self.consume(TokenType::Integer)?
2171                            .value
2172                            .parse::<i64>()
2173                            .unwrap_or(0),
2174                    )
2175                }
2176                "temperature" => node.temperature = Some(self.consume_number()?),
2177                "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2178                _ => self.skip_value(),
2179            }
2180        }
2181        self.consume(TokenType::RBrace)?;
2182        Ok(node)
2183    }
2184
2185    // ── ANCHOR ───────────────────────────────────────────────────
2186
2187    fn parse_anchor(&mut self) -> Result<AnchorConstraint, ParseError> {
2188        let tok = self.consume(TokenType::Anchor)?;
2189        let loc = self.loc_of(&tok);
2190        let name = self.consume(TokenType::Identifier)?.value;
2191        self.consume(TokenType::LBrace)?;
2192
2193        let mut node = AnchorConstraint {
2194            name,
2195            require: String::new(),
2196            reject: Vec::new(),
2197            enforce: String::new(),
2198            description: String::new(),
2199            confidence_floor: None,
2200            unknown_response: String::new(),
2201            on_violation: String::new(),
2202            on_violation_target: String::new(),
2203            loc,
2204            leading_trivia: Vec::new(),
2205            trailing_trivia: Vec::new(),
2206        };
2207
2208        while !self.check(TokenType::RBrace) {
2209            let field_name = self.current().value.clone();
2210            self.advance();
2211            self.consume(TokenType::Colon)?;
2212
2213            match field_name.as_str() {
2214                "require" => node.require = self.consume_any_ident_or_kw()?.value,
2215                "description" => node.description = self.consume(TokenType::StringLit)?.value,
2216                "reject" => node.reject = self.parse_bracketed_identifiers()?,
2217                "enforce" => node.enforce = self.consume_any_ident_or_kw()?.value,
2218                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
2219                "unknown_response" => {
2220                    node.unknown_response = self.consume(TokenType::StringLit)?.value
2221                }
2222                "on_violation" => {
2223                    // Parse: raise ErrorName | fallback(...) | identifier
2224                    let action = self.consume_any_ident_or_kw()?.value;
2225                    node.on_violation = action.clone();
2226                    if action == "raise" || action == "fallback" {
2227                        node.on_violation_target = self.consume_any_ident_or_kw()?.value;
2228                    }
2229                }
2230                _ => self.skip_value(),
2231            }
2232        }
2233        self.consume(TokenType::RBrace)?;
2234        Ok(node)
2235    }
2236
2237    // ── MEMORY ───────────────────────────────────────────────────
2238
2239    fn parse_memory(&mut self) -> Result<MemoryDefinition, ParseError> {
2240        let tok = self.consume(TokenType::Memory)?;
2241        let loc = self.loc_of(&tok);
2242        let name = self.consume(TokenType::Identifier)?.value;
2243        self.consume(TokenType::LBrace)?;
2244
2245        let mut node = MemoryDefinition {
2246            name,
2247            store: String::new(),
2248            backend: String::new(),
2249            retrieval: String::new(),
2250            decay: String::new(),
2251            loc,
2252            leading_trivia: Vec::new(),
2253            trailing_trivia: Vec::new(),
2254        };
2255
2256        while !self.check(TokenType::RBrace) {
2257            let field_name = self.current().value.clone();
2258            self.advance();
2259            self.consume(TokenType::Colon)?;
2260
2261            match field_name.as_str() {
2262                "store" => node.store = self.consume_any_ident_or_kw()?.value,
2263                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
2264                "retrieval" => node.retrieval = self.consume_any_ident_or_kw()?.value,
2265                "decay" => {
2266                    if self.check(TokenType::Duration) {
2267                        node.decay = self.advance().value.clone();
2268                    } else {
2269                        node.decay = self.consume_any_ident_or_kw()?.value;
2270                    }
2271                }
2272                _ => self.skip_value(),
2273            }
2274        }
2275        self.consume(TokenType::RBrace)?;
2276        Ok(node)
2277    }
2278
2279    // ── TOOL ─────────────────────────────────────────────────────
2280
2281    fn parse_tool(&mut self) -> Result<ToolDefinition, ParseError> {
2282        let tok = self.consume(TokenType::Tool)?;
2283        let loc = self.loc_of(&tok);
2284        let name = self.consume(TokenType::Identifier)?.value;
2285        self.consume(TokenType::LBrace)?;
2286
2287        let mut node = ToolDefinition {
2288            name,
2289            provider: String::new(),
2290            max_results: None,
2291            filter_expr: String::new(),
2292            timeout: String::new(),
2293            runtime: String::new(),
2294            sandbox: None,
2295            effects: None,
2296            parameters: Vec::new(),
2297            output_type: None,
2298            loc,
2299            leading_trivia: Vec::new(),
2300            trailing_trivia: Vec::new(),
2301        };
2302
2303        while !self.check(TokenType::RBrace) {
2304            let field_name = self.current().value.clone();
2305            self.advance();
2306            self.consume(TokenType::Colon)?;
2307
2308            match field_name.as_str() {
2309                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
2310                "max_results" => {
2311                    node.max_results = Some(
2312                        self.consume(TokenType::Integer)?
2313                            .value
2314                            .parse::<i64>()
2315                            .unwrap_or(0),
2316                    )
2317                }
2318                "filter" => node.filter_expr = self.parse_filter_expression()?,
2319                "timeout" => node.timeout = self.consume(TokenType::Duration)?.value,
2320                "runtime" => node.runtime = self.consume_any_ident_or_kw()?.value,
2321                "sandbox" => node.sandbox = Some(self.parse_bool()?),
2322                "effects" => node.effects = Some(self.parse_effect_row()?),
2323                // §Fase 58.a — the tool's typed input schema + output type.
2324                "parameters" => node.parameters = self.parse_tool_param_schema()?,
2325                "output_type" => node.output_type = Some(self.parse_output_type_string()?),
2326                _ => self.skip_value(),
2327            }
2328        }
2329        self.consume(TokenType::RBrace)?;
2330        Ok(node)
2331    }
2332
2333    /// §Fase 58.a — parse a tool's INPUT SCHEMA: a brace-delimited list of
2334    /// `name: Type` parameters (`parameters: { query: String, max_results: Int }`).
2335    /// Reuses the flow-parameter shape (`Parameter`), so the same `TypeExpr`
2336    /// grammar — generics like `List<T>`, `?`-optionals — applies. A trailing
2337    /// comma is tolerated; an empty `{}` yields no parameters.
2338    fn parse_tool_param_schema(&mut self) -> Result<Vec<Parameter>, ParseError> {
2339        self.consume(TokenType::LBrace)?;
2340        let mut params = Vec::new();
2341        while !self.check(TokenType::RBrace) {
2342            // Accept a keyword-as-name (`filter`, `type`, `domain`, …) — real
2343            // adopter tool schemas use such parameter names; the `:` after it
2344            // disambiguates.
2345            let name = self.consume_any_ident_or_kw()?;
2346            let ploc = self.loc_of(&name);
2347            self.consume(TokenType::Colon)?;
2348            let type_expr = self.parse_type_expr()?;
2349            params.push(Parameter {
2350                name: name.value,
2351                type_expr,
2352                loc: ploc,
2353            });
2354            if self.check(TokenType::Comma) {
2355                self.advance();
2356            } else {
2357                break;
2358            }
2359        }
2360        self.consume(TokenType::RBrace)?;
2361        Ok(params)
2362    }
2363
2364    fn parse_filter_expression(&mut self) -> Result<String, ParseError> {
2365        let name = self.consume_any_ident_or_kw()?.value;
2366        if self.check(TokenType::LParen) {
2367            self.advance();
2368            let mut parts = vec![name, "(".to_string()];
2369            while !self.check(TokenType::RParen) {
2370                parts.push(self.advance().value.clone());
2371            }
2372            self.consume(TokenType::RParen)?;
2373            parts.push(")".to_string());
2374            Ok(parts.join(""))
2375        } else {
2376            Ok(name)
2377        }
2378    }
2379
2380    fn parse_effect_row(&mut self) -> Result<EffectRow, ParseError> {
2381        let tok = self.consume(TokenType::Lt)?;
2382        let loc = self.loc_of(&tok);
2383        let mut effects = Vec::new();
2384        let mut epistemic_level = String::new();
2385
2386        while !self.check(TokenType::Gt) {
2387            let name = self.consume_any_ident_or_kw()?.value;
2388            if self.check(TokenType::Colon) {
2389                self.advance();
2390                // Fase 11.c / 11.e — qualifiers can be compound slugs
2391                // from a closed catalogue:
2392                //
2393                //   * dot-separated  — `legal:HIPAA.164_502`,
2394                //                       `legal:GDPR.Art6.Consent`,
2395                //                       `legal:PCI_DSS.v4_Req3`
2396                //   * colon-separated — `ots:transform:mulaw8:pcm16`,
2397                //                       `ots:backend:native`
2398                //   * mixed           — supported by the same loop.
2399                //
2400                // The lexer fragments dotted slugs across IDENT /
2401                // INTEGER tokens (e.g., `164_502` lexes as INTEGER
2402                // `164` + IDENT `_502` because `_` starts a fresh
2403                // identifier); we recombine here using source-column
2404                // adjacency so the type checker sees the catalog
2405                // string verbatim.
2406                let level = self.parse_qualifier_value()?;
2407                if name == "epistemic" {
2408                    epistemic_level = level;
2409                } else {
2410                    effects.push(format!("{name}:{level}"));
2411                }
2412            } else {
2413                effects.push(name);
2414            }
2415            if self.check(TokenType::Comma) {
2416                self.advance();
2417            }
2418        }
2419        self.consume(TokenType::Gt)?;
2420
2421        Ok(EffectRow {
2422            effects,
2423            epistemic_level,
2424            loc,
2425        })
2426    }
2427
2428    /// Parse a compound qualifier value following an effect's first
2429    /// colon — supports both dot-separated (`HIPAA.164_502`) and
2430    /// colon-separated (`transform:mulaw8:pcm16`) catalogue slugs, as
2431    /// well as mixed forms.
2432    ///
2433    /// The grammar is: `segment ((`.` | `:`) segment)*` where a
2434    /// segment is a contiguous run of IDENT / INTEGER tokens (see
2435    /// [`Self::consume_dotted_slug_segment`]).
2436    fn parse_qualifier_value(&mut self) -> Result<String, ParseError> {
2437        let mut buf = self.consume_dotted_slug_segment()?;
2438        loop {
2439            let sep = if self.check(TokenType::Dot) {
2440                '.'
2441            } else if self.check(TokenType::Colon) {
2442                ':'
2443            } else {
2444                break;
2445            };
2446            self.advance();
2447            let part = self.consume_dotted_slug_segment()?;
2448            buf.push(sep);
2449            buf.push_str(&part);
2450        }
2451        Ok(buf)
2452    }
2453
2454    /// Consume a contiguous run of IDENT / INTEGER / keyword-ident
2455    /// tokens whose source positions are adjacent (no whitespace
2456    /// between them), concatenating their text into a single segment.
2457    ///
2458    /// Needed for closed-catalogue qualifier slugs whose segment
2459    /// mixes digits and identifier characters — e.g. `HIPAA.164_502`
2460    /// lexes as INTEGER `164` + IDENT `_502` because `_` starts a
2461    /// fresh identifier; the catalog value is the concatenation
2462    /// `164_502`. Adjacency is determined by matching
2463    /// `(line, column + len)` of the previous token against the next
2464    /// token's start position.
2465    fn consume_dotted_slug_segment(&mut self) -> Result<String, ParseError> {
2466        let first = self.consume_any_ident_or_kw()?;
2467        let mut buf = first.value.clone();
2468        let mut next_line = first.line;
2469        let mut next_col = first.column + first.value.chars().count() as u32;
2470        loop {
2471            let cur = self.current();
2472            let is_segment_token = matches!(cur.ttype, TokenType::Identifier | TokenType::Integer,);
2473            if !is_segment_token {
2474                break;
2475            }
2476            if cur.line != next_line || cur.column != next_col {
2477                break;
2478            }
2479            buf.push_str(&cur.value);
2480            next_col = cur.column + cur.value.chars().count() as u32;
2481            next_line = cur.line;
2482            self.pos += 1;
2483        }
2484        Ok(buf)
2485    }
2486
2487    // ── TYPE ─────────────────────────────────────────────────────
2488
2489    fn parse_type_def(&mut self) -> Result<TypeDefinition, ParseError> {
2490        let tok = self.consume(TokenType::Type)?;
2491        let loc = self.loc_of(&tok);
2492        let name = self.consume(TokenType::Identifier)?.value;
2493
2494        let mut node = TypeDefinition {
2495            name,
2496            fields: Vec::new(),
2497            range_constraint: None,
2498            where_clause: None,
2499            compliance: Vec::new(),
2500            loc: loc.clone(),
2501            leading_trivia: Vec::new(),
2502            trailing_trivia: Vec::new(),
2503        };
2504
2505        // Optional range: (0.0..1.0)
2506        if self.check(TokenType::LParen) {
2507            self.advance();
2508            let min_val = self.consume_number()?;
2509            self.consume(TokenType::DotDot)?;
2510            let max_val = self.consume_number()?;
2511            self.consume(TokenType::RParen)?;
2512            node.range_constraint = Some(RangeConstraint {
2513                min_value: min_val,
2514                max_value: max_val,
2515                loc: loc.clone(),
2516            });
2517        }
2518
2519        // Optional where clause
2520        if self.check(TokenType::Where) {
2521            self.advance();
2522            let mut expr_parts = Vec::new();
2523            while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
2524                if self.check(TokenType::Eof) {
2525                    break;
2526                }
2527                expr_parts.push(self.advance().value.clone());
2528            }
2529            node.where_clause = Some(WhereClause {
2530                expression: expr_parts.join(" "),
2531                loc: loc.clone(),
2532            });
2533        }
2534
2535        // Optional ESK Fase 6.1 — `compliance [HIPAA, ...]` prefix modifier
2536        // between `type Name` / `range` / `where` and the body `{`.
2537        if self.check(TokenType::Identifier) && self.current().value == "compliance" {
2538            self.advance();
2539            node.compliance = self.parse_bracketed_identifiers()?;
2540        }
2541
2542        // Optional body: { field: Type, ... }
2543        if self.check(TokenType::LBrace) {
2544            self.advance();
2545            while !self.check(TokenType::RBrace) {
2546                let field_name = self.consume(TokenType::Identifier)?;
2547                let field_loc = self.loc_of(&field_name);
2548                self.consume(TokenType::Colon)?;
2549                let type_expr = self.parse_type_expr()?;
2550                node.fields.push(TypeField {
2551                    name: field_name.value,
2552                    type_expr,
2553                    loc: field_loc,
2554                });
2555                if self.check(TokenType::Comma) {
2556                    self.advance();
2557                }
2558            }
2559            self.consume(TokenType::RBrace)?;
2560        }
2561
2562        Ok(node)
2563    }
2564
2565    fn parse_type_expr(&mut self) -> Result<TypeExpr, ParseError> {
2566        let name_tok = self.consume(TokenType::Identifier)?;
2567        let loc = self.loc_of(&name_tok);
2568        let mut generic_param = String::new();
2569        let mut optional = false;
2570
2571        if self.check(TokenType::Lt) {
2572            self.advance();
2573            // §Fase 39.a — recursive: the generic param can itself be a
2574            // nested type expression. `FlowEnvelope<List<TenantRecord>>`
2575            // parses as outer=FlowEnvelope, inner=List<TenantRecord>.
2576            // Pre-39.a the inner had to be a single Identifier; nested
2577            // generics like the canonical FlowEnvelope<T> wrapper
2578            // required this lift. Backwards-compat preserved for
2579            // single-level generics like `Stream<Token>` and
2580            // `List<T>` — the recursion lands once and returns the
2581            // same flat string the v1.x parser produced.
2582            let inner = self.parse_type_expr()?;
2583            generic_param = if inner.generic_param.is_empty() {
2584                inner.name
2585            } else {
2586                format!("{}<{}>", inner.name, inner.generic_param)
2587            };
2588            self.consume(TokenType::Gt)?;
2589        }
2590        if self.check(TokenType::Question) {
2591            self.advance();
2592            optional = true;
2593        }
2594
2595        Ok(TypeExpr {
2596            name: name_tok.value,
2597            generic_param,
2598            optional,
2599            loc,
2600        })
2601    }
2602
2603    /// Parse a type expression in a context where the AST stores the
2604    /// shape as a flat string (step / reason / forge / ots-apply
2605    /// productions). Mirrors Python `_parse_output_type_string`.
2606    ///
2607    /// Accepts:
2608    /// - `Identifier`        → `"Identifier"`
2609    /// - `Stream<String>`    → `"Stream<String>"`
2610    /// - `Optional?`         → `"Optional?"`
2611    /// - `Stream<String>?`   → `"Stream<String>?"`
2612    ///
2613    /// **Why this exists** — pre-fix, the step parser called
2614    /// `consume(TokenType::Identifier)?.value` which captured only
2615    /// the head identifier and left `< … >` unconsumed. For
2616    /// `output: Stream<Token>`, this produced `output_type =
2617    /// "Stream"`, and downstream `flow_has_stream_output`'s
2618    /// `starts_with("Stream<") && ends_with('>')` predicate then
2619    /// returned false → `implicit_transport == "json"` → the
2620    /// dynamic-route fallback in `axon-rs` served JSON instead of
2621    /// SSE even when the adopter's source canonically declared the
2622    /// algebraic stream effect. Surfaced 2026-05-12 by adopter
2623    /// `docs/MIGRATION_TO_AXON.md` audit after the v1.23.0 wire-
2624    /// layer didn't honor the declarative effect. Python parser was
2625    /// fixed for the same gap 2026-05-09; this is the Rust cross-
2626    /// stack catch-up.
2627    fn parse_output_type_string(&mut self) -> Result<String, ParseError> {
2628        let expr = self.parse_type_expr()?;
2629        let mut s = expr.name;
2630        if !expr.generic_param.is_empty() {
2631            s.push('<');
2632            s.push_str(&expr.generic_param);
2633            s.push('>');
2634        }
2635        if expr.optional {
2636            s.push('?');
2637        }
2638        Ok(s)
2639    }
2640
2641    // ── FLOW ─────────────────────────────────────────────────────
2642
2643    fn parse_flow(&mut self) -> Result<FlowDefinition, ParseError> {
2644        let tok = self.consume(TokenType::Flow)?;
2645        let loc = self.loc_of(&tok);
2646        let name = self.consume(TokenType::Identifier)?.value;
2647
2648        self.consume(TokenType::LParen)?;
2649        let mut parameters = Vec::new();
2650        if !self.check(TokenType::RParen) {
2651            parameters = self.parse_param_list()?;
2652        }
2653        self.consume(TokenType::RParen)?;
2654
2655        let mut return_type = None;
2656        if self.check(TokenType::Arrow) {
2657            self.advance();
2658            return_type = Some(self.parse_type_expr()?);
2659        }
2660
2661        self.consume(TokenType::LBrace)?;
2662        let mut body = Vec::new();
2663        while !self.check(TokenType::RBrace) {
2664            body.push(self.parse_flow_step()?);
2665        }
2666        self.consume(TokenType::RBrace)?;
2667
2668        Ok(FlowDefinition {
2669            name,
2670            parameters,
2671            return_type,
2672            body,
2673            loc,
2674            leading_trivia: Vec::new(),
2675            trailing_trivia: Vec::new(),
2676        })
2677    }
2678
2679    fn parse_param_list(&mut self) -> Result<Vec<Parameter>, ParseError> {
2680        let mut params = Vec::new();
2681
2682        let name = self.consume(TokenType::Identifier)?;
2683        let ploc = self.loc_of(&name);
2684        self.consume(TokenType::Colon)?;
2685        let type_expr = self.parse_type_expr()?;
2686        params.push(Parameter {
2687            name: name.value,
2688            type_expr,
2689            loc: ploc,
2690        });
2691
2692        while self.check(TokenType::Comma) {
2693            self.advance();
2694            let name = self.consume(TokenType::Identifier)?;
2695            let ploc = self.loc_of(&name);
2696            self.consume(TokenType::Colon)?;
2697            let type_expr = self.parse_type_expr()?;
2698            params.push(Parameter {
2699                name: name.value,
2700                type_expr,
2701                loc: ploc,
2702            });
2703        }
2704        Ok(params)
2705    }
2706
2707    // ── FLOW STEP dispatch ───────────────────────────────────────
2708
2709    fn parse_flow_step(&mut self) -> Result<FlowStep, ParseError> {
2710        let tok = self.current().clone();
2711
2712        match tok.ttype {
2713            TokenType::Step => self.parse_step().map(FlowStep::Step),
2714            TokenType::If => self.parse_if().map(FlowStep::If),
2715            TokenType::For => self.parse_for_in().map(FlowStep::ForIn),
2716            TokenType::Let => self.parse_let().map(FlowStep::Let),
2717            TokenType::Return => self.parse_return().map(FlowStep::Return),
2718            TokenType::Break => self.parse_break().map(FlowStep::Break),
2719            TokenType::Continue => self.parse_continue().map(FlowStep::Continue),
2720            TokenType::Lambda => self.parse_lambda_data_apply().map(FlowStep::LambdaDataApply),
2721
2722            // ── Tier 2 flow steps (typed AST) ─────────────────────
2723            TokenType::Probe => self.parse_flow_step_simple("probe").map(|l| FlowStep::Probe(ProbeStep { target: l.1, loc: l.0 })),
2724            TokenType::Reason => self.parse_flow_step_simple("reason").map(|l| FlowStep::Reason(ReasonStep { strategy: String::new(), target: l.1, loc: l.0 })),
2725            TokenType::Validate => self.parse_flow_step_simple("validate").map(|l| FlowStep::Validate(ValidateStep { target: l.1, rule: String::new(), loc: l.0 })),
2726            TokenType::Refine => self.parse_flow_step_simple("refine").map(|l| FlowStep::Refine(RefineStep { target: l.1, strategy: String::new(), loc: l.0 })),
2727            TokenType::Weave => self.parse_weave_step(),
2728            TokenType::Use => self.parse_use_step(),
2729            TokenType::Remember => self.parse_remember_step(),
2730            TokenType::Recall => self.parse_recall_step(),
2731            TokenType::Par => self.parse_block_step("par").map(|l| FlowStep::Par(ParBlock { loc: l })),
2732            TokenType::Hibernate => self.parse_hibernate_step(),
2733            TokenType::Deliberate => self.parse_block_step("deliberate").map(|l| FlowStep::Deliberate(DeliberateBlock { loc: l })),
2734            TokenType::Consensus => self.parse_block_step("consensus").map(|l| FlowStep::Consensus(ConsensusBlock { loc: l })),
2735            TokenType::Forge => self.parse_block_step("forge").map(|l| FlowStep::Forge(ForgeBlock { loc: l })),
2736            TokenType::Focus => self.parse_flow_step_simple("focus").map(|l| FlowStep::Focus(FocusStep { expression: l.1, loc: l.0 })),
2737            TokenType::Associate => self.parse_associate_step(),
2738            TokenType::Aggregate => self.parse_aggregate_step(),
2739            TokenType::Explore => self.parse_explore_step(),
2740            TokenType::Ingest => self.parse_ingest_step(),
2741            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 })),
2742            TokenType::Stream => self.parse_block_step("stream").map(|l| FlowStep::Stream(StreamBlock { loc: l })),
2743            TokenType::Navigate => self.parse_navigate_step(),
2744            TokenType::Drill => self.parse_drill_step(),
2745            TokenType::Trail => self.parse_flow_step_simple("trail").map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 })),
2746            TokenType::Corroborate => self.parse_corroborate_step(),
2747            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 })),
2748            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 })),
2749            TokenType::Compute => self.parse_apply_step("compute").map(|l| FlowStep::ComputeApply(ComputeApplyStep { compute_name: l.1, arguments: Vec::new(), output_name: l.3, loc: l.0 })),
2750            TokenType::Listen => self.parse_listen_step(),
2751            TokenType::Daemon => self.parse_flow_step_simple("daemon").map(|l| FlowStep::DaemonStep(DaemonStepNode { daemon_ref: l.1, loc: l.0 })),
2752            // §λ-L-E Fase 13 — Mobile typed channels (paper §3.1, §3.2, §4.3)
2753            TokenType::Emit => self.parse_emit_step(),
2754            TokenType::Publish => self.parse_publish_step(),
2755            TokenType::Discover => self.parse_discover_step(),
2756            TokenType::Persist => self.parse_persist_step(),
2757            TokenType::Retrieve => self.parse_retrieve_step(),
2758            TokenType::Mutate => self.parse_mutate_step(),
2759            TokenType::Purge => self.parse_store_where_step().map(|(loc, store_name, where_expr)| FlowStep::Purge(PurgeStep { store_name, where_expr, loc })),
2760            TokenType::Transact => self.parse_block_step("transact").map(|l| FlowStep::Transact(TransactBlock { loc: l })),
2761
2762            _ => {
2763                // §Fase 28.e — append "Did you mean X?" hint when the
2764                // unknown token looks like a typo'd flow-body keyword
2765                // (e.g. `stepp` / `reasn` / `validte`). D3, D11.
2766                let hint = crate::smart_suggest::suggest_for(
2767                    &tok.value,
2768                    crate::smart_suggest::FLOW_BODY_KEYWORD_NAMES,
2769                );
2770                let base = format!(
2771                    "Unexpected token in flow body: '{}' — expected step, if, for, let, return, ...",
2772                    tok.value
2773                );
2774                let message = if hint.is_empty() {
2775                    base
2776                } else {
2777                    format!("{base}. {hint}")
2778                };
2779                Err(ParseError {
2780                    message,
2781                    line: tok.line,
2782                    column: tok.column,
2783                    ..Default::default()
2784                })
2785            }
2786        }
2787    }
2788
2789    // ── STEP ─────────────────────────────────────────────────────
2790
2791    fn parse_step(&mut self) -> Result<StepNode, ParseError> {
2792        let tok = self.consume(TokenType::Step)?;
2793        let loc = self.loc_of(&tok);
2794        let name = self.consume(TokenType::Identifier)?.value;
2795
2796        let mut persona_ref = String::new();
2797        if self.check(TokenType::Use) {
2798            self.advance();
2799            persona_ref = self.consume_any_ident_or_kw()?.value;
2800        }
2801
2802        self.consume(TokenType::LBrace)?;
2803
2804        let mut node = StepNode {
2805            name,
2806            persona_ref,
2807            given: String::new(),
2808            ask: String::new(),
2809            output_type: String::new(),
2810            confidence_floor: None,
2811            navigate_ref: String::new(),
2812            apply_ref: String::new(),
2813            loc,
2814        };
2815
2816        while !self.check(TokenType::RBrace) {
2817            let inner = self.current().clone();
2818
2819            match inner.ttype {
2820                TokenType::Given => {
2821                    self.advance();
2822                    self.consume(TokenType::Colon)?;
2823                    node.given = self.parse_expression_string()?;
2824                }
2825                TokenType::Ask => {
2826                    self.advance();
2827                    self.consume(TokenType::Colon)?;
2828                    node.ask = self.consume(TokenType::StringLit)?.value;
2829                }
2830                TokenType::Output => {
2831                    // Mirror of Python `_parse_step` `case "output":`
2832                    // which uses `_parse_output_type_string` — accepts
2833                    // the FULL generic-aware shape `Stream<T>`,
2834                    // `Stream<T>?`, `Identifier?`, NOT just the bare
2835                    // head identifier. Pre-fix the step parser dropped
2836                    // `<T>` and downstream `flow_has_stream_output`'s
2837                    // `starts_with("Stream<") && ends_with('>')` then
2838                    // returned false → `implicit_transport == "json"`
2839                    // → dynamic routes served JSON instead of SSE.
2840                    self.advance();
2841                    self.consume(TokenType::Colon)?;
2842                    node.output_type = self.parse_output_type_string()?;
2843                }
2844                TokenType::Navigate => {
2845                    self.advance();
2846                    self.consume(TokenType::Colon)?;
2847                    node.navigate_ref = self.parse_dotted_identifier()?;
2848                }
2849                TokenType::Identifier if inner.value == "confidence_floor" => {
2850                    self.advance();
2851                    self.consume(TokenType::Colon)?;
2852                    node.confidence_floor = Some(self.consume_number()?);
2853                }
2854                TokenType::Identifier if inner.value == "apply" => {
2855                    self.advance();
2856                    self.consume(TokenType::Colon)?;
2857                    node.apply_ref = self.consume_any_ident_or_kw()?.value;
2858                }
2859                // §Fase 54.a — a `use` nested inside a `step { }` body used
2860                // to be skipped structurally (grouped with the sub-constructs
2861                // below), silently degrading the tool dispatch to an
2862                // unconstrained LLM step with NO diagnostic. That fallthrough
2863                // drops the AST node before the type-checker can see it, so the
2864                // resource the tool would provision is never linearly accounted
2865                // for (use_tool soundness). Reject it here, at the parser —
2866                // the only place that still sees the token — and redirect to
2867                // the canonical forms.
2868                TokenType::Use => {
2869                    let tool = self
2870                        .tokens
2871                        .get(self.pos + 1)
2872                        .map(|t| t.value.as_str())
2873                        .filter(|v| !v.is_empty())
2874                        .unwrap_or("<Tool>");
2875                    return Err(ParseError {
2876                        message: format!(
2877                            "`use` is not valid inside a `step {{ }}` body — the tool dispatch \
2878                             would be silently dropped. To invoke a tool, either write the \
2879                             flow-level step `use {tool} on <arg>` (outside this block), or bind \
2880                             it inside this step with `apply: {tool}`. To attach a persona, put \
2881                             it in the step header: `step <name> use <Persona> {{ … }}`."
2882                        ),
2883                        line: inner.line,
2884                        column: inner.column,
2885                        ..Default::default()
2886                    });
2887                }
2888                // Sub-constructs (probe, reason, weave, stream) → skip structurally
2889                TokenType::Probe
2890                | TokenType::Reason
2891                | TokenType::Weave
2892                | TokenType::Stream => {
2893                    self.skip_flow_step_structural()?;
2894                }
2895                _ => {
2896                    return Err(ParseError {
2897                        message: format!(
2898                            "Unexpected token in step body: '{}' — expected given, ask, \
2899                             probe, reason, weave, stream, output, confidence_floor, navigate, apply",
2900                            inner.value
2901                        ),
2902                        line: inner.line,
2903                        column: inner.column,
2904                                            ..Default::default()
2905                    });
2906                }
2907            }
2908        }
2909        self.consume(TokenType::RBrace)?;
2910        Ok(node)
2911    }
2912
2913    /// Skip a flow-level sub-construct structurally (consume keyword + args + optional block).
2914    fn skip_flow_step_structural(&mut self) -> Result<(), ParseError> {
2915        // Consume the keyword
2916        self.advance();
2917        // Consume tokens until we hit a { or a closing }, or a known flow step keyword
2918        while !self.check(TokenType::LBrace)
2919            && !self.check(TokenType::RBrace)
2920            && !self.check(TokenType::Eof)
2921        {
2922            // Check if we hit a new step-level keyword (means this was a one-liner)
2923            let tt = &self.current().ttype;
2924            if matches!(
2925                tt,
2926                TokenType::Step
2927                    | TokenType::Given
2928                    | TokenType::Ask
2929                    | TokenType::Output
2930                    | TokenType::Navigate
2931                    | TokenType::Use
2932                    | TokenType::Probe
2933                    | TokenType::Reason
2934                    | TokenType::Weave
2935                    | TokenType::Stream
2936                    | TokenType::If
2937                    | TokenType::For
2938                    | TokenType::Let
2939                    | TokenType::Return
2940            ) {
2941                return Ok(());
2942            }
2943            self.advance();
2944        }
2945        // If block, skip it
2946        if self.check(TokenType::LBrace) {
2947            self.skip_braced_block()?;
2948        }
2949        Ok(())
2950    }
2951
2952    // ── INTENT ───────────────────────────────────────────────────
2953
2954    fn parse_intent(&mut self) -> Result<IntentNode, ParseError> {
2955        let tok = self.consume(TokenType::Intent)?;
2956        let loc = self.loc_of(&tok);
2957        let name = self.consume(TokenType::Identifier)?.value;
2958        self.consume(TokenType::LBrace)?;
2959
2960        let mut node = IntentNode {
2961            name,
2962            given: String::new(),
2963            ask: String::new(),
2964            output_type: None,
2965            confidence_floor: None,
2966            loc,
2967            leading_trivia: Vec::new(),
2968            trailing_trivia: Vec::new(),
2969        };
2970
2971        while !self.check(TokenType::RBrace) {
2972            let field_name = self.current().value.clone();
2973            self.advance();
2974            self.consume(TokenType::Colon)?;
2975
2976            match field_name.as_str() {
2977                "given" => node.given = self.consume(TokenType::Identifier)?.value,
2978                "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
2979                "output" => node.output_type = Some(self.parse_type_expr()?),
2980                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
2981                _ => self.skip_value(),
2982            }
2983        }
2984        self.consume(TokenType::RBrace)?;
2985        Ok(node)
2986    }
2987
2988    // ── RUN ──────────────────────────────────────────────────────
2989
2990    fn parse_run(&mut self) -> Result<RunStatement, ParseError> {
2991        let tok = self.consume(TokenType::Run)?;
2992        let loc = self.loc_of(&tok);
2993        let flow_name = self.consume(TokenType::Identifier)?.value;
2994
2995        self.consume(TokenType::LParen)?;
2996        let mut arguments = Vec::new();
2997        if !self.check(TokenType::RParen) {
2998            arguments = self.parse_argument_list()?;
2999        }
3000        self.consume(TokenType::RParen)?;
3001
3002        let mut node = RunStatement {
3003            flow_name,
3004            arguments,
3005            persona: String::new(),
3006            context: String::new(),
3007            anchors: Vec::new(),
3008            on_failure: String::new(),
3009            on_failure_params: Vec::new(),
3010            output_to: String::new(),
3011            effort: String::new(),
3012            loc,
3013            leading_trivia: Vec::new(),
3014            trailing_trivia: Vec::new(),
3015        };
3016
3017        while self.check_run_modifier() {
3018            let mod_tok = self.current().clone();
3019            match mod_tok.ttype {
3020                TokenType::As => {
3021                    self.advance();
3022                    node.persona = self.consume(TokenType::Identifier)?.value;
3023                }
3024                TokenType::Within => {
3025                    self.advance();
3026                    node.context = self.consume(TokenType::Identifier)?.value;
3027                }
3028                TokenType::ConstrainedBy => {
3029                    self.advance();
3030                    node.anchors = self.parse_bracketed_identifiers()?;
3031                }
3032                TokenType::OnFailure => {
3033                    self.advance();
3034                    self.consume(TokenType::Colon)?;
3035                    node.on_failure = self.consume_any_ident_or_kw()?.value;
3036                    // Parse optional params: (key: val, ...)
3037                    if self.check(TokenType::LParen) {
3038                        self.advance();
3039                        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3040                            let key = self.consume_any_ident_or_kw()?.value;
3041                            self.consume(TokenType::Colon)?;
3042                            let val = self.consume_any_ident_or_kw()?.value;
3043                            node.on_failure_params.push((key, val));
3044                            if self.check(TokenType::Comma) {
3045                                self.advance();
3046                            }
3047                        }
3048                        if self.check(TokenType::RParen) {
3049                            self.advance();
3050                        }
3051                    }
3052                }
3053                TokenType::OutputTo => {
3054                    self.advance();
3055                    self.consume(TokenType::Colon)?;
3056                    node.output_to = self.consume(TokenType::StringLit)?.value;
3057                }
3058                TokenType::Effort => {
3059                    self.advance();
3060                    self.consume(TokenType::Colon)?;
3061                    node.effort = self.consume_any_ident_or_kw()?.value;
3062                }
3063                _ => break,
3064            }
3065        }
3066
3067        Ok(node)
3068    }
3069
3070    // ── EPISTEMIC BLOCK ──────────────────────────────────────────
3071
3072    fn parse_epistemic_block(&mut self) -> Result<EpistemicBlock, ParseError> {
3073        let tok = self.current().clone();
3074        let mode = match tok.ttype {
3075            TokenType::Know => "know",
3076            TokenType::Believe => "believe",
3077            TokenType::Speculate => "speculate",
3078            TokenType::Doubt => "doubt",
3079            _ => unreachable!(),
3080        };
3081        self.advance();
3082        let loc = self.loc_of(&tok);
3083
3084        self.consume(TokenType::LBrace)?;
3085        let mut body = Vec::new();
3086        while !self.check(TokenType::RBrace) {
3087            body.push(self.parse_declaration()?);
3088        }
3089        self.consume(TokenType::RBrace)?;
3090
3091        Ok(EpistemicBlock {
3092            mode: mode.to_string(),
3093            body,
3094            loc,
3095            leading_trivia: Vec::new(),
3096            trailing_trivia: Vec::new(),
3097        })
3098    }
3099
3100    // ── IF ────────────────────────────────────────────────────────
3101
3102    fn parse_if(&mut self) -> Result<ConditionalNode, ParseError> {
3103        let tok = self.consume(TokenType::If)?;
3104        let loc = self.loc_of(&tok);
3105
3106        // Parse condition
3107        let mut parts = vec![self.consume_any_ident_or_kw()?.value];
3108        while self.check(TokenType::Dot) {
3109            self.advance();
3110            parts.push(self.consume_any_ident_or_kw()?.value);
3111        }
3112        let condition = parts.join(".");
3113
3114        let mut comparison_op = String::new();
3115        let mut comparison_value = String::new();
3116        if self.check_comparison() {
3117            comparison_op = self.advance().value.clone();
3118            let val_tok = self.current().clone();
3119            if val_tok.ttype == TokenType::StringLit {
3120                comparison_value = val_tok.value;
3121                self.advance();
3122            } else {
3123                comparison_value = self.advance().value.clone();
3124            }
3125        }
3126
3127        // Compound conditions (or)
3128        let mut conditions = Vec::new();
3129        let mut conjunctor = String::new();
3130        while self.check(TokenType::Or) {
3131            conjunctor = "or".to_string();
3132            self.advance();
3133            let mut cond_parts = vec![self.consume_any_ident_or_kw()?.value];
3134            while self.check(TokenType::Dot) {
3135                self.advance();
3136                cond_parts.push(self.consume_any_ident_or_kw()?.value);
3137            }
3138            let cond_str = cond_parts.join(".");
3139            let mut cond_op = String::new();
3140            let mut cond_val = String::new();
3141            if self.check_comparison() {
3142                cond_op = self.advance().value.clone();
3143                let val_tok = self.current().clone();
3144                if val_tok.ttype == TokenType::StringLit {
3145                    cond_val = val_tok.value;
3146                    self.advance();
3147                } else {
3148                    cond_val = self.advance().value.clone();
3149                }
3150            }
3151            conditions.push((cond_str, cond_op, cond_val));
3152        }
3153
3154        let mut then_body = Vec::new();
3155        let mut else_body = Vec::new();
3156
3157        // Arrow form or block form
3158        if self.check(TokenType::Arrow) {
3159            self.advance();
3160            then_body.push(self.parse_flow_step()?);
3161        } else if self.check(TokenType::LBrace) {
3162            self.advance();
3163            while !self.check(TokenType::RBrace) {
3164                then_body.push(self.parse_flow_step()?);
3165            }
3166            self.consume(TokenType::RBrace)?;
3167        }
3168
3169        // Else branch
3170        if self.check(TokenType::Else) {
3171            self.advance();
3172            if self.check(TokenType::Arrow) {
3173                self.advance();
3174                else_body.push(self.parse_flow_step()?);
3175            } else if self.check(TokenType::LBrace) {
3176                self.advance();
3177                while !self.check(TokenType::RBrace) {
3178                    else_body.push(self.parse_flow_step()?);
3179                }
3180                self.consume(TokenType::RBrace)?;
3181            }
3182        }
3183
3184        Ok(ConditionalNode {
3185            condition,
3186            comparison_op,
3187            comparison_value,
3188            then_body,
3189            else_body,
3190            conditions,
3191            conjunctor,
3192            loc,
3193        })
3194    }
3195
3196    // ── FOR IN ───────────────────────────────────────────────────
3197
3198    fn parse_for_in(&mut self) -> Result<ForInStatement, ParseError> {
3199        let tok = self.consume(TokenType::For)?;
3200        let loc = self.loc_of(&tok);
3201        let variable = self.consume(TokenType::Identifier)?.value;
3202        self.consume(TokenType::In)?;
3203        let iterable = self.parse_dotted_identifier()?;
3204
3205        self.consume(TokenType::LBrace)?;
3206        // Fase 19.e — increment loop_depth so `parse_break` /
3207        // `parse_continue` inside the body pass the scope check.
3208        // Decrement on every exit path (Ok / Err) so a parse error
3209        // mid-body does not leave the depth permanently elevated
3210        // for later top-level parsing — `?` would skip the
3211        // decrement otherwise.
3212        self.loop_depth += 1;
3213        let body_result = (|| -> Result<Vec<FlowStep>, ParseError> {
3214            let mut body = Vec::new();
3215            while !self.check(TokenType::RBrace) {
3216                body.push(self.parse_flow_step()?);
3217            }
3218            Ok(body)
3219        })();
3220        self.loop_depth -= 1;
3221        let body = body_result?;
3222        self.consume(TokenType::RBrace)?;
3223
3224        Ok(ForInStatement {
3225            variable,
3226            iterable,
3227            body,
3228            loc,
3229        })
3230    }
3231
3232    /// Fase 19.e — `break` keyword. Compile-time scope check
3233    /// (`loop_depth == 0`) rejects break outside a for-in body.
3234    fn parse_break(&mut self) -> Result<BreakStatement, ParseError> {
3235        let tok = self.consume(TokenType::Break)?;
3236        let loc = self.loc_of(&tok);
3237        if self.loop_depth == 0 {
3238            return Err(ParseError {
3239                message: "'break' outside of a for-in loop body".to_string(),
3240                line: tok.line,
3241                column: tok.column,
3242                            ..Default::default()
3243            });
3244        }
3245        Ok(BreakStatement { loc })
3246    }
3247
3248    /// Fase 19.e — `continue` keyword. Same scope check as
3249    /// `parse_break`.
3250    fn parse_continue(&mut self) -> Result<ContinueStatement, ParseError> {
3251        let tok = self.consume(TokenType::Continue)?;
3252        let loc = self.loc_of(&tok);
3253        if self.loop_depth == 0 {
3254            return Err(ParseError {
3255                message: "'continue' outside of a for-in loop body".to_string(),
3256                line: tok.line,
3257                column: tok.column,
3258                            ..Default::default()
3259            });
3260        }
3261        Ok(ContinueStatement { loc })
3262    }
3263
3264    // ── LET ──────────────────────────────────────────────────────
3265
3266    fn parse_let(&mut self) -> Result<LetStatement, ParseError> {
3267        let tok = self.consume(TokenType::Let)?;
3268        let loc = self.loc_of(&tok);
3269
3270        // Name can be an identifier or a keyword used as binding name
3271        let name = self.consume_any_ident_or_kw()?.value;
3272        self.consume(TokenType::Assign)?;
3273        // Fase 17.a — reset side-channel before parsing value; the
3274        // atom / expr helpers tag the kind as they descend.
3275        self.last_let_value_kind = "literal".to_string();
3276        let value = self.parse_let_value_expr()?;
3277
3278        Ok(LetStatement {
3279            identifier: name,
3280            value_expr: value,
3281            value_kind: self.last_let_value_kind.clone(),
3282            loc,
3283            leading_trivia: Vec::new(),
3284            trailing_trivia: Vec::new(),
3285        })
3286    }
3287
3288    fn parse_let_value_expr(&mut self) -> Result<String, ParseError> {
3289        let atom = self.parse_let_atom()?;
3290
3291        // Arithmetic expression: collect as string
3292        if matches!(
3293            self.current().ttype,
3294            TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
3295        ) {
3296            let mut parts = vec![atom];
3297            while matches!(
3298                self.current().ttype,
3299                TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
3300            ) {
3301                parts.push(self.advance().value.clone());
3302                parts.push(self.parse_let_atom()?);
3303            }
3304            self.last_let_value_kind = "expression".to_string();
3305            return Ok(parts.join(" "));
3306        }
3307        Ok(atom)
3308    }
3309
3310    fn parse_let_atom(&mut self) -> Result<String, ParseError> {
3311        let tok = self.current().clone();
3312
3313        match tok.ttype {
3314            TokenType::StringLit => {
3315                self.last_let_value_kind = "literal".to_string();
3316                self.advance();
3317                Ok(tok.value)
3318            }
3319            TokenType::Integer | TokenType::Float => {
3320                self.last_let_value_kind = "literal".to_string();
3321                self.advance();
3322                Ok(tok.value)
3323            }
3324            TokenType::Bool => {
3325                self.last_let_value_kind = "literal".to_string();
3326                self.advance();
3327                Ok(tok.value)
3328            }
3329            TokenType::Identifier => {
3330                self.last_let_value_kind = "reference".to_string();
3331                self.parse_dotted_identifier()
3332            }
3333            TokenType::LBracket => {
3334                self.last_let_value_kind = "literal".to_string();
3335                self.parse_let_list_literal()
3336            }
3337            _ => {
3338                // Keywords starting a dotted path (pix.document_tree)
3339                if self.pos + 1 < self.tokens.len()
3340                    && self.tokens[self.pos + 1].ttype == TokenType::Dot
3341                {
3342                    self.last_let_value_kind = "reference".to_string();
3343                    return self.parse_dotted_identifier();
3344                }
3345                Err(ParseError {
3346                    message: format!(
3347                        "Expected value expression, found {:?}('{}')",
3348                        tok.ttype, tok.value
3349                    ),
3350                    line: tok.line,
3351                    column: tok.column,
3352                                    ..Default::default()
3353                })
3354            }
3355        }
3356    }
3357
3358    fn parse_let_list_literal(&mut self) -> Result<String, ParseError> {
3359        self.consume(TokenType::LBracket)?;
3360        let mut items = Vec::new();
3361        if !self.check(TokenType::RBracket) {
3362            items.push(self.parse_let_value_expr()?);
3363            while self.check(TokenType::Comma) {
3364                self.advance();
3365                if self.check(TokenType::RBracket) {
3366                    break; // trailing comma
3367                }
3368                items.push(self.parse_let_value_expr()?);
3369            }
3370        }
3371        self.consume(TokenType::RBracket)?;
3372        Ok(format!("[{}]", items.join(", ")))
3373    }
3374
3375    // ── RETURN ───────────────────────────────────────────────────
3376
3377    fn parse_return(&mut self) -> Result<ReturnStatement, ParseError> {
3378        let tok = self.consume(TokenType::Return)?;
3379        let loc = self.loc_of(&tok);
3380        let value = self.parse_let_value_expr()?;
3381        Ok(ReturnStatement {
3382            value_expr: value,
3383            loc,
3384        })
3385    }
3386
3387    // ── TIER 2 FLOW STEP HELPERS ────────────────────────────────────
3388
3389    /// Parse: keyword target (consumes keyword + one identifier/keyword-as-value).
3390    fn parse_flow_step_simple(&mut self, _kw: &str) -> Result<(Loc, String), ParseError> {
3391        let tok = self.current().clone();
3392        self.advance(); // consume keyword
3393        let target = if self.at_declaration_start()
3394            || self.check(TokenType::RBrace)
3395            || self.check(TokenType::Eof)
3396        {
3397            String::new()
3398        } else {
3399            self.consume_any_ident_or_kw()?.value.clone()
3400        };
3401        // Skip optional braced block
3402        if self.check(TokenType::LBrace) {
3403            self.skip_braced_block()?;
3404        }
3405        Ok((
3406            Loc {
3407                line: tok.line,
3408                column: tok.column,
3409            },
3410            target,
3411        ))
3412    }
3413
3414    /// Parse: keyword { ... } — block-level step, skip body structurally.
3415    fn parse_block_step(&mut self, _kw: &str) -> Result<Loc, ParseError> {
3416        let tok = self.current().clone();
3417        self.advance();
3418        // Skip optional arguments before brace
3419        while !self.check(TokenType::LBrace)
3420            && !self.check(TokenType::RBrace)
3421            && !self.check(TokenType::Eof)
3422            && !self.at_declaration_start()
3423        {
3424            self.advance();
3425        }
3426        if self.check(TokenType::LBrace) {
3427            self.skip_braced_block()?;
3428        }
3429        Ok(Loc {
3430            line: tok.line,
3431            column: tok.column,
3432        })
3433    }
3434
3435    /// Parse: keyword Name on target -> output_type (apply pattern).
3436    fn parse_apply_step(&mut self, _kw: &str) -> Result<(Loc, String, String, String), ParseError> {
3437        let tok = self.current().clone();
3438        self.advance(); // consume keyword
3439        let name = self.consume_any_ident_or_kw()?.value.clone();
3440        let mut target = String::new();
3441        let mut output_type = String::new();
3442        // "on" target
3443        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
3444            let next = self.current().clone();
3445            if next.value == "on" {
3446                self.advance();
3447                target = self.consume_any_ident_or_kw()?.value.clone();
3448            }
3449        }
3450        // -> output_type
3451        if self.check(TokenType::Arrow) {
3452            self.advance();
3453            output_type = self.consume_any_ident_or_kw()?.value.clone();
3454        }
3455        // Skip optional braced block
3456        if self.check(TokenType::LBrace) {
3457            self.skip_braced_block()?;
3458        }
3459        Ok((
3460            Loc {
3461                line: tok.line,
3462                column: tok.column,
3463            },
3464            name,
3465            target,
3466            output_type,
3467        ))
3468    }
3469
3470    fn parse_weave_step(&mut self) -> Result<FlowStep, ParseError> {
3471        let tok = self.current().clone();
3472        self.advance();
3473        let mut node = WeaveStep {
3474            sources: Vec::new(),
3475            target: String::new(),
3476            format_type: String::new(),
3477            priority: Vec::new(),
3478            style: String::new(),
3479            loc: Loc {
3480                line: tok.line,
3481                column: tok.column,
3482            },
3483        };
3484        if self.check(TokenType::LBrace) {
3485            self.advance();
3486            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3487                let f = self.current().value.clone();
3488                self.advance();
3489                if self.check(TokenType::Colon) {
3490                    self.advance();
3491                    match f.as_str() {
3492                        "sources" => node.sources = self.parse_bracketed_identifiers()?,
3493                        "target" => node.target = self.consume_any_ident_or_kw()?.value.clone(),
3494                        "format" => {
3495                            node.format_type = self.consume_any_ident_or_kw()?.value.clone()
3496                        }
3497                        "priority" => node.priority = self.parse_bracketed_identifiers()?,
3498                        "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
3499                        _ => self.skip_value(),
3500                    }
3501                }
3502            }
3503            if self.check(TokenType::RBrace) {
3504                self.advance();
3505            }
3506        }
3507        Ok(FlowStep::Weave(node))
3508    }
3509
3510    fn parse_use_step(&mut self) -> Result<FlowStep, ParseError> {
3511        let tok = self.current().clone();
3512        self.advance();
3513        let tool_name = self.consume_any_ident_or_kw()?.value.clone();
3514        // §Fase 58.b — two mutually-exclusive `use` argument surfaces:
3515        //   * `use Tool(query = "${q}", max_results = 5)` — D2 canonical
3516        //     multi-field keyword args (§58.b `UseArgs::Named`).
3517        //   * `use Tool on "${arg}"` / `on query` — the §54.b single positional
3518        //     argument (D5 back-compat, `UseArgs::LegacyPositional`):
3519        //       - a STRING LITERAL carrying interpolation (`on "${query}"`)
3520        //         resolved at dispatch against request-bound flow params;
3521        //       - a BARE identifier / literal (`on query` / `on 42`) verbatim.
3522        //     (Unquoted `${query}` is intentionally NOT a form — interpolation
3523        //     lives inside string literals everywhere in Axon.)
3524        let args = if self.check(TokenType::LParen) {
3525            UseArgs::Named(self.parse_named_arg_list()?)
3526        } else {
3527            let mut argument = String::new();
3528            if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
3529                let next = self.current().clone();
3530                if next.value == "on" {
3531                    self.advance();
3532                    argument = self.consume_any_ident_or_kw()?.value.clone();
3533                }
3534            }
3535            UseArgs::LegacyPositional(argument)
3536        };
3537        if self.check(TokenType::LBrace) {
3538            self.skip_braced_block()?;
3539        }
3540        Ok(FlowStep::UseTool(UseToolStep {
3541            tool_name,
3542            args,
3543            loc: Loc {
3544                line: tok.line,
3545                column: tok.column,
3546            },
3547        }))
3548    }
3549
3550    /// §Fase 58.b — parse `(name = value, …)` keyword args for the canonical
3551    /// `use Tool(...)` multi-field dispatch. Values are captured as expression
3552    /// strings (StringLit / Integer / Float / Bool / dotted identifier / list)
3553    /// via the shared `parse_let_atom`, since the frontend has no structured
3554    /// `Expr`. A trailing comma is tolerated; `()` yields no args.
3555    fn parse_named_arg_list(&mut self) -> Result<Vec<(String, String, String)>, ParseError> {
3556        self.consume(TokenType::LParen)?;
3557        let mut args = Vec::new();
3558        while !self.check(TokenType::RParen) {
3559            // Accept a keyword-as-name (`filter`, `type`, `from`, …) — real
3560            // adopter schemas use such names; the following `=` disambiguates.
3561            let name = self.consume_any_ident_or_kw()?.value;
3562            self.consume(TokenType::Assign)?;
3563            let value = self.parse_let_atom()?;
3564            // §Fase 60 — `parse_let_atom` classified the value (`"literal"` vs
3565            // `"reference"`); carry it so the runtime resolves a bare
3566            // identifier / `Step.output` as a binding lookup, not a literal.
3567            let value_kind = self.last_let_value_kind.clone();
3568            args.push((name, value, value_kind));
3569            if self.check(TokenType::Comma) {
3570                self.advance();
3571            } else {
3572                break;
3573            }
3574        }
3575        self.consume(TokenType::RParen)?;
3576        Ok(args)
3577    }
3578
3579    fn parse_remember_step(&mut self) -> Result<FlowStep, ParseError> {
3580        let tok = self.current().clone();
3581        self.advance();
3582        let expr = self.consume_any_ident_or_kw()?.value.clone();
3583        let mut mem = String::new();
3584        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
3585            let next = self.current().clone();
3586            if next.value == "in" || next.ttype == TokenType::In {
3587                self.advance();
3588                mem = self.consume_any_ident_or_kw()?.value.clone();
3589            }
3590        }
3591        Ok(FlowStep::Remember(RememberStep {
3592            expression: expr,
3593            memory_target: mem,
3594            loc: Loc {
3595                line: tok.line,
3596                column: tok.column,
3597            },
3598        }))
3599    }
3600
3601    fn parse_recall_step(&mut self) -> Result<FlowStep, ParseError> {
3602        let tok = self.current().clone();
3603        self.advance();
3604        let query = if self.check(TokenType::StringLit) {
3605            self.consume(TokenType::StringLit)?.value.clone()
3606        } else {
3607            self.consume_any_ident_or_kw()?.value.clone()
3608        };
3609        let mut mem = String::new();
3610        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
3611            let next = self.current().clone();
3612            if next.value == "from" || next.ttype == TokenType::From {
3613                self.advance();
3614                mem = self.consume_any_ident_or_kw()?.value.clone();
3615            }
3616        }
3617        Ok(FlowStep::Recall(RecallStep {
3618            query,
3619            memory_source: mem,
3620            loc: Loc {
3621                line: tok.line,
3622                column: tok.column,
3623            },
3624        }))
3625    }
3626
3627    fn parse_hibernate_step(&mut self) -> Result<FlowStep, ParseError> {
3628        let tok = self.current().clone();
3629        self.advance();
3630        let mut event = String::new();
3631        let mut timeout = String::new();
3632        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
3633            event = self.consume_any_ident_or_kw()?.value.clone();
3634        }
3635        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
3636            let next = self.current().clone();
3637            if next.ttype == TokenType::Duration {
3638                self.advance();
3639                timeout = next.value.clone();
3640            }
3641        }
3642        Ok(FlowStep::Hibernate(HibernateStep {
3643            event_name: event,
3644            timeout,
3645            loc: Loc {
3646                line: tok.line,
3647                column: tok.column,
3648            },
3649        }))
3650    }
3651
3652    fn parse_associate_step(&mut self) -> Result<FlowStep, ParseError> {
3653        let tok = self.current().clone();
3654        self.advance();
3655        let left = self.consume_any_ident_or_kw()?.value.clone();
3656        let mut right = String::new();
3657        let mut using = String::new();
3658        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
3659            right = self.consume_any_ident_or_kw()?.value.clone();
3660        }
3661        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
3662            let next = self.current().clone();
3663            if next.value == "using" {
3664                self.advance();
3665                using = self.consume_any_ident_or_kw()?.value.clone();
3666            }
3667        }
3668        Ok(FlowStep::Associate(AssociateStep {
3669            left,
3670            right,
3671            using_field: using,
3672            loc: Loc {
3673                line: tok.line,
3674                column: tok.column,
3675            },
3676        }))
3677    }
3678
3679    fn parse_aggregate_step(&mut self) -> Result<FlowStep, ParseError> {
3680        let tok = self.current().clone();
3681        self.advance();
3682        let target = self.consume_any_ident_or_kw()?.value.clone();
3683        let mut group_by = Vec::new();
3684        let mut alias = String::new();
3685        if self.check(TokenType::LBrace) {
3686            self.advance();
3687            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3688                let f = self.current().value.clone();
3689                self.advance();
3690                if self.check(TokenType::Colon) {
3691                    self.advance();
3692                    match f.as_str() {
3693                        "group_by" => group_by = self.parse_bracketed_identifiers()?,
3694                        "alias" | "as" => alias = self.consume_any_ident_or_kw()?.value.clone(),
3695                        _ => self.skip_value(),
3696                    }
3697                }
3698            }
3699            if self.check(TokenType::RBrace) {
3700                self.advance();
3701            }
3702        }
3703        Ok(FlowStep::Aggregate(AggregateStep {
3704            target,
3705            group_by,
3706            alias,
3707            loc: Loc {
3708                line: tok.line,
3709                column: tok.column,
3710            },
3711        }))
3712    }
3713
3714    fn parse_explore_step(&mut self) -> Result<FlowStep, ParseError> {
3715        let tok = self.current().clone();
3716        self.advance();
3717        let target = self.consume_any_ident_or_kw()?.value.clone();
3718        let mut limit = None;
3719        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
3720            if self.current().ttype == TokenType::Integer {
3721                limit = self.current().value.parse::<i64>().ok();
3722                self.advance();
3723            }
3724        }
3725        Ok(FlowStep::ExploreStep(ExploreStepNode {
3726            target,
3727            limit,
3728            loc: Loc {
3729                line: tok.line,
3730                column: tok.column,
3731            },
3732        }))
3733    }
3734
3735    fn parse_ingest_step(&mut self) -> Result<FlowStep, ParseError> {
3736        let tok = self.current().clone();
3737        self.advance();
3738        let source = self.consume_any_ident_or_kw()?.value.clone();
3739        let mut target = String::new();
3740        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
3741            let next = self.current().clone();
3742            if next.value == "into" || next.ttype == TokenType::Into {
3743                self.advance();
3744                target = self.consume_any_ident_or_kw()?.value.clone();
3745            }
3746        }
3747        if self.check(TokenType::LBrace) {
3748            self.skip_braced_block()?;
3749        }
3750        Ok(FlowStep::Ingest(IngestStep {
3751            source,
3752            target,
3753            loc: Loc {
3754                line: tok.line,
3755                column: tok.column,
3756            },
3757        }))
3758    }
3759
3760    fn parse_navigate_step(&mut self) -> Result<FlowStep, ParseError> {
3761        let tok = self.current().clone();
3762        self.advance();
3763        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
3764        let mut node = NavigateStep {
3765            pix_name,
3766            corpus_name: String::new(),
3767            query_expr: String::new(),
3768            trail_enabled: false,
3769            output_name: String::new(),
3770            loc: Loc {
3771                line: tok.line,
3772                column: tok.column,
3773            },
3774        };
3775        if self.check(TokenType::LBrace) {
3776            self.advance();
3777            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3778                let f = self.current().value.clone();
3779                self.advance();
3780                if self.check(TokenType::Colon) {
3781                    self.advance();
3782                    match f.as_str() {
3783                        "corpus" => {
3784                            node.corpus_name = self.consume_any_ident_or_kw()?.value.clone()
3785                        }
3786                        "query" => {
3787                            node.query_expr = self.consume(TokenType::StringLit)?.value.clone()
3788                        }
3789                        "trail" => {
3790                            node.trail_enabled = self.consume_any_ident_or_kw()?.value == "true"
3791                        }
3792                        "output" | "as" => {
3793                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
3794                        }
3795                        _ => self.skip_value(),
3796                    }
3797                }
3798            }
3799            if self.check(TokenType::RBrace) {
3800                self.advance();
3801            }
3802        }
3803        Ok(FlowStep::Navigate(node))
3804    }
3805
3806    fn parse_drill_step(&mut self) -> Result<FlowStep, ParseError> {
3807        let tok = self.current().clone();
3808        self.advance();
3809        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
3810        let mut node = DrillStep {
3811            pix_name,
3812            subtree_path: String::new(),
3813            query_expr: String::new(),
3814            output_name: String::new(),
3815            loc: Loc {
3816                line: tok.line,
3817                column: tok.column,
3818            },
3819        };
3820        if self.check(TokenType::LBrace) {
3821            self.advance();
3822            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3823                let f = self.current().value.clone();
3824                self.advance();
3825                if self.check(TokenType::Colon) {
3826                    self.advance();
3827                    match f.as_str() {
3828                        "subtree" | "path" => {
3829                            node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
3830                        }
3831                        "query" => {
3832                            node.query_expr = self.consume(TokenType::StringLit)?.value.clone()
3833                        }
3834                        "output" | "as" => {
3835                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
3836                        }
3837                        _ => self.skip_value(),
3838                    }
3839                }
3840            }
3841            if self.check(TokenType::RBrace) {
3842                self.advance();
3843            }
3844        }
3845        Ok(FlowStep::Drill(node))
3846    }
3847
3848    fn parse_corroborate_step(&mut self) -> Result<FlowStep, ParseError> {
3849        let tok = self.current().clone();
3850        self.advance();
3851        let nav_ref = self.consume_any_ident_or_kw()?.value.clone();
3852        let mut output = String::new();
3853        if self.check(TokenType::Arrow) {
3854            self.advance();
3855            output = self.consume_any_ident_or_kw()?.value.clone();
3856        }
3857        Ok(FlowStep::Corroborate(CorroborateStep {
3858            navigate_ref: nav_ref,
3859            output_name: output,
3860            loc: Loc {
3861                line: tok.line,
3862                column: tok.column,
3863            },
3864        }))
3865    }
3866
3867    fn parse_listen_step(&mut self) -> Result<FlowStep, ParseError> {
3868        let tok = self.current().clone();
3869        self.advance();
3870        // §λ-L-E Fase 13 D4 — dual-mode listen:
3871        //   • String topic (legacy, deprecated since Fase 13)
3872        //   • Identifier (canonical: declared ChannelDefinition)
3873        let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
3874            (self.consume(TokenType::StringLit)?.value.clone(), false)
3875        } else {
3876            (self.consume_any_ident_or_kw()?.value.clone(), true)
3877        };
3878        let mut alias = String::new();
3879        if !self.at_declaration_start()
3880            && !self.check(TokenType::RBrace)
3881            && !self.check(TokenType::LBrace)
3882        {
3883            let next = self.current().clone();
3884            if next.value == "as" || next.ttype == TokenType::As {
3885                self.advance();
3886                alias = self.consume_any_ident_or_kw()?.value.clone();
3887            }
3888        }
3889        if self.check(TokenType::LBrace) {
3890            self.skip_braced_block()?;
3891        }
3892        Ok(FlowStep::Listen(ListenStep {
3893            channel,
3894            channel_is_ref,
3895            event_alias: alias,
3896            loc: Loc {
3897                line: tok.line,
3898                column: tok.column,
3899            },
3900        }))
3901    }
3902
3903    fn parse_retrieve_step(&mut self) -> Result<FlowStep, ParseError> {
3904        let tok = self.current().clone();
3905        self.advance();
3906        let store = self.consume_any_ident_or_kw()?.value.clone();
3907        let mut where_expr = String::new();
3908        let mut alias = String::new();
3909        if self.check(TokenType::LBrace) {
3910            self.advance();
3911            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3912                let f = self.current().value.clone();
3913                self.advance();
3914                if self.check(TokenType::Colon) {
3915                    self.advance();
3916                    match f.as_str() {
3917                        "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
3918                        "as" | "alias" => alias = self.consume_any_ident_or_kw()?.value.clone(),
3919                        _ => self.skip_value(),
3920                    }
3921                }
3922            }
3923            if self.check(TokenType::RBrace) {
3924                self.advance();
3925            }
3926        }
3927        Ok(FlowStep::Retrieve(RetrieveStep {
3928            store_name: store,
3929            where_expr,
3930            alias,
3931            loc: Loc {
3932                line: tok.line,
3933                column: tok.column,
3934            },
3935        }))
3936    }
3937
3938    /// §Fase 35.m — Parse a `purge` step, capturing the optional
3939    /// `{ where: "<expr>" }` filter. (Fase 35.p moved `mutate` to its
3940    /// own `parse_mutate_step`, which also captures SET columns; this
3941    /// helper now serves `purge` alone — a `DELETE` has no SET clause.)
3942    ///
3943    /// Before Fase 35.m these two steps parsed via `parse_flow_step_simple`,
3944    /// which *skipped* the braced block — so a written `where:` clause
3945    /// was silently dropped and every `mutate`/`purge` ran against the
3946    /// whole store, leaving the entire Fase 35.b/c parameterized-filter
3947    /// machinery unreachable for them. This mirror of `parse_retrieve_step`
3948    /// (minus the `as:` alias — a mutate/purge binds no result) closes
3949    /// that gap. Returns `(loc, store_name, where_expr)`.
3950    fn parse_store_where_step(
3951        &mut self,
3952    ) -> Result<(Loc, String, String), ParseError> {
3953        let tok = self.current().clone();
3954        self.advance(); // consume the keyword
3955        let store = if self.at_declaration_start()
3956            || self.check(TokenType::RBrace)
3957            || self.check(TokenType::Eof)
3958        {
3959            String::new()
3960        } else {
3961            self.consume_any_ident_or_kw()?.value.clone()
3962        };
3963        let mut where_expr = String::new();
3964        if self.check(TokenType::LBrace) {
3965            self.advance();
3966            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3967                let field = self.current().value.clone();
3968                self.advance();
3969                if self.check(TokenType::Colon) {
3970                    self.advance();
3971                    match field.as_str() {
3972                        "where" => {
3973                            where_expr =
3974                                self.consume(TokenType::StringLit)?.value.clone()
3975                        }
3976                        _ => self.skip_value(),
3977                    }
3978                }
3979            }
3980            if self.check(TokenType::RBrace) {
3981                self.advance();
3982            }
3983        }
3984        Ok((
3985            Loc {
3986                line: tok.line,
3987                column: tok.column,
3988            },
3989            store,
3990            where_expr,
3991        ))
3992    }
3993
3994    /// §Fase 35.o — Parse a `persist` step, capturing the optional
3995    /// `{ col: value }` field block.
3996    ///
3997    /// Before Fase 35.o `persist` parsed via `parse_flow_step_simple`,
3998    /// which *skipped* the braced block — so a written field block was
3999    /// silently dropped and the runtime fell back to writing every
4000    /// context binding as a row, which fails against any real table
4001    /// (flows always carry more bindings than a table has columns).
4002    /// This captures the declared columns into `PersistStep.fields`;
4003    /// the runtime writes exactly those (interpolated). A `persist`
4004    /// with no block keeps the v1.30.0 user-bindings fallback — fully
4005    /// backward-compatible. Mirror of `parse_retrieve_step`, but the
4006    /// keys are arbitrary column names rather than the fixed
4007    /// `where:` / `as:` filter keys.
4008    ///
4009    /// The optional `into` connector (`persist into <store>`) is
4010    /// accepted and skipped — before Fase 35.o `into` was captured as
4011    /// the store name.
4012    fn parse_persist_step(&mut self) -> Result<FlowStep, ParseError> {
4013        let tok = self.current().clone();
4014        self.advance(); // consume `persist`
4015        // Optional `into` connector — skip it so the store name that
4016        // follows is not mistaken for the target.
4017        if self.current().value == "into" && !self.check(TokenType::LBrace) {
4018            self.advance();
4019        }
4020        let store = if self.at_declaration_start()
4021            || self.check(TokenType::LBrace)
4022            || self.check(TokenType::RBrace)
4023            || self.check(TokenType::Eof)
4024        {
4025            String::new()
4026        } else {
4027            self.consume_any_ident_or_kw()?.value.clone()
4028        };
4029        let mut fields: Vec<(String, String)> = Vec::new();
4030        if self.check(TokenType::LBrace) {
4031            self.advance();
4032            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4033                let col = self.current().value.clone();
4034                self.advance();
4035                if self.check(TokenType::Colon) {
4036                    self.advance();
4037                    let value = if self.check(TokenType::StringLit) {
4038                        self.consume(TokenType::StringLit)?.value.clone()
4039                    } else if self.check(TokenType::RBrace)
4040                        || self.check(TokenType::Eof)
4041                        || self.check(TokenType::Colon)
4042                    {
4043                        String::new()
4044                    } else {
4045                        let v = self.current().clone();
4046                        self.advance();
4047                        v.value.clone()
4048                    };
4049                    fields.push((col, value));
4050                }
4051            }
4052            if self.check(TokenType::RBrace) {
4053                self.advance();
4054            }
4055        }
4056        Ok(FlowStep::Persist(PersistStep {
4057            store_name: store,
4058            fields,
4059            loc: Loc {
4060                line: tok.line,
4061                column: tok.column,
4062            },
4063        }))
4064    }
4065
4066    /// §Fase 35.p — Parse a `mutate` step, capturing both the
4067    /// `{ where: "<expr>" }` filter AND the `{ col: value }` SET
4068    /// assignments.
4069    ///
4070    /// Before Fase 35.p `mutate` parsed via `parse_store_where_step`,
4071    /// which captured only `where:` and *skipped* every other key — so
4072    /// the runtime built the `UPDATE … SET` clause from every flow
4073    /// binding (params + step results + `let`s), which fails against
4074    /// any real table (`column "X" does not exist`). This closes the
4075    /// gap symmetrically to 35.o's `persist` block: every key other
4076    /// than `where:` is a SET column; a `mutate` with no SET column
4077    /// keeps the v1.31.0 user-bindings fallback. `where:` keeps its
4078    /// string-literal grammar (as in `retrieve` / `purge`).
4079    fn parse_mutate_step(&mut self) -> Result<FlowStep, ParseError> {
4080        let tok = self.current().clone();
4081        self.advance(); // consume `mutate`
4082        let store = if self.at_declaration_start()
4083            || self.check(TokenType::LBrace)
4084            || self.check(TokenType::RBrace)
4085            || self.check(TokenType::Eof)
4086        {
4087            String::new()
4088        } else {
4089            self.consume_any_ident_or_kw()?.value.clone()
4090        };
4091        let mut where_expr = String::new();
4092        let mut fields: Vec<(String, String)> = Vec::new();
4093        if self.check(TokenType::LBrace) {
4094            self.advance();
4095            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4096                let key = self.current().value.clone();
4097                self.advance();
4098                if self.check(TokenType::Colon) {
4099                    self.advance();
4100                    if key == "where" {
4101                        where_expr =
4102                            self.consume(TokenType::StringLit)?.value.clone();
4103                    } else {
4104                        let value = if self.check(TokenType::StringLit) {
4105                            self.consume(TokenType::StringLit)?.value.clone()
4106                        } else if self.check(TokenType::RBrace)
4107                            || self.check(TokenType::Eof)
4108                            || self.check(TokenType::Colon)
4109                        {
4110                            String::new()
4111                        } else {
4112                            let v = self.current().clone();
4113                            self.advance();
4114                            v.value.clone()
4115                        };
4116                        fields.push((key, value));
4117                    }
4118                }
4119            }
4120            if self.check(TokenType::RBrace) {
4121                self.advance();
4122            }
4123        }
4124        Ok(FlowStep::Mutate(MutateStep {
4125            store_name: store,
4126            where_expr,
4127            fields,
4128            loc: Loc {
4129                line: tok.line,
4130                column: tok.column,
4131            },
4132        }))
4133    }
4134
4135    // ── TIER 2 DECLARATIONS ────────────────────────────────────────
4136
4137    fn parse_agent(&mut self) -> Result<AgentDefinition, ParseError> {
4138        let tok = self.consume(TokenType::Agent)?;
4139        let name = self.consume(TokenType::Identifier)?.value;
4140        let mut node = AgentDefinition {
4141            name,
4142            goal: String::new(),
4143            tools: Vec::new(),
4144            memory_ref: String::new(),
4145            strategy: String::new(),
4146            on_stuck: String::new(),
4147            shield_ref: String::new(),
4148            max_iterations: None,
4149            max_tokens: None,
4150            max_time: String::new(),
4151            max_cost: None,
4152            loc: Loc {
4153                line: tok.line,
4154                column: tok.column,
4155            },
4156            leading_trivia: Vec::new(),
4157            trailing_trivia: Vec::new(),
4158        };
4159        // Skip optional parameters/return type before brace
4160        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
4161            self.advance();
4162        }
4163        self.consume(TokenType::LBrace)?;
4164        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4165            let field = self.current().clone();
4166            let field_name = field.value.clone();
4167            self.advance();
4168            if self.check(TokenType::Colon) {
4169                self.advance();
4170                match field_name.as_str() {
4171                    "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
4172                    "tools" => node.tools = self.parse_bracketed_identifiers()?,
4173                    "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
4174                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
4175                    "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
4176                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
4177                    "max_iterations" => node.max_iterations = self.parse_optional_int(),
4178                    "max_tokens" => node.max_tokens = self.parse_optional_int(),
4179                    "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
4180                    "max_cost" => node.max_cost = self.parse_optional_float(),
4181                    _ => self.skip_value(),
4182                }
4183            } else if self.check(TokenType::LBrace) {
4184                self.skip_braced_block()?;
4185            }
4186        }
4187        self.consume(TokenType::RBrace)?;
4188        Ok(node)
4189    }
4190
4191    /// §Fase 53 — `extension Name { category: effects|scan, members: [ … ] }`.
4192    /// The parser is permissive on field/category VALUES (validated in
4193    /// §53.c by the type-checker — no-shadowing, category-membership);
4194    /// it only enforces the structural grammar here.
4195    fn parse_extension(&mut self) -> Result<ExtensionDefinition, ParseError> {
4196        let tok = self.consume(TokenType::Extension)?;
4197        let name = self.consume(TokenType::Identifier)?.value;
4198        let mut node = ExtensionDefinition {
4199            name,
4200            category: String::new(),
4201            members: Vec::new(),
4202            loc: Loc {
4203                line: tok.line,
4204                column: tok.column,
4205            },
4206            leading_trivia: Vec::new(),
4207            trailing_trivia: Vec::new(),
4208        };
4209        self.consume(TokenType::LBrace)?;
4210        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4211            let field_name = self.current().value.clone();
4212            self.advance();
4213            if self.check(TokenType::Colon) {
4214                self.advance();
4215                match field_name.as_str() {
4216                    "category" => {
4217                        node.category = self.consume_any_ident_or_kw()?.value.clone()
4218                    }
4219                    "members" => node.members = self.parse_extension_members()?,
4220                    _ => self.skip_value(),
4221                }
4222            } else if self.check(TokenType::LBrace) {
4223                self.skip_braced_block()?;
4224            }
4225        }
4226        self.consume(TokenType::RBrace)?;
4227        Ok(node)
4228    }
4229
4230    /// §Fase 53 — parse `[ "name" [ : { semantics: "…", default_confidence: 0.8 } ], … ]`.
4231    /// Each member is a string literal optionally followed by a metadata
4232    /// block. Trailing/interleaved commas are tolerated.
4233    fn parse_extension_members(&mut self) -> Result<Vec<ExtensionMember>, ParseError> {
4234        let mut members = Vec::new();
4235        self.consume(TokenType::LBracket)?;
4236        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
4237            let name_tok = self.consume(TokenType::StringLit)?;
4238            let mut member = ExtensionMember {
4239                name: name_tok.value.clone(),
4240                semantics: None,
4241                default_confidence: None,
4242                loc: Loc {
4243                    line: name_tok.line,
4244                    column: name_tok.column,
4245                },
4246            };
4247            // Optional `: { semantics: "…", default_confidence: 0.8 }`.
4248            if self.check(TokenType::Colon) {
4249                self.advance();
4250                self.consume(TokenType::LBrace)?;
4251                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4252                    let mkey = self.current().value.clone();
4253                    self.advance();
4254                    if self.check(TokenType::Colon) {
4255                        self.advance();
4256                        match mkey.as_str() {
4257                            "semantics" => {
4258                                member.semantics =
4259                                    Some(self.consume(TokenType::StringLit)?.value.clone())
4260                            }
4261                            "default_confidence" => {
4262                                member.default_confidence = self.parse_optional_float()
4263                            }
4264                            _ => self.skip_value(),
4265                        }
4266                    }
4267                    if self.check(TokenType::Comma) {
4268                        self.advance();
4269                    }
4270                }
4271                self.consume(TokenType::RBrace)?;
4272            }
4273            members.push(member);
4274            if self.check(TokenType::Comma) {
4275                self.advance();
4276            }
4277        }
4278        self.consume(TokenType::RBracket)?;
4279        Ok(members)
4280    }
4281
4282    fn parse_shield(&mut self) -> Result<ShieldDefinition, ParseError> {
4283        let tok = self.consume(TokenType::Shield)?;
4284        let name = self.consume(TokenType::Identifier)?.value;
4285        let mut node = ShieldDefinition {
4286            name,
4287            scan: Vec::new(),
4288            strategy: String::new(),
4289            on_breach: String::new(),
4290            severity: String::new(),
4291            quarantine: String::new(),
4292            max_retries: None,
4293            confidence_threshold: None,
4294            allow_tools: Vec::new(),
4295            deny_tools: Vec::new(),
4296            sandbox: None,
4297            redact: Vec::new(),
4298            log: String::new(),
4299            deflect_message: String::new(),
4300            taint: String::new(),
4301            compliance: Vec::new(),
4302            loc: Loc {
4303                line: tok.line,
4304                column: tok.column,
4305            },
4306            leading_trivia: Vec::new(),
4307            trailing_trivia: Vec::new(),
4308        };
4309        self.consume(TokenType::LBrace)?;
4310        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4311            let field_name = self.current().value.clone();
4312            self.advance();
4313            if self.check(TokenType::Colon) {
4314                self.advance();
4315                match field_name.as_str() {
4316                    "scan" => node.scan = self.parse_bracketed_identifiers()?,
4317                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
4318                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
4319                    "severity" => node.severity = self.consume_any_ident_or_kw()?.value.clone(),
4320                    "quarantine" => {
4321                        node.quarantine = self.consume(TokenType::StringLit)?.value.clone()
4322                    }
4323                    "max_retries" => node.max_retries = self.parse_optional_int(),
4324                    "confidence_threshold" => {
4325                        node.confidence_threshold = self.parse_optional_float()
4326                    }
4327                    "allow_tools" => node.allow_tools = self.parse_bracketed_identifiers()?,
4328                    "deny_tools" => node.deny_tools = self.parse_bracketed_identifiers()?,
4329                    "sandbox" => {
4330                        node.sandbox = Some(self.consume_any_ident_or_kw()?.value == "true")
4331                    }
4332                    "redact" => node.redact = self.parse_bracketed_identifiers()?,
4333                    "log" => node.log = self.consume_any_ident_or_kw()?.value.clone(),
4334                    "deflect_message" => {
4335                        node.deflect_message = self.consume(TokenType::StringLit)?.value.clone()
4336                    }
4337                    "taint" => node.taint = self.consume_any_ident_or_kw()?.value.clone(),
4338                    // ESK Fase 6.1 — covered regulatory classes.
4339                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
4340                    _ => self.skip_value(),
4341                }
4342            } else if self.check(TokenType::LBrace) {
4343                self.skip_braced_block()?;
4344            }
4345        }
4346        self.consume(TokenType::RBrace)?;
4347        Ok(node)
4348    }
4349
4350    fn parse_pix(&mut self) -> Result<PixDefinition, ParseError> {
4351        let tok = self.consume(TokenType::Pix)?;
4352        let name = self.consume(TokenType::Identifier)?.value;
4353        let mut node = PixDefinition {
4354            name,
4355            source: String::new(),
4356            depth: None,
4357            branching: None,
4358            model: String::new(),
4359            loc: Loc {
4360                line: tok.line,
4361                column: tok.column,
4362            },
4363            leading_trivia: Vec::new(),
4364            trailing_trivia: Vec::new(),
4365        };
4366        self.consume(TokenType::LBrace)?;
4367        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4368            let field_name = self.current().value.clone();
4369            self.advance();
4370            if self.check(TokenType::Colon) {
4371                self.advance();
4372                match field_name.as_str() {
4373                    "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
4374                    "depth" => node.depth = self.parse_optional_int(),
4375                    "branching" => node.branching = self.parse_optional_int(),
4376                    "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
4377                    _ => self.skip_value(),
4378                }
4379            } else if self.check(TokenType::LBrace) {
4380                self.skip_braced_block()?;
4381            }
4382        }
4383        self.consume(TokenType::RBrace)?;
4384        Ok(node)
4385    }
4386
4387    fn parse_psyche(&mut self) -> Result<PsycheDefinition, ParseError> {
4388        let tok = self.consume(TokenType::Psyche)?;
4389        let name = self.consume(TokenType::Identifier)?.value;
4390        let mut node = PsycheDefinition {
4391            name,
4392            dimensions: Vec::new(),
4393            manifold_noise: None,
4394            manifold_momentum: None,
4395            safety_constraints: Vec::new(),
4396            quantum_enabled: None,
4397            inference_mode: String::new(),
4398            loc: Loc {
4399                line: tok.line,
4400                column: tok.column,
4401            },
4402            leading_trivia: Vec::new(),
4403            trailing_trivia: Vec::new(),
4404        };
4405        self.consume(TokenType::LBrace)?;
4406        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4407            let field_name = self.current().value.clone();
4408            self.advance();
4409            if self.check(TokenType::Colon) {
4410                self.advance();
4411                match field_name.as_str() {
4412                    "dimensions" => node.dimensions = self.parse_bracketed_identifiers()?,
4413                    "manifold_noise" => node.manifold_noise = self.parse_optional_float(),
4414                    "manifold_momentum" => node.manifold_momentum = self.parse_optional_float(),
4415                    "safety_constraints" => {
4416                        node.safety_constraints = self.parse_bracketed_identifiers()?
4417                    }
4418                    "quantum_enabled" => {
4419                        node.quantum_enabled = Some(self.consume_any_ident_or_kw()?.value == "true")
4420                    }
4421                    "inference_mode" => {
4422                        node.inference_mode = self.consume_any_ident_or_kw()?.value.clone()
4423                    }
4424                    _ => self.skip_value(),
4425                }
4426            } else if self.check(TokenType::LBrace) {
4427                self.skip_braced_block()?;
4428            }
4429        }
4430        self.consume(TokenType::RBrace)?;
4431        Ok(node)
4432    }
4433
4434    fn parse_corpus(&mut self) -> Result<CorpusDefinition, ParseError> {
4435        let tok = self.consume(TokenType::Corpus)?;
4436        let name = self.consume(TokenType::Identifier)?.value;
4437        let mut node = CorpusDefinition {
4438            name,
4439            documents: Vec::new(),
4440            mcp_server: String::new(),
4441            mcp_resource_uri: String::new(),
4442            loc: Loc {
4443                line: tok.line,
4444                column: tok.column,
4445            },
4446            leading_trivia: Vec::new(),
4447            trailing_trivia: Vec::new(),
4448        };
4449        // corpus Name from mcp("server", "uri") — short form
4450        if self.check(TokenType::From) {
4451            self.advance();
4452            self.consume(TokenType::Mcp)?;
4453            self.consume(TokenType::LParen)?;
4454            node.mcp_server = self.consume(TokenType::StringLit)?.value.clone();
4455            self.consume(TokenType::Comma)?;
4456            node.mcp_resource_uri = self.consume(TokenType::StringLit)?.value.clone();
4457            self.consume(TokenType::RParen)?;
4458            return Ok(node);
4459        }
4460        self.consume(TokenType::LBrace)?;
4461        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4462            let field_name = self.current().value.clone();
4463            self.advance();
4464            if self.check(TokenType::Colon) {
4465                self.advance();
4466                match field_name.as_str() {
4467                    "documents" => node.documents = self.parse_bracketed_identifiers()?,
4468                    _ => self.skip_value(),
4469                }
4470            } else if self.check(TokenType::LBrace) {
4471                self.skip_braced_block()?;
4472            }
4473        }
4474        self.consume(TokenType::RBrace)?;
4475        Ok(node)
4476    }
4477
4478    fn parse_dataspace(&mut self) -> Result<DataspaceDefinition, ParseError> {
4479        let tok = self.consume(TokenType::Dataspace)?;
4480        let name = self.consume(TokenType::Identifier)?.value;
4481        let node = DataspaceDefinition {
4482            name,
4483            loc: Loc {
4484                line: tok.line,
4485                column: tok.column,
4486            },
4487            leading_trivia: Vec::new(),
4488            trailing_trivia: Vec::new(),
4489        };
4490        if self.check(TokenType::LBrace) {
4491            self.skip_braced_block()?;
4492        }
4493        Ok(node)
4494    }
4495
4496    fn parse_ots(&mut self) -> Result<OtsDefinition, ParseError> {
4497        let tok = self.consume(TokenType::Ots)?;
4498        let name = self.consume(TokenType::Identifier)?.value;
4499        let mut node = OtsDefinition {
4500            name,
4501            teleology: String::new(),
4502            homotopy_search: String::new(),
4503            loss_function: String::new(),
4504            loc: Loc {
4505                line: tok.line,
4506                column: tok.column,
4507            },
4508            leading_trivia: Vec::new(),
4509            trailing_trivia: Vec::new(),
4510        };
4511        // Skip optional type params <In, Out>
4512        if self.check(TokenType::Lt) {
4513            while !self.check(TokenType::Gt) && !self.check(TokenType::Eof) {
4514                self.advance();
4515            }
4516            if self.check(TokenType::Gt) {
4517                self.advance();
4518            }
4519        }
4520        self.consume(TokenType::LBrace)?;
4521        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4522            let field_name = self.current().value.clone();
4523            self.advance();
4524            if self.check(TokenType::Colon) {
4525                self.advance();
4526                match field_name.as_str() {
4527                    "teleology" => {
4528                        node.teleology = self.consume(TokenType::StringLit)?.value.clone()
4529                    }
4530                    "homotopy_search" => {
4531                        node.homotopy_search = self.consume_any_ident_or_kw()?.value.clone()
4532                    }
4533                    "loss_function" => {
4534                        node.loss_function = self.consume(TokenType::StringLit)?.value.clone()
4535                    }
4536                    _ => self.skip_value(),
4537                }
4538            } else if self.check(TokenType::LBrace) {
4539                self.skip_braced_block()?;
4540            }
4541        }
4542        self.consume(TokenType::RBrace)?;
4543        Ok(node)
4544    }
4545
4546    fn parse_mandate(&mut self) -> Result<MandateDefinition, ParseError> {
4547        let tok = self.consume(TokenType::Mandate)?;
4548        let name = self.consume(TokenType::Identifier)?.value;
4549        let mut node = MandateDefinition {
4550            name,
4551            constraint: String::new(),
4552            kp: None,
4553            ki: None,
4554            kd: None,
4555            tolerance: None,
4556            max_steps: None,
4557            on_violation: String::new(),
4558            loc: Loc {
4559                line: tok.line,
4560                column: tok.column,
4561            },
4562            leading_trivia: Vec::new(),
4563            trailing_trivia: Vec::new(),
4564        };
4565        self.consume(TokenType::LBrace)?;
4566        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4567            let field_name = self.current().value.clone();
4568            self.advance();
4569            if self.check(TokenType::Colon) {
4570                self.advance();
4571                match field_name.as_str() {
4572                    "constraint" => {
4573                        node.constraint = self.consume(TokenType::StringLit)?.value.clone()
4574                    }
4575                    "kp" | "Kp" => node.kp = self.parse_optional_float(),
4576                    "ki" | "Ki" => node.ki = self.parse_optional_float(),
4577                    "kd" | "Kd" => node.kd = self.parse_optional_float(),
4578                    "tolerance" => node.tolerance = self.parse_optional_float(),
4579                    "max_steps" => node.max_steps = self.parse_optional_int(),
4580                    "on_violation" => {
4581                        node.on_violation = self.consume_any_ident_or_kw()?.value.clone()
4582                    }
4583                    _ => self.skip_value(),
4584                }
4585            } else if self.check(TokenType::LBrace) {
4586                self.skip_braced_block()?;
4587            }
4588        }
4589        self.consume(TokenType::RBrace)?;
4590        Ok(node)
4591    }
4592
4593    fn parse_compute(&mut self) -> Result<ComputeDefinition, ParseError> {
4594        let tok = self.consume(TokenType::Compute)?;
4595        let name = self.consume(TokenType::Identifier)?.value;
4596        let mut node = ComputeDefinition {
4597            name,
4598            shield_ref: String::new(),
4599            loc: Loc {
4600                line: tok.line,
4601                column: tok.column,
4602            },
4603            leading_trivia: Vec::new(),
4604            trailing_trivia: Vec::new(),
4605        };
4606        // Skip optional parameters/return type before brace
4607        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
4608            self.advance();
4609        }
4610        self.consume(TokenType::LBrace)?;
4611        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4612            let field_name = self.current().value.clone();
4613            self.advance();
4614            if self.check(TokenType::Colon) {
4615                self.advance();
4616                match field_name.as_str() {
4617                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
4618                    _ => self.skip_value(),
4619                }
4620            } else if self.check(TokenType::LBrace) {
4621                self.skip_braced_block()?;
4622            }
4623        }
4624        self.consume(TokenType::RBrace)?;
4625        Ok(node)
4626    }
4627
4628    fn parse_daemon(&mut self) -> Result<DaemonDefinition, ParseError> {
4629        let tok = self.consume(TokenType::Daemon)?;
4630        let name = self.consume(TokenType::Identifier)?.value;
4631        let mut node = DaemonDefinition {
4632            name,
4633            goal: String::new(),
4634            tools: Vec::new(),
4635            memory_ref: String::new(),
4636            strategy: String::new(),
4637            on_stuck: String::new(),
4638            shield_ref: String::new(),
4639            max_tokens: None,
4640            max_time: String::new(),
4641            max_cost: None,
4642            listeners: Vec::new(),
4643            loc: Loc {
4644                line: tok.line,
4645                column: tok.column,
4646            },
4647            leading_trivia: Vec::new(),
4648            trailing_trivia: Vec::new(),
4649        };
4650        // Skip optional parameters/return type before brace
4651        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
4652            self.advance();
4653        }
4654        self.consume(TokenType::LBrace)?;
4655        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4656            let field = self.current().clone();
4657            let field_name = field.value.clone();
4658            self.advance();
4659            if self.check(TokenType::Colon) {
4660                self.advance();
4661                match field_name.as_str() {
4662                    "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
4663                    "tools" => node.tools = self.parse_bracketed_identifiers()?,
4664                    "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
4665                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
4666                    "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
4667                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
4668                    "max_tokens" => node.max_tokens = self.parse_optional_int(),
4669                    "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
4670                    "max_cost" => node.max_cost = self.parse_optional_float(),
4671                    _ => self.skip_value(),
4672                }
4673            } else if field.ttype == TokenType::Listen {
4674                // §λ-L-E Fase 13 D4 — preserve listen blocks for type
4675                // checking.  We backtracked past the `listen` keyword
4676                // by `advance()` above, so reconstruct a synthetic
4677                // listener using the same dual-mode dispatch the flow
4678                // step parser uses (string topic OR typed channel ref).
4679                let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
4680                    (self.consume(TokenType::StringLit)?.value.clone(), false)
4681                } else {
4682                    (self.consume_any_ident_or_kw()?.value.clone(), true)
4683                };
4684                let mut alias = String::new();
4685                if !self.at_declaration_start()
4686                    && !self.check(TokenType::RBrace)
4687                    && !self.check(TokenType::LBrace)
4688                {
4689                    let next = self.current().clone();
4690                    if next.value == "as" || next.ttype == TokenType::As {
4691                        self.advance();
4692                        alias = self.consume_any_ident_or_kw()?.value.clone();
4693                    }
4694                }
4695                let listen_loc = Loc {
4696                    line: field.line,
4697                    column: field.column,
4698                };
4699                if self.check(TokenType::LBrace) {
4700                    self.skip_braced_block()?;
4701                }
4702                node.listeners.push(ListenStep {
4703                    channel,
4704                    channel_is_ref,
4705                    event_alias: alias,
4706                    loc: listen_loc,
4707                });
4708            } else if self.check(TokenType::LBrace) {
4709                self.skip_braced_block()?;
4710            }
4711        }
4712        self.consume(TokenType::RBrace)?;
4713        Ok(node)
4714    }
4715
4716    fn parse_axonstore(&mut self) -> Result<AxonStoreDefinition, ParseError> {
4717        let tok = self.consume(TokenType::AxonStore)?;
4718        let name = self.consume(TokenType::Identifier)?.value;
4719        let mut node = AxonStoreDefinition {
4720            name,
4721            backend: String::new(),
4722            connection: String::new(),
4723            confidence_floor: None,
4724            isolation: String::new(),
4725            on_breach: String::new(),
4726            capability: String::new(),
4727            column_schema: None,
4728            loc: Loc {
4729                line: tok.line,
4730                column: tok.column,
4731            },
4732            leading_trivia: Vec::new(),
4733            trailing_trivia: Vec::new(),
4734        };
4735        self.consume(TokenType::LBrace)?;
4736        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4737            let field = self.current().clone();
4738            let field_name = field.value.clone();
4739            // §Fase 38.b (D1) — `schema:` declaration in three closed
4740            // forms: inline column block, manifest reference (string
4741            // literal), or env-var schema namespace (`env:VAR` —
4742            // unquoted or quoted). Parse the form; the §38.d / §38.e
4743            // type-checker consumes the resulting AST.
4744            if field.ttype == TokenType::Schema {
4745                self.advance();
4746                let parsed = self.parse_store_schema_declaration(&node.name, field.line, field.column)?;
4747                node.column_schema = Some(parsed);
4748                continue;
4749            }
4750            self.advance();
4751            if self.check(TokenType::Colon) {
4752                self.advance();
4753                match field_name.as_str() {
4754                    "backend" => node.backend = self.consume_any_ident_or_kw()?.value.clone(),
4755                    "connection" => {
4756                        node.connection = self.consume(TokenType::StringLit)?.value.clone()
4757                    }
4758                    "confidence_floor" => node.confidence_floor = self.parse_optional_float(),
4759                    "isolation" => node.isolation = self.consume_any_ident_or_kw()?.value.clone(),
4760                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
4761                    // §Fase 35.j (D11) — Pillar IV: the capability slug
4762                    // required to access this store. Validated against
4763                    // the closed slug grammar shared with `requires:`.
4764                    "capability" => {
4765                        let slug_tok = self.consume(TokenType::StringLit)?.clone();
4766                        if !is_valid_capability_slug(&slug_tok.value) {
4767                            return Err(ParseError {
4768                                message: format!(
4769                                    "Invalid capability slug '{}' in axonstore '{}' \
4770                                     `capability:`. Capability slugs must match \
4771                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
4772                                     lowercase identifiers starting with a letter. Examples: \
4773                                     `admin`, `tenant.read`, `hipaa.phi.read`.",
4774                                    slug_tok.value, node.name
4775                                ),
4776                                line: slug_tok.line,
4777                                column: slug_tok.column,
4778                                ..Default::default()
4779                            });
4780                        }
4781                        node.capability = slug_tok.value.clone();
4782                    }
4783                    _ => self.skip_value(),
4784                }
4785            } else if self.check(TokenType::LBrace) {
4786                self.skip_braced_block()?;
4787            }
4788        }
4789        self.consume(TokenType::RBrace)?;
4790        Ok(node)
4791    }
4792
4793    /// §Fase 38.b (D1) — parse the three closed forms of an `axonstore`
4794    /// `schema:` declaration:
4795    ///
4796    ///   * form (a) **inline** — `schema { col: Type [constraint…], … }`
4797    ///   * form (b) **manifest reference** — `schema: "qualified.name"`
4798    ///     (string literal that does NOT start with `env:`)
4799    ///   * form (c) **env-var schema namespace** — `schema: env:VAR`
4800    ///     (unquoted) OR `schema: "env:VAR"` (quoted; the literal
4801    ///     starts with `env:`)
4802    ///
4803    /// Called immediately AFTER `schema` is consumed.
4804    fn parse_store_schema_declaration(
4805        &mut self,
4806        store_name: &str,
4807        sch_line: u32,
4808        sch_col: u32,
4809    ) -> Result<crate::store_schema::StoreColumnSchema, ParseError> {
4810        use crate::store_schema::{StoreColumn, StoreColumnSchema, StoreColumnType};
4811
4812        // — Form (a) — inline column block: `schema { ... }`. —
4813        if self.check(TokenType::LBrace) {
4814            self.consume(TokenType::LBrace)?;
4815            let mut columns: Vec<StoreColumn> = Vec::new();
4816            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4817                let col_tok = self.current().clone();
4818                let col_name = self.consume_any_ident_or_kw()?.value.clone();
4819                self.consume(TokenType::Colon)?;
4820                let type_tok = self.consume_any_ident_or_kw()?.clone();
4821                let col_type = StoreColumnType::from_token(&type_tok.value).ok_or_else(|| {
4822                    let names = StoreColumnType::all_canonical_names();
4823                    let suggestion =
4824                        crate::smart_suggest::suggest_for(&type_tok.value, &names);
4825                    let suggest_suffix = if suggestion.is_empty() {
4826                        String::new()
4827                    } else {
4828                        format!(" {suggestion}")
4829                    };
4830                    let known = names.join(", ");
4831                    ParseError {
4832                        message: format!(
4833                            "Unknown column type `{}` for column `{}` in \
4834                             axonstore `{}` `schema:` block. The closed \
4835                             v1.38.0 column-type catalog (Fase 38.b D1) \
4836                             is {{{known}}} (plus common lowercase \
4837                             aliases — `int`/`integer`/`int4` for \
4838                             `Int`, `bool`/`boolean` for `Bool`, etc.).\
4839                             {suggest_suffix}",
4840                            type_tok.value, col_name, store_name
4841                        ),
4842                        line: type_tok.line,
4843                        column: type_tok.column,
4844                        ..Default::default()
4845                    }
4846                })?;
4847
4848                let mut col = StoreColumn {
4849                    name: col_name,
4850                    col_type,
4851                    primary_key: false,
4852                    auto_increment: false,
4853                    not_null: false,
4854                    unique: false,
4855                    default_value: String::new(),
4856                    // §Fase 38.x.d (D1) — `identity` is now a recognized
4857                    // inline keyword (see the constraint loop below).
4858                    // Defaults to false; set to true when the adopter
4859                    // writes `id: BigInt primary_key identity`.
4860                    identity: false,
4861                    line: col_tok.line,
4862                    column: col_tok.column,
4863                };
4864
4865                // Trailing constraints (position-independent), matching
4866                // the Python `_parse_store_column` surface.
4867                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4868                    if self.current().ttype != TokenType::Identifier {
4869                        // The next column starts with a non-identifier
4870                        // (rare) — stop the constraint scan.
4871                        break;
4872                    }
4873                    let constraint = self.current().value.clone();
4874                    match constraint.as_str() {
4875                        "primary_key" => {
4876                            col.primary_key = true;
4877                            self.advance();
4878                        }
4879                        "auto_increment" => {
4880                            col.auto_increment = true;
4881                            self.advance();
4882                        }
4883                        "not_null" => {
4884                            col.not_null = true;
4885                            self.advance();
4886                        }
4887                        "unique" => {
4888                            col.unique = true;
4889                            self.advance();
4890                        }
4891                        // §Fase 38.x.d (D1) — `identity` marks a column
4892                        // as `GENERATED ALWAYS/BY DEFAULT AS IDENTITY`.
4893                        // Distinct from `auto_increment` (legacy SERIAL
4894                        // via `nextval(...)` default). T803 skips
4895                        // identity columns from the NOT-NULL-omission
4896                        // check because Postgres auto-fills them; the
4897                        // distinction matters because IDENTITY ALWAYS
4898                        // also rejects user-supplied values, where
4899                        // SERIAL accepts them (a future 38.x.e arm in
4900                        // T802 may surface this).
4901                        "identity" => {
4902                            col.identity = true;
4903                            self.advance();
4904                        }
4905                        "default" => {
4906                            self.advance();
4907                            let dv = self.current().clone();
4908                            if matches!(
4909                                dv.ttype,
4910                                TokenType::StringLit
4911                                    | TokenType::Integer
4912                                    | TokenType::Float
4913                            ) {
4914                                col.default_value = dv.value.clone();
4915                                self.advance();
4916                            } else {
4917                                col.default_value =
4918                                    self.consume_any_ident_or_kw()?.value.clone();
4919                            }
4920                        }
4921                        _ => break,
4922                    }
4923                }
4924
4925                columns.push(col);
4926            }
4927            self.consume(TokenType::RBrace)?;
4928            return Ok(StoreColumnSchema::Inline {
4929                columns,
4930                leading_trivia: Vec::new(),
4931                line: sch_line,
4932                column: sch_col,
4933            });
4934        }
4935
4936        // — Forms (b) + (c) require a `:` separator. —
4937        if !self.check(TokenType::Colon) {
4938            let cur = self.current().clone();
4939            return Err(ParseError {
4940                message: format!(
4941                    "axonstore `{store_name}` `schema:` declaration expects \
4942                     `{{ … }}` (inline columns), `: \"manifest.ref\"` \
4943                     (manifest reference), or `: env:VAR` (per-tenant schema \
4944                     namespace). Got `{}` instead.",
4945                    cur.value
4946                ),
4947                line: cur.line,
4948                column: cur.column,
4949                ..Default::default()
4950            });
4951        }
4952        self.consume(TokenType::Colon)?;
4953
4954        // — Form (b) or (c)-quoted — string literal value. —
4955        if self.check(TokenType::StringLit) {
4956            let lit = self.consume(TokenType::StringLit)?.clone();
4957            let value = lit.value.clone();
4958            if let Some(var) = value.strip_prefix("env:") {
4959                let var = var.trim();
4960                if var.is_empty() {
4961                    return Err(ParseError {
4962                        message: format!(
4963                            "axonstore `{store_name}` `schema: \"env:\"` is \
4964                             missing the variable name after the `env:` \
4965                             prefix."
4966                        ),
4967                        line: lit.line,
4968                        column: lit.column,
4969                        ..Default::default()
4970                    });
4971                }
4972                return Ok(StoreColumnSchema::EnvVar {
4973                    var_name: var.to_string(),
4974                    line: sch_line,
4975                    column: sch_col,
4976                });
4977            }
4978            // Plain string → manifest reference.
4979            if value.trim().is_empty() {
4980                return Err(ParseError {
4981                    message: format!(
4982                        "axonstore `{store_name}` `schema:` manifest reference \
4983                         is empty. Expected `\"qualified.name\"` — e.g. \
4984                         `\"public.tenants\"`."
4985                    ),
4986                    line: lit.line,
4987                    column: lit.column,
4988                    ..Default::default()
4989                });
4990            }
4991            return Ok(StoreColumnSchema::ManifestRef {
4992                qualified_name: value,
4993                line: sch_line,
4994                column: sch_col,
4995            });
4996        }
4997
4998        // — Form (c) unquoted — `env:VAR`. The lexer emits `env` as an
4999        //   identifier, then `:`, then the identifier var name. —
5000        let env_tok = self.current().clone();
5001        if env_tok.value == "env" {
5002            self.advance();
5003            if !self.check(TokenType::Colon) {
5004                return Err(ParseError {
5005                    message: format!(
5006                        "axonstore `{store_name}` `schema: env` is missing the \
5007                         `:` separator. Expected `schema: env:VAR`."
5008                    ),
5009                    line: env_tok.line,
5010                    column: env_tok.column,
5011                    ..Default::default()
5012                });
5013            }
5014            self.advance(); // past ':'
5015            let var_tok = self.consume_any_ident_or_kw()?.clone();
5016            if var_tok.value.trim().is_empty() {
5017                return Err(ParseError {
5018                    message: format!(
5019                        "axonstore `{store_name}` `schema: env:` is missing \
5020                         the variable name."
5021                    ),
5022                    line: var_tok.line,
5023                    column: var_tok.column,
5024                    ..Default::default()
5025                });
5026            }
5027            return Ok(StoreColumnSchema::EnvVar {
5028                var_name: var_tok.value.clone(),
5029                line: sch_line,
5030                column: sch_col,
5031            });
5032        }
5033
5034        Err(ParseError {
5035            message: format!(
5036                "axonstore `{store_name}` `schema:` declaration expects \
5037                 `{{ … }}` (inline columns), `\"manifest.ref\"` (manifest \
5038                 reference), or `env:VAR` (per-tenant schema namespace). \
5039                 Got `{}` instead.",
5040                env_tok.value
5041            ),
5042            line: env_tok.line,
5043            column: env_tok.column,
5044            ..Default::default()
5045        })
5046    }
5047
5048    // ── §λ-L-E Fase 1 — Resource primitive ────────────────────────
5049
5050    /// Parse: `resource Name { kind, endpoint, capacity, lifetime, certainty_floor, shield }`.
5051    ///
5052    /// Mirrors `axon.compiler.parser.Parser._parse_resource`. Unknown fields
5053    /// are silently skipped (keeps the grammar forward-compatible).
5054    fn parse_resource(&mut self) -> Result<ResourceDefinition, ParseError> {
5055        let tok = self.consume(TokenType::Resource)?;
5056        let name = self.consume(TokenType::Identifier)?.value;
5057        let mut node = ResourceDefinition {
5058            name,
5059            kind: String::new(),
5060            endpoint: String::new(),
5061            capacity: None,
5062            lifetime: "affine".to_string(),
5063            certainty_floor: None,
5064            shield_ref: String::new(),
5065            loc: Loc {
5066                line: tok.line,
5067                column: tok.column,
5068            },
5069            leading_trivia: Vec::new(),
5070            trailing_trivia: Vec::new(),
5071        };
5072        self.consume(TokenType::LBrace)?;
5073        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5074            let field_tok = self.current().clone();
5075            let field_name = field_tok.value.clone();
5076            self.advance();
5077            if !self.check(TokenType::Colon) {
5078                // Tolerate stray brace or unknown layout.
5079                if self.check(TokenType::LBrace) {
5080                    self.skip_braced_block()?;
5081                }
5082                continue;
5083            }
5084            self.advance(); // past ':'
5085            match field_name.as_str() {
5086                "kind" => node.kind = self.consume_any_ident_or_kw()?.value,
5087                "endpoint" => node.endpoint = self.consume(TokenType::StringLit)?.value,
5088                "capacity" => {
5089                    node.capacity = self.parse_optional_int();
5090                }
5091                "lifetime" => {
5092                    let lt_tok = self.consume_any_ident_or_kw()?;
5093                    let lt = lt_tok.value;
5094                    if !matches!(lt.as_str(), "linear" | "affine" | "persistent") {
5095                        return Err(ParseError {
5096                            message: format!(
5097                                "Invalid lifetime '{lt}' in resource '{}' — \
5098                                 expected linear | affine | persistent",
5099                                node.name
5100                            ),
5101                            line: lt_tok.line,
5102                            column: lt_tok.column,
5103                                                    ..Default::default()
5104                        });
5105                    }
5106                    node.lifetime = lt;
5107                }
5108                "certainty_floor" => {
5109                    node.certainty_floor = self.parse_optional_float();
5110                }
5111                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
5112                _ => self.skip_value(),
5113            }
5114        }
5115        self.consume(TokenType::RBrace)?;
5116        Ok(node)
5117    }
5118
5119    /// Parse: `fabric Name { provider, region, zones, ephemeral, shield }`.
5120    fn parse_fabric(&mut self) -> Result<FabricDefinition, ParseError> {
5121        let tok = self.consume(TokenType::Fabric)?;
5122        let name = self.consume(TokenType::Identifier)?.value;
5123        let mut node = FabricDefinition {
5124            name,
5125            provider: String::new(),
5126            region: String::new(),
5127            zones: None,
5128            ephemeral: None,
5129            shield_ref: String::new(),
5130            loc: Loc {
5131                line: tok.line,
5132                column: tok.column,
5133            },
5134            leading_trivia: Vec::new(),
5135            trailing_trivia: Vec::new(),
5136        };
5137        self.consume(TokenType::LBrace)?;
5138        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5139            let field_name = self.current().value.clone();
5140            self.advance();
5141            if !self.check(TokenType::Colon) {
5142                if self.check(TokenType::LBrace) {
5143                    self.skip_braced_block()?;
5144                }
5145                continue;
5146            }
5147            self.advance(); // past ':'
5148            match field_name.as_str() {
5149                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
5150                "region" => node.region = self.consume(TokenType::StringLit)?.value,
5151                "zones" => node.zones = self.parse_optional_int(),
5152                "ephemeral" => {
5153                    let b = self.parse_bool()?;
5154                    node.ephemeral = Some(b);
5155                }
5156                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
5157                _ => self.skip_value(),
5158            }
5159        }
5160        self.consume(TokenType::RBrace)?;
5161        Ok(node)
5162    }
5163
5164    /// Parse: `manifest Name { resources, fabric, region, zones, compliance }`.
5165    fn parse_manifest(&mut self) -> Result<ManifestDefinition, ParseError> {
5166        let tok = self.consume(TokenType::Manifest)?;
5167        let name = self.consume(TokenType::Identifier)?.value;
5168        let mut node = ManifestDefinition {
5169            name,
5170            resources: Vec::new(),
5171            fabric_ref: String::new(),
5172            region: String::new(),
5173            zones: None,
5174            compliance: Vec::new(),
5175            loc: Loc {
5176                line: tok.line,
5177                column: tok.column,
5178            },
5179            leading_trivia: Vec::new(),
5180            trailing_trivia: Vec::new(),
5181        };
5182        self.consume(TokenType::LBrace)?;
5183        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5184            let field_name = self.current().value.clone();
5185            self.advance();
5186            if !self.check(TokenType::Colon) {
5187                if self.check(TokenType::LBrace) {
5188                    self.skip_braced_block()?;
5189                }
5190                continue;
5191            }
5192            self.advance();
5193            match field_name.as_str() {
5194                "resources" => node.resources = self.parse_bracketed_identifiers()?,
5195                "fabric" => node.fabric_ref = self.consume_any_ident_or_kw()?.value,
5196                "region" => node.region = self.consume(TokenType::StringLit)?.value,
5197                "zones" => node.zones = self.parse_optional_int(),
5198                "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
5199                _ => self.skip_value(),
5200            }
5201        }
5202        self.consume(TokenType::RBrace)?;
5203        Ok(node)
5204    }
5205
5206    /// Parse: `observe Name from Manifest { sources, quorum, timeout, on_partition, certainty_floor }`.
5207    fn parse_observe(&mut self) -> Result<ObserveDefinition, ParseError> {
5208        let tok = self.consume(TokenType::Observe)?;
5209        let name = self.consume(TokenType::Identifier)?.value;
5210        // `from <Manifest>` — required per Python grammar.
5211        self.consume(TokenType::From)?;
5212        let target = self.consume(TokenType::Identifier)?.value;
5213        let mut node = ObserveDefinition {
5214            name,
5215            target,
5216            sources: Vec::new(),
5217            quorum: None,
5218            timeout: String::new(),
5219            on_partition: "fail".to_string(),
5220            certainty_floor: None,
5221            loc: Loc {
5222                line: tok.line,
5223                column: tok.column,
5224            },
5225            leading_trivia: Vec::new(),
5226            trailing_trivia: Vec::new(),
5227        };
5228        self.consume(TokenType::LBrace)?;
5229        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5230            let field_name = self.current().value.clone();
5231            self.advance();
5232            if !self.check(TokenType::Colon) {
5233                if self.check(TokenType::LBrace) {
5234                    self.skip_braced_block()?;
5235                }
5236                continue;
5237            }
5238            self.advance();
5239            match field_name.as_str() {
5240                "sources" => node.sources = self.parse_bracketed_identifiers()?,
5241                "quorum" => node.quorum = self.parse_optional_int(),
5242                "timeout" => {
5243                    let t = self.current().clone();
5244                    match t.ttype {
5245                        TokenType::Duration | TokenType::StringLit => {
5246                            self.advance();
5247                            node.timeout = t.value;
5248                        }
5249                        _ => node.timeout = self.consume_any_ident_or_kw()?.value,
5250                    }
5251                }
5252                "on_partition" => {
5253                    let p_tok = self.consume_any_ident_or_kw()?;
5254                    let p = p_tok.value;
5255                    if !matches!(p.as_str(), "fail" | "shield_quarantine") {
5256                        return Err(ParseError {
5257                            message: format!(
5258                                "Invalid on_partition '{p}' in observe '{}' — \
5259                                 expected fail | shield_quarantine",
5260                                node.name
5261                            ),
5262                            line: p_tok.line,
5263                            column: p_tok.column,
5264                                                    ..Default::default()
5265                        });
5266                    }
5267                    node.on_partition = p;
5268                }
5269                "certainty_floor" => node.certainty_floor = self.parse_optional_float(),
5270                _ => self.skip_value(),
5271            }
5272        }
5273        self.consume(TokenType::RBrace)?;
5274        Ok(node)
5275    }
5276
5277    // ── §λ-L-E Fase 3 — Control cognitivo ─────────────────────────
5278
5279    /// Parse: `reconcile Name { observe, threshold, tolerance, on_drift, shield, mandate, max_retries }`.
5280    fn parse_reconcile(&mut self) -> Result<ReconcileDefinition, ParseError> {
5281        let tok = self.consume(TokenType::Reconcile)?;
5282        let name = self.consume(TokenType::Identifier)?.value;
5283        let mut node = ReconcileDefinition {
5284            name,
5285            observe_ref: String::new(),
5286            threshold: None,
5287            tolerance: None,
5288            on_drift: "provision".to_string(),
5289            shield_ref: String::new(),
5290            mandate_ref: String::new(),
5291            max_retries: 3,
5292            loc: Loc {
5293                line: tok.line,
5294                column: tok.column,
5295            },
5296            leading_trivia: Vec::new(),
5297            trailing_trivia: Vec::new(),
5298        };
5299        self.consume(TokenType::LBrace)?;
5300        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5301            let field_name = self.current().value.clone();
5302            self.advance();
5303            if !self.check(TokenType::Colon) {
5304                if self.check(TokenType::LBrace) {
5305                    self.skip_braced_block()?;
5306                }
5307                continue;
5308            }
5309            self.advance();
5310            match field_name.as_str() {
5311                "observe" => node.observe_ref = self.consume_any_ident_or_kw()?.value,
5312                "threshold" => node.threshold = self.parse_optional_float(),
5313                "tolerance" => node.tolerance = self.parse_optional_float(),
5314                "on_drift" => {
5315                    let d_tok = self.consume_any_ident_or_kw()?;
5316                    let d = d_tok.value;
5317                    if !matches!(d.as_str(), "provision" | "alert" | "refine") {
5318                        return Err(ParseError {
5319                            message: format!(
5320                                "Invalid on_drift '{d}' in reconcile '{}' — \
5321                                 expected provision | alert | refine",
5322                                node.name
5323                            ),
5324                            line: d_tok.line,
5325                            column: d_tok.column,
5326                                                    ..Default::default()
5327                        });
5328                    }
5329                    node.on_drift = d;
5330                }
5331                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
5332                "mandate" => node.mandate_ref = self.consume_any_ident_or_kw()?.value,
5333                "max_retries" => {
5334                    if let Some(v) = self.parse_optional_int() {
5335                        node.max_retries = v;
5336                    }
5337                }
5338                _ => self.skip_value(),
5339            }
5340        }
5341        self.consume(TokenType::RBrace)?;
5342        Ok(node)
5343    }
5344
5345    /// Parse: `lease Name { resource, duration, acquire, on_expire }`.
5346    fn parse_lease(&mut self) -> Result<LeaseDefinition, ParseError> {
5347        let tok = self.consume(TokenType::Lease)?;
5348        let name = self.consume(TokenType::Identifier)?.value;
5349        let mut node = LeaseDefinition {
5350            name,
5351            resource_ref: String::new(),
5352            duration: String::new(),
5353            acquire: "on_start".to_string(),
5354            on_expire: "anchor_breach".to_string(),
5355            loc: Loc {
5356                line: tok.line,
5357                column: tok.column,
5358            },
5359            leading_trivia: Vec::new(),
5360            trailing_trivia: Vec::new(),
5361        };
5362        self.consume(TokenType::LBrace)?;
5363        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5364            let field_name = self.current().value.clone();
5365            self.advance();
5366            if !self.check(TokenType::Colon) {
5367                if self.check(TokenType::LBrace) {
5368                    self.skip_braced_block()?;
5369                }
5370                continue;
5371            }
5372            self.advance();
5373            match field_name.as_str() {
5374                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
5375                "duration" => {
5376                    let t = self.current().clone();
5377                    match t.ttype {
5378                        TokenType::Duration | TokenType::StringLit => {
5379                            self.advance();
5380                            node.duration = t.value;
5381                        }
5382                        _ => node.duration = self.consume_any_ident_or_kw()?.value,
5383                    }
5384                }
5385                "acquire" => {
5386                    let a_tok = self.consume_any_ident_or_kw()?;
5387                    let a = a_tok.value;
5388                    if !matches!(a.as_str(), "on_start" | "on_demand") {
5389                        return Err(ParseError {
5390                            message: format!(
5391                                "Invalid acquire '{a}' in lease '{}' — \
5392                                 expected on_start | on_demand",
5393                                node.name
5394                            ),
5395                            line: a_tok.line,
5396                            column: a_tok.column,
5397                                                    ..Default::default()
5398                        });
5399                    }
5400                    node.acquire = a;
5401                }
5402                "on_expire" => {
5403                    let e_tok = self.consume_any_ident_or_kw()?;
5404                    let e = e_tok.value;
5405                    if !matches!(e.as_str(), "anchor_breach" | "release" | "extend") {
5406                        return Err(ParseError {
5407                            message: format!(
5408                                "Invalid on_expire '{e}' in lease '{}' — \
5409                                 expected anchor_breach | release | extend",
5410                                node.name
5411                            ),
5412                            line: e_tok.line,
5413                            column: e_tok.column,
5414                                                    ..Default::default()
5415                        });
5416                    }
5417                    node.on_expire = e;
5418                }
5419                _ => self.skip_value(),
5420            }
5421        }
5422        self.consume(TokenType::RBrace)?;
5423        Ok(node)
5424    }
5425
5426    /// Parse: `ensemble Name { observations, quorum, aggregation, certainty_mode }`.
5427    fn parse_ensemble(&mut self) -> Result<EnsembleDefinition, ParseError> {
5428        let tok = self.consume(TokenType::Ensemble)?;
5429        let name = self.consume(TokenType::Identifier)?.value;
5430        let mut node = EnsembleDefinition {
5431            name,
5432            observations: Vec::new(),
5433            quorum: None,
5434            aggregation: "majority".to_string(),
5435            certainty_mode: "min".to_string(),
5436            loc: Loc {
5437                line: tok.line,
5438                column: tok.column,
5439            },
5440            leading_trivia: Vec::new(),
5441            trailing_trivia: Vec::new(),
5442        };
5443        self.consume(TokenType::LBrace)?;
5444        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5445            let field_name = self.current().value.clone();
5446            self.advance();
5447            if !self.check(TokenType::Colon) {
5448                if self.check(TokenType::LBrace) {
5449                    self.skip_braced_block()?;
5450                }
5451                continue;
5452            }
5453            self.advance();
5454            match field_name.as_str() {
5455                "observations" => node.observations = self.parse_bracketed_identifiers()?,
5456                "quorum" => node.quorum = self.parse_optional_int(),
5457                "aggregation" => {
5458                    let a_tok = self.consume_any_ident_or_kw()?;
5459                    let a = a_tok.value;
5460                    if !matches!(a.as_str(), "majority" | "weighted" | "byzantine") {
5461                        return Err(ParseError {
5462                            message: format!(
5463                                "Invalid aggregation '{a}' in ensemble '{}' — \
5464                                 expected majority | weighted | byzantine",
5465                                node.name
5466                            ),
5467                            line: a_tok.line,
5468                            column: a_tok.column,
5469                                                    ..Default::default()
5470                        });
5471                    }
5472                    node.aggregation = a;
5473                }
5474                "certainty_mode" => {
5475                    let c_tok = self.consume_any_ident_or_kw()?;
5476                    let c = c_tok.value;
5477                    if !matches!(c.as_str(), "min" | "weighted" | "harmonic") {
5478                        return Err(ParseError {
5479                            message: format!(
5480                                "Invalid certainty_mode '{c}' in ensemble '{}' — \
5481                                 expected min | weighted | harmonic",
5482                                node.name
5483                            ),
5484                            line: c_tok.line,
5485                            column: c_tok.column,
5486                                                    ..Default::default()
5487                        });
5488                    }
5489                    node.certainty_mode = c;
5490                }
5491                _ => self.skip_value(),
5492            }
5493        }
5494        self.consume(TokenType::RBrace)?;
5495        Ok(node)
5496    }
5497
5498    // ── §λ-L-E Fase 4 — Topology + π-calculus binary sessions ─────
5499
5500    /// Parse: `session Name { role1: [step, …]  role2: [step, …] }`.
5501    ///
5502    /// The enclosing `parse_session_definition` disambiguates from the session
5503    /// step token `session` (which does not exist) by always entering from the
5504    /// top-level dispatch; the identifier role name is consumed after `{`.
5505    fn parse_session_definition(&mut self) -> Result<SessionDefinition, ParseError> {
5506        let tok = self.consume(TokenType::Session)?;
5507        let name = self.consume(TokenType::Identifier)?.value;
5508        let mut node = SessionDefinition {
5509            name,
5510            roles: Vec::new(),
5511            loc: Loc {
5512                line: tok.line,
5513                column: tok.column,
5514            },
5515            leading_trivia: Vec::new(),
5516            trailing_trivia: Vec::new(),
5517        };
5518        self.consume(TokenType::LBrace)?;
5519        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5520            let role_tok = self.consume_any_ident_or_kw()?;
5521            self.consume(TokenType::Colon)?;
5522            let steps = self.parse_session_steps()?;
5523            node.roles.push(SessionRole {
5524                name: role_tok.value,
5525                steps,
5526                loc: Loc {
5527                    line: role_tok.line,
5528                    column: role_tok.column,
5529                },
5530            });
5531        }
5532        self.consume(TokenType::RBrace)?;
5533        Ok(node)
5534    }
5535
5536    /// §Fase 41.b — Parse:
5537    /// `socket Name { protocol: SessionRef, backpressure: credit(n),
5538    ///               reconnect: cognitive_state, legal_basis: ... }`.
5539    /// Fields are `key: value` pairs (order-free); only `protocol` is required.
5540    fn parse_socket(&mut self) -> Result<SocketDefinition, ParseError> {
5541        let tok = self.consume(TokenType::Socket)?;
5542        let name = self.consume(TokenType::Identifier)?.value;
5543        let mut node = SocketDefinition {
5544            name,
5545            loc: Loc { line: tok.line, column: tok.column },
5546            ..Default::default()
5547        };
5548        self.consume(TokenType::LBrace)?;
5549        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5550            let key = self.consume_any_ident_or_kw()?.value;
5551            self.consume(TokenType::Colon)?;
5552            match key.as_str() {
5553                "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
5554                "backpressure" => {
5555                    // `credit(n)` — the typed-resource window.
5556                    let kind = self.consume_any_ident_or_kw()?.value;
5557                    if kind != "credit" {
5558                        return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
5559                    }
5560                    self.consume(TokenType::LParen)?;
5561                    let n = self
5562                        .consume(TokenType::Integer)?
5563                        .value
5564                        .parse::<i64>()
5565                        .map_err(|_| self.error("backpressure credit must be an integer"))?;
5566                    self.consume(TokenType::RParen)?;
5567                    node.backpressure_credit = Some(n);
5568                }
5569                "reconnect" => {
5570                    let mode = self.consume_any_ident_or_kw()?.value;
5571                    node.reconnect = mode == "cognitive_state";
5572                }
5573                "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
5574                other => return Err(self.error(&format!("unknown socket field `{other}`"))),
5575            }
5576            // Optional comma between fields.
5577            if self.check(TokenType::Comma) {
5578                self.consume(TokenType::Comma)?;
5579            }
5580        }
5581        self.consume(TokenType::RBrace)?;
5582        Ok(node)
5583    }
5584
5585    /// Parse: `[send T, receive U, loop, end]`.
5586    fn parse_session_steps(&mut self) -> Result<Vec<SessionStep>, ParseError> {
5587        self.consume(TokenType::LBracket)?;
5588        let mut steps = Vec::new();
5589        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
5590            steps.push(self.parse_session_step()?);
5591            if self.check(TokenType::Comma) {
5592                self.advance();
5593            }
5594        }
5595        self.consume(TokenType::RBracket)?;
5596        Ok(steps)
5597    }
5598
5599    fn parse_session_step(&mut self) -> Result<SessionStep, ParseError> {
5600        let tok = self.current().clone();
5601        let loc = Loc { line: tok.line, column: tok.column };
5602        match tok.ttype {
5603            TokenType::Send => {
5604                self.advance();
5605                let msg = self.consume_any_ident_or_kw()?;
5606                Ok(SessionStep { op: "send".into(), message_type: msg.value, loc, ..Default::default() })
5607            }
5608            TokenType::Receive => {
5609                self.advance();
5610                let msg = self.consume_any_ident_or_kw()?;
5611                Ok(SessionStep { op: "receive".into(), message_type: msg.value, loc, ..Default::default() })
5612            }
5613            TokenType::Loop => {
5614                self.advance();
5615                Ok(SessionStep { op: "loop".into(), loc, ..Default::default() })
5616            }
5617            TokenType::End => {
5618                self.advance();
5619                Ok(SessionStep { op: "end".into(), loc, ..Default::default() })
5620            }
5621            // §Fase 41.b — choice: `select { ℓ: [..], … }` (⊕) | `branch { ℓ: [..], … }` (&).
5622            // `select`/`branch` are not keywords — they arrive as identifiers.
5623            TokenType::Identifier if tok.value == "select" || tok.value == "branch" => {
5624                self.parse_session_choice(&tok.value, loc)
5625            }
5626            _ => Err(ParseError {
5627                message: format!(
5628                    "Invalid session step '{}' — expected send | receive | loop | end | select | branch",
5629                    tok.value
5630                ),
5631                line: tok.line,
5632                column: tok.column,
5633                ..Default::default()
5634            }),
5635        }
5636    }
5637
5638    /// §Fase 41.b — Parse a choice step: `select { ask: [..], cancel: [..] }`
5639    /// (or `branch { … }`). Each `label: [steps]` arm is a nested sub-protocol.
5640    fn parse_session_choice(&mut self, op: &str, loc: Loc) -> Result<SessionStep, ParseError> {
5641        self.advance(); // consume `select` / `branch`
5642        self.consume(TokenType::LBrace)?;
5643        let mut branches = Vec::new();
5644        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5645            let label_tok = self.consume_any_ident_or_kw()?;
5646            self.consume(TokenType::Colon)?;
5647            let steps = self.parse_session_steps()?;
5648            branches.push(SessionBranch {
5649                label: label_tok.value,
5650                steps,
5651                loc: Loc { line: label_tok.line, column: label_tok.column },
5652            });
5653            if self.check(TokenType::Comma) {
5654                self.advance();
5655            }
5656        }
5657        self.consume(TokenType::RBrace)?;
5658        Ok(SessionStep { op: op.to_string(), branches, loc, ..Default::default() })
5659    }
5660
5661    /// Parse: `topology Name { nodes: [A, B, …]  edges: [A -> B : Session, …] }`.
5662    fn parse_topology(&mut self) -> Result<TopologyDefinition, ParseError> {
5663        let tok = self.consume(TokenType::Topology)?;
5664        let name = self.consume(TokenType::Identifier)?.value;
5665        let mut node = TopologyDefinition {
5666            name,
5667            nodes: Vec::new(),
5668            edges: Vec::new(),
5669            loc: Loc {
5670                line: tok.line,
5671                column: tok.column,
5672            },
5673            leading_trivia: Vec::new(),
5674            trailing_trivia: Vec::new(),
5675        };
5676        self.consume(TokenType::LBrace)?;
5677        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5678            let field_name = self.current().value.clone();
5679            self.advance();
5680            if !self.check(TokenType::Colon) {
5681                if self.check(TokenType::LBrace) {
5682                    self.skip_braced_block()?;
5683                }
5684                continue;
5685            }
5686            self.advance();
5687            match field_name.as_str() {
5688                "nodes" => node.nodes = self.parse_bracketed_identifiers()?,
5689                "edges" => node.edges = self.parse_topology_edges()?,
5690                _ => self.skip_value(),
5691            }
5692        }
5693        self.consume(TokenType::RBrace)?;
5694        Ok(node)
5695    }
5696
5697    fn parse_topology_edges(&mut self) -> Result<Vec<TopologyEdge>, ParseError> {
5698        self.consume(TokenType::LBracket)?;
5699        let mut edges = Vec::new();
5700        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
5701            edges.push(self.parse_topology_edge()?);
5702            if self.check(TokenType::Comma) {
5703                self.advance();
5704            }
5705        }
5706        self.consume(TokenType::RBracket)?;
5707        Ok(edges)
5708    }
5709
5710    fn parse_topology_edge(&mut self) -> Result<TopologyEdge, ParseError> {
5711        let src_tok = self.consume_any_ident_or_kw()?;
5712        self.consume(TokenType::Arrow)?;
5713        let tgt_tok = self.consume_any_ident_or_kw()?;
5714        self.consume(TokenType::Colon)?;
5715        let sess_tok = self.consume_any_ident_or_kw()?;
5716        Ok(TopologyEdge {
5717            source: src_tok.value,
5718            target: tgt_tok.value,
5719            session_ref: sess_tok.value,
5720            loc: Loc {
5721                line: src_tok.line,
5722                column: src_tok.column,
5723            },
5724        })
5725    }
5726
5727    // ── §λ-L-E Fase 5 — Cognitive immune system (paper_immune_v2.md) ────
5728
5729    /// Parse: `immune Name { watch, sensitivity, baseline, window, scope, tau, decay }`.
5730    fn parse_immune(&mut self) -> Result<ImmuneDefinition, ParseError> {
5731        let tok = self.consume(TokenType::Immune)?;
5732        let name = self.consume(TokenType::Identifier)?.value;
5733        let mut node = ImmuneDefinition {
5734            name,
5735            watch: Vec::new(),
5736            sensitivity: None,
5737            baseline: "learned".to_string(),
5738            window: 100,
5739            scope: String::new(),
5740            tau: String::new(),
5741            decay: "exponential".to_string(),
5742            loc: Loc {
5743                line: tok.line,
5744                column: tok.column,
5745            },
5746            leading_trivia: Vec::new(),
5747            trailing_trivia: Vec::new(),
5748        };
5749        self.consume(TokenType::LBrace)?;
5750        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5751            let field_name = self.current().value.clone();
5752            self.advance();
5753            if !self.check(TokenType::Colon) {
5754                if self.check(TokenType::LBrace) {
5755                    self.skip_braced_block()?;
5756                }
5757                continue;
5758            }
5759            self.advance();
5760            match field_name.as_str() {
5761                "watch" => node.watch = self.parse_bracketed_identifiers()?,
5762                "sensitivity" => node.sensitivity = self.parse_optional_float(),
5763                "baseline" => node.baseline = self.consume_any_ident_or_kw()?.value,
5764                "window" => {
5765                    if let Some(v) = self.parse_optional_int() {
5766                        node.window = v;
5767                    }
5768                }
5769                "scope" => {
5770                    let s_tok = self.consume_any_ident_or_kw()?;
5771                    let s = s_tok.value;
5772                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
5773                        return Err(ParseError {
5774                            message: format!(
5775                                "Invalid scope '{s}' in immune '{}' — \
5776                                 expected tenant | flow | global",
5777                                node.name
5778                            ),
5779                            line: s_tok.line,
5780                            column: s_tok.column,
5781                                                    ..Default::default()
5782                        });
5783                    }
5784                    node.scope = s;
5785                }
5786                "tau" => {
5787                    let t = self.current().clone();
5788                    match t.ttype {
5789                        TokenType::Duration | TokenType::StringLit => {
5790                            self.advance();
5791                            node.tau = t.value;
5792                        }
5793                        _ => node.tau = self.consume_any_ident_or_kw()?.value,
5794                    }
5795                }
5796                "decay" => {
5797                    let d_tok = self.consume_any_ident_or_kw()?;
5798                    let d = d_tok.value;
5799                    if !matches!(d.as_str(), "exponential" | "linear" | "none") {
5800                        return Err(ParseError {
5801                            message: format!(
5802                                "Invalid decay '{d}' in immune '{}' — \
5803                                 expected exponential | linear | none",
5804                                node.name
5805                            ),
5806                            line: d_tok.line,
5807                            column: d_tok.column,
5808                                                    ..Default::default()
5809                        });
5810                    }
5811                    node.decay = d;
5812                }
5813                _ => self.skip_value(),
5814            }
5815        }
5816        self.consume(TokenType::RBrace)?;
5817        Ok(node)
5818    }
5819
5820    /// Parse: `reflex Name { trigger, on_level, action, scope, sla }`.
5821    fn parse_reflex(&mut self) -> Result<ReflexDefinition, ParseError> {
5822        let tok = self.consume(TokenType::Reflex)?;
5823        let name = self.consume(TokenType::Identifier)?.value;
5824        let mut node = ReflexDefinition {
5825            name,
5826            trigger: String::new(),
5827            on_level: "doubt".to_string(),
5828            action: String::new(),
5829            scope: String::new(),
5830            sla: String::new(),
5831            loc: Loc {
5832                line: tok.line,
5833                column: tok.column,
5834            },
5835            leading_trivia: Vec::new(),
5836            trailing_trivia: Vec::new(),
5837        };
5838        self.consume(TokenType::LBrace)?;
5839        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5840            let field_name = self.current().value.clone();
5841            self.advance();
5842            if !self.check(TokenType::Colon) {
5843                if self.check(TokenType::LBrace) {
5844                    self.skip_braced_block()?;
5845                }
5846                continue;
5847            }
5848            self.advance();
5849            match field_name.as_str() {
5850                "trigger" => node.trigger = self.consume_any_ident_or_kw()?.value,
5851                "on_level" => {
5852                    let l_tok = self.consume_any_ident_or_kw()?;
5853                    let l = l_tok.value;
5854                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
5855                        return Err(ParseError {
5856                            message: format!(
5857                                "Invalid on_level '{l}' in reflex '{}' — \
5858                                 expected know | believe | speculate | doubt",
5859                                node.name
5860                            ),
5861                            line: l_tok.line,
5862                            column: l_tok.column,
5863                                                    ..Default::default()
5864                        });
5865                    }
5866                    node.on_level = l;
5867                }
5868                "action" => {
5869                    let a_tok = self.consume_any_ident_or_kw()?;
5870                    let a = a_tok.value;
5871                    if !matches!(
5872                        a.as_str(),
5873                        "drop"
5874                            | "revoke"
5875                            | "emit"
5876                            | "redact"
5877                            | "quarantine"
5878                            | "terminate"
5879                            | "alert"
5880                    ) {
5881                        return Err(ParseError {
5882                            message: format!(
5883                                "Invalid action '{a}' in reflex '{}' — \
5884                                 expected drop | revoke | emit | redact | \
5885                                 quarantine | terminate | alert",
5886                                node.name
5887                            ),
5888                            line: a_tok.line,
5889                            column: a_tok.column,
5890                                                    ..Default::default()
5891                        });
5892                    }
5893                    node.action = a;
5894                }
5895                "scope" => {
5896                    let s_tok = self.consume_any_ident_or_kw()?;
5897                    let s = s_tok.value;
5898                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
5899                        return Err(ParseError {
5900                            message: format!(
5901                                "Invalid scope '{s}' in reflex '{}' — \
5902                                 expected tenant | flow | global",
5903                                node.name
5904                            ),
5905                            line: s_tok.line,
5906                            column: s_tok.column,
5907                                                    ..Default::default()
5908                        });
5909                    }
5910                    node.scope = s;
5911                }
5912                "sla" => {
5913                    let t = self.current().clone();
5914                    match t.ttype {
5915                        TokenType::Duration | TokenType::StringLit => {
5916                            self.advance();
5917                            node.sla = t.value;
5918                        }
5919                        _ => node.sla = self.consume_any_ident_or_kw()?.value,
5920                    }
5921                }
5922                _ => self.skip_value(),
5923            }
5924        }
5925        self.consume(TokenType::RBrace)?;
5926        Ok(node)
5927    }
5928
5929    /// Parse: `heal Name { source, on_level, mode, scope, review_sla, shield, max_patches }`.
5930    fn parse_heal(&mut self) -> Result<HealDefinition, ParseError> {
5931        let tok = self.consume(TokenType::Heal)?;
5932        let name = self.consume(TokenType::Identifier)?.value;
5933        let mut node = HealDefinition {
5934            name,
5935            source: String::new(),
5936            on_level: "doubt".to_string(),
5937            mode: "human_in_loop".to_string(),
5938            scope: String::new(),
5939            review_sla: String::new(),
5940            shield_ref: String::new(),
5941            max_patches: 3,
5942            loc: Loc {
5943                line: tok.line,
5944                column: tok.column,
5945            },
5946            leading_trivia: Vec::new(),
5947            trailing_trivia: Vec::new(),
5948        };
5949        self.consume(TokenType::LBrace)?;
5950        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5951            let field_name = self.current().value.clone();
5952            self.advance();
5953            if !self.check(TokenType::Colon) {
5954                if self.check(TokenType::LBrace) {
5955                    self.skip_braced_block()?;
5956                }
5957                continue;
5958            }
5959            self.advance();
5960            match field_name.as_str() {
5961                "source" => node.source = self.consume_any_ident_or_kw()?.value,
5962                "on_level" => {
5963                    let l_tok = self.consume_any_ident_or_kw()?;
5964                    let l = l_tok.value;
5965                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
5966                        return Err(ParseError {
5967                            message: format!(
5968                                "Invalid on_level '{l}' in heal '{}' — \
5969                                 expected know | believe | speculate | doubt",
5970                                node.name
5971                            ),
5972                            line: l_tok.line,
5973                            column: l_tok.column,
5974                                                    ..Default::default()
5975                        });
5976                    }
5977                    node.on_level = l;
5978                }
5979                "mode" => {
5980                    let m_tok = self.consume_any_ident_or_kw()?;
5981                    let m = m_tok.value;
5982                    if !matches!(m.as_str(), "audit_only" | "human_in_loop" | "adversarial") {
5983                        return Err(ParseError {
5984                            message: format!(
5985                                "Invalid mode '{m}' in heal '{}' — \
5986                                 expected audit_only | human_in_loop | adversarial",
5987                                node.name
5988                            ),
5989                            line: m_tok.line,
5990                            column: m_tok.column,
5991                                                    ..Default::default()
5992                        });
5993                    }
5994                    node.mode = m;
5995                }
5996                "scope" => {
5997                    let s_tok = self.consume_any_ident_or_kw()?;
5998                    let s = s_tok.value;
5999                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
6000                        return Err(ParseError {
6001                            message: format!(
6002                                "Invalid scope '{s}' in heal '{}' — \
6003                                 expected tenant | flow | global",
6004                                node.name
6005                            ),
6006                            line: s_tok.line,
6007                            column: s_tok.column,
6008                                                    ..Default::default()
6009                        });
6010                    }
6011                    node.scope = s;
6012                }
6013                "review_sla" => {
6014                    let t = self.current().clone();
6015                    match t.ttype {
6016                        TokenType::Duration | TokenType::StringLit => {
6017                            self.advance();
6018                            node.review_sla = t.value;
6019                        }
6020                        _ => node.review_sla = self.consume_any_ident_or_kw()?.value,
6021                    }
6022                }
6023                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
6024                "max_patches" => {
6025                    if let Some(v) = self.parse_optional_int() {
6026                        node.max_patches = v;
6027                    }
6028                }
6029                _ => self.skip_value(),
6030            }
6031        }
6032        self.consume(TokenType::RBrace)?;
6033        Ok(node)
6034    }
6035
6036    // ── §λ-L-E Fase 9 — UI cognitiva (component / view) ────────────
6037
6038    /// Parse: `component Name { renders, via_shield, on_interact, render_hint }`.
6039    fn parse_component(&mut self) -> Result<ComponentDefinition, ParseError> {
6040        let tok = self.consume(TokenType::Component)?;
6041        let name = self.consume(TokenType::Identifier)?.value;
6042        let mut node = ComponentDefinition {
6043            name,
6044            renders: String::new(),
6045            via_shield: String::new(),
6046            on_interact: String::new(),
6047            render_hint: "custom".to_string(),
6048            loc: Loc {
6049                line: tok.line,
6050                column: tok.column,
6051            },
6052            leading_trivia: Vec::new(),
6053            trailing_trivia: Vec::new(),
6054        };
6055        self.consume(TokenType::LBrace)?;
6056        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6057            let field_name = self.current().value.clone();
6058            self.advance();
6059            if !self.check(TokenType::Colon) {
6060                if self.check(TokenType::LBrace) {
6061                    self.skip_braced_block()?;
6062                }
6063                continue;
6064            }
6065            self.advance();
6066            match field_name.as_str() {
6067                "renders" => node.renders = self.consume_any_ident_or_kw()?.value,
6068                "via_shield" => node.via_shield = self.consume_any_ident_or_kw()?.value,
6069                "on_interact" => node.on_interact = self.consume_any_ident_or_kw()?.value,
6070                "render_hint" => {
6071                    let h_tok = self.consume_any_ident_or_kw()?;
6072                    let h = h_tok.value;
6073                    if !matches!(h.as_str(), "card" | "list" | "form" | "chart" | "custom") {
6074                        return Err(ParseError {
6075                            message: format!(
6076                                "Invalid render_hint '{h}' in component '{}' — \
6077                                 expected card | list | form | chart | custom",
6078                                node.name
6079                            ),
6080                            line: h_tok.line,
6081                            column: h_tok.column,
6082                                                    ..Default::default()
6083                        });
6084                    }
6085                    node.render_hint = h;
6086                }
6087                _ => self.skip_value(),
6088            }
6089        }
6090        self.consume(TokenType::RBrace)?;
6091        Ok(node)
6092    }
6093
6094    /// Parse: `view Name { title, components: [...], route }`.
6095    fn parse_view(&mut self) -> Result<ViewDefinition, ParseError> {
6096        let tok = self.consume(TokenType::View)?;
6097        let name = self.consume(TokenType::Identifier)?.value;
6098        let mut node = ViewDefinition {
6099            name,
6100            title: String::new(),
6101            components: Vec::new(),
6102            route: String::new(),
6103            loc: Loc {
6104                line: tok.line,
6105                column: tok.column,
6106            },
6107            leading_trivia: Vec::new(),
6108            trailing_trivia: Vec::new(),
6109        };
6110        self.consume(TokenType::LBrace)?;
6111        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6112            let field_name = self.current().value.clone();
6113            self.advance();
6114            if !self.check(TokenType::Colon) {
6115                if self.check(TokenType::LBrace) {
6116                    self.skip_braced_block()?;
6117                }
6118                continue;
6119            }
6120            self.advance();
6121            match field_name.as_str() {
6122                "title" => node.title = self.consume(TokenType::StringLit)?.value,
6123                "components" => node.components = self.parse_bracketed_identifiers()?,
6124                "route" => node.route = self.consume(TokenType::StringLit)?.value,
6125                _ => self.skip_value(),
6126            }
6127        }
6128        self.consume(TokenType::RBrace)?;
6129        Ok(node)
6130    }
6131
6132    fn parse_axonendpoint(&mut self) -> Result<AxonEndpointDefinition, ParseError> {
6133        let tok = self.consume(TokenType::AxonEndpoint)?;
6134        let name = self.consume(TokenType::Identifier)?.value;
6135        let mut node = AxonEndpointDefinition {
6136            name,
6137            method: String::new(),
6138            path: String::new(),
6139            body_type: String::new(),
6140            execute_flow: String::new(),
6141            output_type: String::new(),
6142            shield_ref: String::new(),
6143            retries: None,
6144            timeout: String::new(),
6145            compliance: Vec::new(),
6146            // §Fase 30 — Defaults preserve backwards compat per D1.
6147            transport: "json".to_string(),
6148            keepalive: String::new(),
6149            // §Fase 31.b — Inference fields (parser-default state).
6150            // Both fields toggle/populate only when the source provides
6151            // an explicit `transport:` declaration (parser sets
6152            // `transport_explicit = true`) AND the type-checker walks
6153            // the program to compute `implicit_transport`.
6154            transport_explicit: false,
6155            implicit_transport: String::new(),
6156            // §Fase 32.g (D8) — auth scope; empty list ≡ no auth gate.
6157            requires_capabilities: Vec::new(),
6158            // §Fase 32.h — Replay-token binding (D9 plan-vivo).
6159            // Parser defaults: not explicit; effective value resolved
6160            // at deploy time using the method-default heuristic.
6161            replay_explicit: false,
6162            replay: false,
6163            // §Fase 33.z.k.b (v1.28.0) — Wire-format dialect default
6164            // empty; the runtime classifier resolves the default
6165            // dialect per the algebraic-effect predicate when the
6166            // source omits `transport: sse(<dialect>)`.
6167            transport_dialect: String::new(),
6168            // §Fase 33.z.k.1 (v1.27.1) — Algebraic-effect override.
6169            // Parser default false; populated by the type-checker's
6170            // compute_implicit_transports pass once the full program
6171            // is known (the predicate cross-references tool effects
6172            // declared anywhere in the program).
6173            has_algebraic_stream_effect: false,
6174            // §Fase 36.d (D2) — declared execution backend; empty ≡
6175            // not declared (the endpoint resolves down the Fase 36 D1
6176            // ladder). A non-empty value is validated against the
6177            // closed `AXONENDPOINT_BACKEND_VALUES` catalog below.
6178            backend: String::new(),
6179            // §Fase 37.y (D1) — Path-param names extracted from the
6180            // `path:` string AFTER the field is parsed. Initialized
6181            // empty; populated by `extract_path_param_names` after
6182            // the `path:` field is read in the loop below.
6183            path_params: Vec::new(),
6184            // §Fase 37.y (D2) — Inline `query: { name: Type, name: Type? }`
6185            // block. Initialized empty; populated by the `"query"` arm
6186            // in the field loop below. Closed catalog enforced at parse
6187            // time per `axonendpoint_is_valid_query_param_type`.
6188            query_params: Vec::new(),
6189            loc: Loc {
6190                line: tok.line,
6191                column: tok.column,
6192            },
6193            leading_trivia: Vec::new(),
6194            trailing_trivia: Vec::new(),
6195        };
6196        self.consume(TokenType::LBrace)?;
6197        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6198            let field_name = self.current().value.clone();
6199            self.advance();
6200            if self.check(TokenType::Colon) {
6201                self.advance();
6202                match field_name.as_str() {
6203                    "method" => {
6204                        // §Fase 32.b D3 — closed method enum
6205                        // `{GET, POST, PUT, DELETE, PATCH}`. Unknown
6206                        // values rejected at parse time with smart-
6207                        // suggest hint (Fase 28.e). HEAD/OPTIONS/etc.
6208                        // are runtime-managed and not adopter-
6209                        // declarable.
6210                        let value_tok = self.consume_any_ident_or_kw()?;
6211                        let value_upper = value_tok.value.to_uppercase();
6212                        if !axonendpoint_is_valid_method(&value_upper) {
6213                            let hint = crate::smart_suggest::suggest_for(
6214                                &value_upper,
6215                                AXONENDPOINT_METHOD_VALUES,
6216                            );
6217                            let base = format!(
6218                                "Invalid method '{}' in axonendpoint '{}'.",
6219                                value_tok.value, node.name
6220                            );
6221                            let message = if hint.is_empty() {
6222                                format!(
6223                                    "{base} expected GET | POST | PUT | DELETE | PATCH, found {}",
6224                                    value_tok.value
6225                                )
6226                            } else {
6227                                format!(
6228                                    "{base} {hint} (expected GET | POST | PUT | DELETE | PATCH, found {})",
6229                                    value_tok.value
6230                                )
6231                            };
6232                            return Err(ParseError {
6233                                message,
6234                                line: value_tok.line,
6235                                column: value_tok.column,
6236                                ..Default::default()
6237                            });
6238                        }
6239                        node.method = value_upper;
6240                    }
6241                    "path" => {
6242                        node.path = self.consume(TokenType::StringLit)?.value.clone();
6243                        // §Fase 37.y (D1) — extract `{name}` placeholders
6244                        // for the Request Binding Contract's path-param
6245                        // source. Duplicate `{name}` in the same path
6246                        // is rejected at parse time (HTTP route patterns
6247                        // structurally reject duplicates; surfacing the
6248                        // error here is friendlier than letting axum
6249                        // panic at registration).
6250                        match extract_path_param_names(&node.path) {
6251                            Ok(names) => node.path_params = names,
6252                            Err(dup) => {
6253                                let cur = self.current().clone();
6254                                return Err(ParseError {
6255                                    message: format!(
6256                                        "axonendpoint '{}' declares path '{}' \
6257                                         containing duplicate placeholder '{{{}}}'. \
6258                                         Each `{{name}}` in a `path:` must be \
6259                                         unique — the runtime cannot bind two \
6260                                         path segments to the same name (Fase 37.y D1).",
6261                                        node.name, node.path, dup,
6262                                    ),
6263                                    line: cur.line,
6264                                    column: cur.column,
6265                                    ..Default::default()
6266                                });
6267                            }
6268                        }
6269                    },
6270                    "body" => node.body_type = self.consume_any_ident_or_kw()?.value.clone(),
6271                    "query" => {
6272                        // §Fase 37.y (D2) — Inline query-parameter block.
6273                        // Grammar: `query: { name: Type [, name: Type?]* }`.
6274                        // Closed type catalog
6275                        // `AXONENDPOINT_QUERY_PARAM_TYPES = {Text, Int,
6276                        // Float, Bool, Uuid}`. Optional via `?` suffix
6277                        // reuses `TypeExpr.optional` semantics already in
6278                        // use for flow parameters + body type fields. A
6279                        // duplicate field name in the same block is a
6280                        // parse error (HTTP query strings DO allow
6281                        // multi-value but v1.38.5 binds the first value
6282                        // only — see plan vivo §7 forward-compat).
6283                        //
6284                        // §Fase 37.y (D2 robustness) — declaring `query:`
6285                        // twice on the same axonendpoint silently merged
6286                        // params pre-hardening. Now it's a parse error
6287                        // so an adopter typo / copy-paste mistake
6288                        // surfaces with line + column instead of
6289                        // producing an unexpectedly-augmented endpoint.
6290                        let lbrace_tok = self.consume(TokenType::LBrace)?;
6291                        let block_line = lbrace_tok.line;
6292                        if !node.query_params.is_empty() {
6293                            return Err(ParseError {
6294                                message: format!(
6295                                    "axonendpoint '{}' declares `query: {{ … }}` \
6296                                     more than once. The query-parameter block \
6297                                     is unique per endpoint; combine all params \
6298                                     into a single block (Fase 37.y D2).",
6299                                    node.name,
6300                                ),
6301                                line: lbrace_tok.line,
6302                                column: lbrace_tok.column,
6303                                ..Default::default()
6304                            });
6305                        }
6306                        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6307                            let name_tok = self.consume(TokenType::Identifier)?;
6308                            let field_name = name_tok.value.clone();
6309                            // Duplicate detection within the block.
6310                            if node
6311                                .query_params
6312                                .iter()
6313                                .any(|f| f.name == field_name)
6314                            {
6315                                return Err(ParseError {
6316                                    message: format!(
6317                                        "axonendpoint '{}' declares duplicate \
6318                                         query param '{}' inside `query: {{ … }}`. \
6319                                         Each name must appear at most once \
6320                                         (Fase 37.y D2).",
6321                                        node.name, field_name,
6322                                    ),
6323                                    line: name_tok.line,
6324                                    column: name_tok.column,
6325                                    ..Default::default()
6326                                });
6327                            }
6328                            self.consume(TokenType::Colon)?;
6329                            let type_expr = self.parse_type_expr()?;
6330                            // §Fase 37.y (D2 robustness) — reject generic
6331                            // type expressions on query params. The
6332                            // closed catalog is 5 primitives; container
6333                            // types (`Optional<T>`, `List<T>`, etc.)
6334                            // would mislead the adopter into thinking
6335                            // they bind multi-value query strings
6336                            // (deferred per plan vivo §7) or that
6337                            // `Optional<Text>` is the canonical way to
6338                            // declare an optional query (it's NOT —
6339                            // `Text?` is). Surface the canonical syntax
6340                            // verbatim so the fix is obvious.
6341                            if !type_expr.generic_param.is_empty() {
6342                                let canonical_hint = if type_expr.name == "Optional" {
6343                                    format!(
6344                                        " Use `{}?` (the `?` suffix) for an \
6345                                         optional query param instead of \
6346                                         `Optional<{}>`.",
6347                                        type_expr.generic_param,
6348                                        type_expr.generic_param,
6349                                    )
6350                                } else if type_expr.name == "List" {
6351                                    " Multi-value query params (e.g. `?tag=a&tag=b`) \
6352                                     are honest-deferred from v1.38.5; bind a \
6353                                     single-value `Text` query param and parse \
6354                                     the value inside the flow."
6355                                        .to_string()
6356                                } else {
6357                                    String::new()
6358                                };
6359                                return Err(ParseError {
6360                                    message: format!(
6361                                        "axonendpoint '{}' query param '{}' uses \
6362                                         a generic type `{}<{}>`. Query params \
6363                                         take a primitive type from the closed \
6364                                         catalog ({}); the `?` suffix marks \
6365                                         optional.{} (Fase 37.y D2).",
6366                                        node.name,
6367                                        field_name,
6368                                        type_expr.name,
6369                                        type_expr.generic_param,
6370                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | "),
6371                                        canonical_hint,
6372                                    ),
6373                                    line: type_expr.loc.line,
6374                                    column: type_expr.loc.column,
6375                                    ..Default::default()
6376                                });
6377                            }
6378                            // Validate against the closed catalog. A
6379                            // miss surfaces a Fase 28-style smart-suggest
6380                            // hint when within edit-distance 2.
6381                            if !axonendpoint_is_valid_query_param_type(&type_expr.name) {
6382                                // `smart_suggest::suggest_for` returns
6383                                // pre-formatted prose like
6384                                // "Did you mean `Text`?" or
6385                                // "Did you mean `Text` or `Int`?" (empty
6386                                // when no candidate within edit-distance
6387                                // 2). Concatenate without re-wrapping.
6388                                let hint = crate::smart_suggest::suggest_for(
6389                                    &type_expr.name,
6390                                    AXONENDPOINT_QUERY_PARAM_TYPES,
6391                                );
6392                                let hint_text = if hint.is_empty() {
6393                                    format!(
6394                                        " Expected one of: {}.",
6395                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
6396                                    )
6397                                } else {
6398                                    format!(
6399                                        " {} Expected one of: {}.",
6400                                        hint,
6401                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
6402                                    )
6403                                };
6404                                return Err(ParseError {
6405                                    message: format!(
6406                                        "axonendpoint '{}' query param '{}' has \
6407                                         unsupported type '{}'.{} (Fase 37.y D2).",
6408                                        node.name, field_name, type_expr.name,
6409                                        hint_text,
6410                                    ),
6411                                    line: type_expr.loc.line,
6412                                    column: type_expr.loc.column,
6413                                    ..Default::default()
6414                                });
6415                            }
6416                            node.query_params.push(TypeField {
6417                                name: field_name,
6418                                type_expr,
6419                                loc: Loc {
6420                                    line: name_tok.line,
6421                                    column: name_tok.column,
6422                                },
6423                            });
6424                            // Trailing comma is optional; the next loop
6425                            // iteration handles `}` cleanly. Accept both
6426                            // `name: Type, name: Type` AND `name: Type
6427                            // name: Type` (the existing parser style is
6428                            // forgiving about list separators).
6429                            if self.check(TokenType::Comma) {
6430                                self.advance();
6431                            }
6432                            let _ = block_line; // suppress unused warning
6433                        }
6434                        self.consume(TokenType::RBrace)?;
6435                    },
6436                    "execute" => node.execute_flow = self.consume_any_ident_or_kw()?.value.clone(),
6437                    "output" => {
6438                        // §Fase 38.x.f — promote axonendpoint `output:`
6439                        // parsing from a single token to the full
6440                        // generic-aware type expression (mirroring
6441                        // `parse_step` for FlowStep::Step which already
6442                        // uses `parse_output_type_string`).
6443                        //
6444                        // Pre-38.x.f: `output: List<Item>` captured only
6445                        // `"List"`, dropping `<Item>` (next tokens were
6446                        // either left unconsumed or absorbed by the
6447                        // following field). v1.39.0's narrow cardinality
6448                        // gate happened to fire correctly for `output: T`
6449                        // + retrieve-tail because the singular-detection
6450                        // path used `!starts_with("List<")` — but the
6451                        // SYMMETRIC `output: List<T>` + singular-tail
6452                        // case (38.x.f D3) needs the FULL `List<T>`
6453                        // shape captured; without it the gate sees
6454                        // `"List"` and misclassifies as Singular.
6455                        node.output_type = self.parse_output_type_string()?;
6456                    }
6457                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
6458                    "retries" => node.retries = self.parse_optional_int(),
6459                    "timeout" => {
6460                        let t = self.current().clone();
6461                        self.advance();
6462                        node.timeout = t.value.clone();
6463                    }
6464                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
6465                    "replay" => {
6466                        // §Fase 32.h (D9 plan-vivo) — Replay-token binding.
6467                        // Boolean `replay: true | false`. Default (when
6468                        // omitted) is method-derived at deploy-time:
6469                        // POST/PUT → true, GET/DELETE → false. Explicit
6470                        // declaration sets `replay_explicit = true` so
6471                        // the runtime knows NOT to override.
6472                        let value_tok = self.consume(TokenType::Bool)?;
6473                        node.replay = value_tok.value.eq_ignore_ascii_case("true");
6474                        node.replay_explicit = true;
6475                    }
6476                    "requires" => {
6477                        // §Fase 32.g (D8) — Auth scope per axonendpoint.
6478                        // Closed slug grammar
6479                        // `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$` enforced
6480                        // at parse time with smart-suggest-style hint.
6481                        // Empty list means "no auth gate" (D9 backwards-
6482                        // compat). Cross-stack with Python parser.
6483                        let bracket_tok = self.current().clone();
6484                        let items = self.parse_bracketed_dot_identifiers()?;
6485                        for slug in &items {
6486                            if !is_valid_capability_slug(slug) {
6487                                return Err(ParseError {
6488                                    message: format!(
6489                                        "Invalid capability slug '{slug}' in axonendpoint '{}' \
6490                                         `requires:`. Capability slugs must match \
6491                                         ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
6492                                         lowercase identifiers starting with a letter. Examples: \
6493                                         `admin`, `legal.read`, `hipaa.phi.read`.",
6494                                        node.name
6495                                    ),
6496                                    line: bracket_tok.line,
6497                                    column: bracket_tok.column,
6498                                    ..Default::default()
6499                                });
6500                            }
6501                        }
6502                        node.requires_capabilities = items;
6503                    }
6504                    // §Fase 30.b — HTTP transport enum (D2 closed) + keepalive (D6 closed).
6505                    // Mirrors `axon/compiler/parser.py` `_parse_axonendpoint`.
6506                    // Drift-gate corpus verifies byte-identical parse cross-stack.
6507                    "transport" => {
6508                        let value_tok = self.consume_any_ident_or_kw()?;
6509                        let value = &value_tok.value;
6510                        if !axonendpoint_is_valid_transport(value) {
6511                            let hint = crate::smart_suggest::suggest_for(
6512                                value,
6513                                AXONENDPOINT_TRANSPORT_VALUES,
6514                            );
6515                            let base = format!(
6516                                "Invalid transport '{}' in axonendpoint '{}'.",
6517                                value, node.name
6518                            );
6519                            let message = if hint.is_empty() {
6520                                format!("{base} expected json | sse | ndjson, found {value}")
6521                            } else {
6522                                format!(
6523                                    "{base} {hint} (expected json | sse | ndjson, found {value})"
6524                                )
6525                            };
6526                            return Err(ParseError {
6527                                message,
6528                                line: value_tok.line,
6529                                column: value_tok.column,
6530                                ..Default::default()
6531                            });
6532                        }
6533                        node.transport = value.clone();
6534                        // §Fase 31.b D1 — mark the field as explicitly
6535                        // declared so the type-checker's implicit-transport
6536                        // inference knows NOT to override this value with
6537                        // the produces_stream-driven inference.
6538                        node.transport_explicit = true;
6539                        // §Fase 33.z.k.b (v1.28.0) — Optional dialect
6540                        // parametrization: `transport: sse(<dialect>)`.
6541                        // Only valid when the base value is `sse`
6542                        // (json + ndjson dialects are the dialects
6543                        // themselves; `json(<x>)` / `ndjson(<x>)`
6544                        // would be parse errors caught below).
6545                        if self.check(TokenType::LParen) {
6546                            if value != "sse" {
6547                                let tok = self.current().clone();
6548                                return Err(ParseError {
6549                                    message: format!(
6550                                        "Dialect parametrization \
6551                                         `transport: {value}(<dialect>)` is \
6552                                         only valid for `sse`; got \
6553                                         `{value}` in axonendpoint '{}'.",
6554                                        node.name
6555                                    ),
6556                                    line: tok.line,
6557                                    column: tok.column,
6558                                    ..Default::default()
6559                                });
6560                            }
6561                            self.advance(); // consume LParen
6562                            let dialect_tok = self.consume_any_ident_or_kw()?;
6563                            let dialect = dialect_tok.value.clone();
6564                            if !AXONENDPOINT_TRANSPORT_DIALECTS
6565                                .iter()
6566                                .any(|&d| d == dialect)
6567                            {
6568                                let hint = crate::smart_suggest::suggest_for(
6569                                    &dialect,
6570                                    AXONENDPOINT_TRANSPORT_DIALECTS,
6571                                );
6572                                let base = format!(
6573                                    "Invalid SSE dialect '{dialect}' in axonendpoint '{}'.",
6574                                    node.name
6575                                );
6576                                let message = if hint.is_empty() {
6577                                    format!(
6578                                        "{base} expected axon | openai | kimi | glm | anthropic, found {dialect}"
6579                                    )
6580                                } else {
6581                                    format!(
6582                                        "{base} {hint} (expected axon | openai | kimi | glm | anthropic, found {dialect})"
6583                                    )
6584                                };
6585                                return Err(ParseError {
6586                                    message,
6587                                    line: dialect_tok.line,
6588                                    column: dialect_tok.column,
6589                                    ..Default::default()
6590                                });
6591                            }
6592                            // Closing RParen.
6593                            let rparen_tok = self.current().clone();
6594                            if !self.check(TokenType::RParen) {
6595                                return Err(ParseError {
6596                                    message: format!(
6597                                        "Expected `)` after dialect name \
6598                                         in axonendpoint '{}' \
6599                                         (transport: sse(<dialect>) grammar).",
6600                                        node.name
6601                                    ),
6602                                    line: rparen_tok.line,
6603                                    column: rparen_tok.column,
6604                                    ..Default::default()
6605                                });
6606                            }
6607                            self.advance(); // consume RParen
6608                            node.transport_dialect = dialect;
6609                        }
6610                    }
6611                    "keepalive" => {
6612                        // Accepts either a DURATION token (e.g. `15s`) or
6613                        // an ident-like token. Validation against the
6614                        // closed enum {5s, 15s, 30s, 60s} happens after.
6615                        let value_tok = self.current().clone();
6616                        self.advance();
6617                        let value = &value_tok.value;
6618                        if !axonendpoint_is_valid_keepalive(value) {
6619                            let hint = crate::smart_suggest::suggest_for(
6620                                value,
6621                                AXONENDPOINT_KEEPALIVE_VALUES,
6622                            );
6623                            let base = format!(
6624                                "Invalid keepalive '{}' in axonendpoint '{}'.",
6625                                value, node.name
6626                            );
6627                            let message = if hint.is_empty() {
6628                                format!("{base} expected 5s | 15s | 30s | 60s, found {value}")
6629                            } else {
6630                                format!(
6631                                    "{base} {hint} (expected 5s | 15s | 30s | 60s, found {value})"
6632                                )
6633                            };
6634                            return Err(ParseError {
6635                                message,
6636                                line: value_tok.line,
6637                                column: value_tok.column,
6638                                ..Default::default()
6639                            });
6640                        }
6641                        node.keepalive = value.clone();
6642                    }
6643                    "backend" => {
6644                        // §Fase 36.d (D2) — declared execution backend.
6645                        // Closed catalog `CANONICAL_PROVIDERS ∪ {auto,
6646                        // stub}`; an unknown name is a parse error with
6647                        // a smart-suggest hint (the same discipline as
6648                        // `method`/`transport`/`keepalive`). The
6649                        // type-checker re-validates defensively for
6650                        // ASTs built outside the parser (LSP, tests).
6651                        let value_tok = self.consume_any_ident_or_kw()?;
6652                        let value = &value_tok.value;
6653                        if !axonendpoint_is_valid_backend(value) {
6654                            let hint = crate::smart_suggest::suggest_for(
6655                                value,
6656                                AXONENDPOINT_BACKEND_VALUES,
6657                            );
6658                            let expected = AXONENDPOINT_BACKEND_VALUES.join(" | ");
6659                            let base = format!(
6660                                "Invalid backend '{}' in axonendpoint '{}'.",
6661                                value, node.name
6662                            );
6663                            let message = if hint.is_empty() {
6664                                format!("{base} expected {expected}, found {value}")
6665                            } else {
6666                                format!(
6667                                    "{base} {hint} (expected {expected}, found {value})"
6668                                )
6669                            };
6670                            return Err(ParseError {
6671                                message,
6672                                line: value_tok.line,
6673                                column: value_tok.column,
6674                                ..Default::default()
6675                            });
6676                        }
6677                        node.backend = value.clone();
6678                    }
6679                    _ => self.skip_value(),
6680                }
6681            } else if self.check(TokenType::LBrace) {
6682                self.skip_braced_block()?;
6683            }
6684        }
6685        self.consume(TokenType::RBrace)?;
6686        Ok(node)
6687    }
6688
6689    // ── Numeric helpers for Tier 2 field parsing ────────────────────
6690
6691    fn parse_optional_int(&mut self) -> Option<i64> {
6692        let tok = self.current().clone();
6693        match tok.ttype {
6694            TokenType::Integer => {
6695                self.advance();
6696                tok.value.parse::<i64>().ok()
6697            }
6698            _ => {
6699                self.advance();
6700                None
6701            }
6702        }
6703    }
6704
6705    fn parse_optional_float(&mut self) -> Option<f64> {
6706        let tok = self.current().clone();
6707        match tok.ttype {
6708            TokenType::Float | TokenType::Integer => {
6709                self.advance();
6710                tok.value.parse::<f64>().ok()
6711            }
6712            _ => {
6713                self.advance();
6714                None
6715            }
6716        }
6717    }
6718
6719    // ── LAMBDA DATA (ΛD) ──────────────────────────────────────────
6720
6721    fn parse_lambda_data(&mut self) -> Result<LambdaDataDefinition, ParseError> {
6722        let tok = self.consume(TokenType::Lambda)?;
6723        let name = self.consume(TokenType::Identifier)?;
6724        self.consume(TokenType::LBrace)?;
6725
6726        let mut node = LambdaDataDefinition {
6727            name: name.value.clone(),
6728            ontology: String::new(),
6729            certainty: 1.0,
6730            temporal_frame_start: String::new(),
6731            temporal_frame_end: String::new(),
6732            provenance: String::new(),
6733            derivation: String::new(),
6734            loc: Loc {
6735                line: tok.line,
6736                column: tok.column,
6737            },
6738            leading_trivia: Vec::new(),
6739            trailing_trivia: Vec::new(),
6740        };
6741
6742        while !self.check(TokenType::RBrace) {
6743            let field = self.current().clone();
6744            match field.ttype {
6745                TokenType::Ontology => {
6746                    self.advance();
6747                    self.consume(TokenType::Colon)?;
6748                    node.ontology = self.consume(TokenType::StringLit)?.value.clone();
6749                }
6750                TokenType::Certainty => {
6751                    self.advance();
6752                    self.consume(TokenType::Colon)?;
6753                    let val = self.current().clone();
6754                    match val.ttype {
6755                        TokenType::Float => {
6756                            self.advance();
6757                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
6758                        }
6759                        TokenType::Integer => {
6760                            self.advance();
6761                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
6762                        }
6763                        _ => {
6764                            return Err(ParseError {
6765                                message: format!(
6766                                    "Expected number for certainty, got '{}'",
6767                                    val.value
6768                                ),
6769                                line: val.line,
6770                                column: val.column,
6771                                                            ..Default::default()
6772                            });
6773                        }
6774                    }
6775                }
6776                TokenType::TemporalFrame => {
6777                    self.advance();
6778                    self.consume(TokenType::Colon)?;
6779                    node.temporal_frame_start = self.consume(TokenType::StringLit)?.value.clone();
6780                    // Optional second string for end frame
6781                    if self.check(TokenType::StringLit) {
6782                        node.temporal_frame_end = self.consume(TokenType::StringLit)?.value.clone();
6783                    }
6784                }
6785                TokenType::Provenance => {
6786                    self.advance();
6787                    self.consume(TokenType::Colon)?;
6788                    node.provenance = self.consume(TokenType::StringLit)?.value.clone();
6789                }
6790                TokenType::Derivation => {
6791                    self.advance();
6792                    self.consume(TokenType::Colon)?;
6793                    let d = self.current().clone();
6794                    self.advance();
6795                    node.derivation = d.value.clone();
6796                }
6797                _ => {
6798                    // Skip unknown fields gracefully
6799                    self.advance();
6800                    if self.check(TokenType::Colon) {
6801                        self.advance();
6802                        self.skip_value();
6803                    }
6804                }
6805            }
6806        }
6807
6808        self.consume(TokenType::RBrace)?;
6809        Ok(node)
6810    }
6811
6812    fn parse_lambda_data_apply(&mut self) -> Result<LambdaDataApplyNode, ParseError> {
6813        let tok = self.consume(TokenType::Lambda)?;
6814        let lambda_name = self.consume(TokenType::Identifier)?;
6815
6816        // Expect "on" keyword (parsed as identifier since it's not reserved)
6817        let on_tok = self.current().clone();
6818        self.advance();
6819        if on_tok.value != "on" {
6820            return Err(ParseError {
6821                message: format!(
6822                    "Expected 'on' after lambda data name in flow step, got '{}'",
6823                    on_tok.value
6824                ),
6825                line: on_tok.line,
6826                column: on_tok.column,
6827                            ..Default::default()
6828            });
6829        }
6830
6831        let target = self.current().clone();
6832        self.advance();
6833
6834        let mut output_type = String::new();
6835        if self.check(TokenType::Arrow) {
6836            self.advance();
6837            output_type = self.consume(TokenType::Identifier)?.value.clone();
6838        }
6839
6840        Ok(LambdaDataApplyNode {
6841            lambda_data_name: lambda_name.value.clone(),
6842            target: target.value.clone(),
6843            output_type,
6844            loc: Loc {
6845                line: tok.line,
6846                column: tok.column,
6847            },
6848        })
6849    }
6850
6851    // ── GENERIC (Tier 2+) ────────────────────────────────────────
6852
6853    fn parse_generic_declaration(&mut self) -> Result<Declaration, ParseError> {
6854        let kw_tok = self.current().clone();
6855        self.advance(); // consume keyword
6856
6857        // Try to consume a name (identifier or keyword-as-name)
6858        let name = if self.current().ttype == TokenType::Identifier {
6859            let n = self.current().value.clone();
6860            self.advance();
6861            n
6862        } else if !self.check(TokenType::LBrace)
6863            && !self.check(TokenType::LParen)
6864            && !self.check(TokenType::Eof)
6865            && self
6866                .current()
6867                .value
6868                .chars()
6869                .all(|c| c.is_alphanumeric() || c == '_')
6870        {
6871            let n = self.current().value.clone();
6872            self.advance();
6873            n
6874        } else {
6875            String::new()
6876        };
6877
6878        // Skip optional parens: (...)
6879        if self.check(TokenType::LParen) {
6880            self.advance();
6881            let mut depth = 1u32;
6882            while depth > 0 && !self.check(TokenType::Eof) {
6883                if self.check(TokenType::LParen) {
6884                    depth += 1;
6885                } else if self.check(TokenType::RParen) {
6886                    depth -= 1;
6887                }
6888                self.advance();
6889            }
6890        }
6891
6892        // Skip tokens until LBrace or next declaration
6893        while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
6894            if self.check(TokenType::Eof) {
6895                break;
6896            }
6897            self.advance();
6898        }
6899
6900        // Skip braced block if present
6901        if self.check(TokenType::LBrace) {
6902            self.skip_braced_block()?;
6903        }
6904
6905        Ok(Declaration::Generic(GenericDeclaration {
6906            keyword: kw_tok.value,
6907            name,
6908            loc: Loc {
6909                line: kw_tok.line,
6910                column: kw_tok.column,
6911            },
6912            leading_trivia: Vec::new(),
6913            trailing_trivia: Vec::new(),
6914        }))
6915    }
6916
6917    // ──────────────────────────────────────────────────────────────────
6918    //  §λ-L-E Fase 13 — Mobile Typed Channels parsers
6919    //  (paper_mobile_channels.md §3 + plan/fase_13)
6920    //  Direct port of axon/compiler/parser.py:_parse_channel/emit/publish/discover.
6921    // ──────────────────────────────────────────────────────────────────
6922
6923    /// Parse: `channel Name { message, qos, lifetime, persistence, shield }`.
6924    fn parse_channel(&mut self) -> Result<ChannelDefinition, ParseError> {
6925        let tok = self.consume(TokenType::Channel)?;
6926        let name = self.consume(TokenType::Identifier)?.value;
6927        let mut node = ChannelDefinition {
6928            name: name.clone(),
6929            message: String::new(),
6930            qos: "at_least_once".to_string(),
6931            lifetime: "affine".to_string(),
6932            persistence: "ephemeral".to_string(),
6933            shield_ref: String::new(),
6934            loc: Loc {
6935                line: tok.line,
6936                column: tok.column,
6937            },
6938            leading_trivia: Vec::new(),
6939            trailing_trivia: Vec::new(),
6940        };
6941        self.consume(TokenType::LBrace)?;
6942        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6943            let field_tok = self.current().clone();
6944            let field_name = field_tok.value.clone();
6945            self.advance();
6946            if !self.check(TokenType::Colon) {
6947                if self.check(TokenType::LBrace) {
6948                    self.skip_braced_block()?;
6949                }
6950                continue;
6951            }
6952            self.advance();
6953            match field_name.as_str() {
6954                "message" => node.message = self.parse_channel_message_type()?,
6955                "qos" => {
6956                    let q_tok = self.consume_any_ident_or_kw()?;
6957                    if !matches!(
6958                        q_tok.value.as_str(),
6959                        "at_most_once" | "at_least_once" | "exactly_once" | "broadcast" | "queue"
6960                    ) {
6961                        return Err(ParseError {
6962                            message: format!(
6963                                "Invalid qos '{}' in channel '{}' — \
6964                                 expected at_most_once | at_least_once | \
6965                                 exactly_once | broadcast | queue",
6966                                q_tok.value, name
6967                            ),
6968                            line: q_tok.line,
6969                            column: q_tok.column,
6970                                                    ..Default::default()
6971                        });
6972                    }
6973                    node.qos = q_tok.value;
6974                }
6975                "lifetime" => {
6976                    let lt_tok = self.consume_any_ident_or_kw()?;
6977                    if !matches!(lt_tok.value.as_str(), "linear" | "affine" | "persistent") {
6978                        return Err(ParseError {
6979                            message: format!(
6980                                "Invalid lifetime '{}' in channel '{}' — \
6981                                 expected linear | affine | persistent",
6982                                lt_tok.value, name
6983                            ),
6984                            line: lt_tok.line,
6985                            column: lt_tok.column,
6986                                                    ..Default::default()
6987                        });
6988                    }
6989                    node.lifetime = lt_tok.value;
6990                }
6991                "persistence" => {
6992                    let p_tok = self.consume_any_ident_or_kw()?;
6993                    if !matches!(p_tok.value.as_str(), "ephemeral" | "persistent_axonstore") {
6994                        return Err(ParseError {
6995                            message: format!(
6996                                "Invalid persistence '{}' in channel '{}' — \
6997                                 expected ephemeral | persistent_axonstore",
6998                                p_tok.value, name
6999                            ),
7000                            line: p_tok.line,
7001                            column: p_tok.column,
7002                                                    ..Default::default()
7003                        });
7004                    }
7005                    node.persistence = p_tok.value;
7006                }
7007                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
7008                _ => self.skip_value(),
7009            }
7010        }
7011        self.consume(TokenType::RBrace)?;
7012        Ok(node)
7013    }
7014
7015    /// Parse a `message:` value, supporting nested `Channel<…>`
7016    /// (second-order session types — paper §3.3).
7017    fn parse_channel_message_type(&mut self) -> Result<String, ParseError> {
7018        let head = self.consume(TokenType::Identifier)?;
7019        let mut spelling = head.value;
7020        if self.check(TokenType::Lt) {
7021            self.advance();
7022            let inner = self.parse_channel_message_type()?;
7023            self.consume(TokenType::Gt)?;
7024            spelling = format!("{}<{}>", spelling, inner);
7025        }
7026        Ok(spelling)
7027    }
7028
7029    /// Parse: `emit ChannelName(value_ref)` — Chan-Output / Chan-Mobility.
7030    ///
7031    /// `value_ref` accepts a bare identifier (variable / channel name for
7032    /// mobility) or a dotted path (`Step.output.field`) referencing a prior
7033    /// step result (Fase 13.i — runtime resolves via ContextManager).
7034    fn parse_emit_step(&mut self) -> Result<FlowStep, ParseError> {
7035        let tok = self.consume(TokenType::Emit)?;
7036        let channel = self.consume(TokenType::Identifier)?.value;
7037        self.consume(TokenType::LParen)?;
7038        let value = self.parse_emit_value_ref()?;
7039        self.consume(TokenType::RParen)?;
7040        Ok(FlowStep::Emit(EmitStatement {
7041            channel_ref: channel,
7042            value_ref: value,
7043            loc: Loc {
7044                line: tok.line,
7045                column: tok.column,
7046            },
7047        }))
7048    }
7049
7050    /// Parse: `IDENTIFIER ('.' (IDENTIFIER | keyword))*` → dot-joined string
7051    /// (Fase 13.i).
7052    ///
7053    /// Mirrors the Python `_parse_emit_value_ref` helper exactly so the IR
7054    /// JSON for `emit Hello(Build.output)` is byte-identical between the
7055    /// two reference implementations.
7056    ///
7057    /// The HEAD must be a real ``Identifier``. Subsequent segments after a
7058    /// `.` may be identifiers OR keywords — common field names like
7059    /// ``output``, ``result``, ``message``, ``state``, etc. are reserved
7060    /// words in Axon but adopters must be able to write them as
7061    /// dotted-access segments. The accepting predicate:
7062    ///   - the lexer carried a non-empty `value` (every Word-like token does)
7063    ///   - the value's first byte is a letter or underscore (filters out
7064    ///     punctuation tokens such as ',', '{', etc.)
7065    fn parse_emit_value_ref(&mut self) -> Result<String, ParseError> {
7066        let head = self.consume(TokenType::Identifier)?.value;
7067        let mut parts = vec![head];
7068        while self.check(TokenType::Dot) {
7069            self.advance(); // consume '.'
7070            let next_tok = self.current().clone();
7071            let valid = !next_tok.value.is_empty()
7072                && next_tok.value.as_bytes()[0].is_ascii_alphabetic()
7073                || next_tok.value.starts_with('_');
7074            if !valid {
7075                return Err(ParseError {
7076                    message: format!(
7077                        "Expected identifier or keyword after '.' in dotted \
7078                         access, found {:?}",
7079                        next_tok.value
7080                    ),
7081                    line: next_tok.line,
7082                    column: next_tok.column,
7083                                    ..Default::default()
7084                });
7085            }
7086            self.advance();
7087            parts.push(next_tok.value);
7088        }
7089        Ok(parts.join("."))
7090    }
7091
7092    /// Parse: `publish ChannelName within ShieldName` — Publish-Ext (D8).
7093    fn parse_publish_step(&mut self) -> Result<FlowStep, ParseError> {
7094        let tok = self.consume(TokenType::Publish)?;
7095        let channel = self.consume(TokenType::Identifier)?.value;
7096        self.consume(TokenType::Within)?;
7097        let shield = self.consume(TokenType::Identifier)?.value;
7098        Ok(FlowStep::Publish(PublishStatement {
7099            channel_ref: channel,
7100            shield_ref: shield,
7101            loc: Loc {
7102                line: tok.line,
7103                column: tok.column,
7104            },
7105        }))
7106    }
7107
7108    /// Parse: `discover ChannelName as alias` — dual of publish.
7109    fn parse_discover_step(&mut self) -> Result<FlowStep, ParseError> {
7110        let tok = self.consume(TokenType::Discover)?;
7111        let cap = self.consume(TokenType::Identifier)?.value;
7112        self.consume(TokenType::As)?;
7113        let alias = self.consume(TokenType::Identifier)?.value;
7114        Ok(FlowStep::Discover(DiscoverStatement {
7115            capability_ref: cap,
7116            alias,
7117            loc: Loc {
7118                line: tok.line,
7119                column: tok.column,
7120            },
7121        }))
7122    }
7123}
7124
7125// ── §λ-L-E Fase 13 — Mobile Typed Channels parser tests ─────────────────────
7126
7127#[cfg(test)]
7128mod fase13_parser_tests {
7129    use super::*;
7130    use crate::lexer::Lexer;
7131
7132    fn parse(src: &str) -> Result<Program, ParseError> {
7133        let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
7134        Parser::new(tokens).parse()
7135    }
7136
7137    #[test]
7138    fn channel_full_parses() {
7139        let src = r#"channel C { message: Order qos: at_least_once lifetime: affine persistence: ephemeral shield: Gate }"#;
7140        let prog = parse(src).expect("parse");
7141        match &prog.declarations[0] {
7142            Declaration::Channel(c) => {
7143                assert_eq!(c.name, "C");
7144                assert_eq!(c.message, "Order");
7145                assert_eq!(c.qos, "at_least_once");
7146                assert_eq!(c.lifetime, "affine");
7147                assert_eq!(c.persistence, "ephemeral");
7148                assert_eq!(c.shield_ref, "Gate");
7149            }
7150            _ => panic!("expected ChannelDefinition"),
7151        }
7152    }
7153
7154    #[test]
7155    fn channel_defaults_match_paper_d1() {
7156        let prog = parse("channel C { message: Order }").expect("parse");
7157        if let Declaration::Channel(c) = &prog.declarations[0] {
7158            assert_eq!(c.qos, "at_least_once"); // default
7159            assert_eq!(c.lifetime, "affine"); // D1 default
7160            assert_eq!(c.persistence, "ephemeral");
7161            assert_eq!(c.shield_ref, "");
7162        } else {
7163            panic!("expected ChannelDefinition");
7164        }
7165    }
7166
7167    #[test]
7168    fn channel_second_order_message_type_parses() {
7169        let prog = parse("channel C { message: Channel<Order> }").expect("parse");
7170        if let Declaration::Channel(c) = &prog.declarations[0] {
7171            assert_eq!(c.message, "Channel<Order>");
7172        } else {
7173            panic!("expected ChannelDefinition");
7174        }
7175    }
7176
7177    #[test]
7178    fn channel_nested_channel_message_type_parses() {
7179        let prog = parse("channel C { message: Channel<Channel<Order>> }").expect("parse");
7180        if let Declaration::Channel(c) = &prog.declarations[0] {
7181            assert_eq!(c.message, "Channel<Channel<Order>>");
7182        } else {
7183            panic!("expected ChannelDefinition");
7184        }
7185    }
7186
7187    #[test]
7188    fn channel_invalid_qos_rejected() {
7189        let err = parse("channel C { message: T qos: bogus }").unwrap_err();
7190        assert!(err.message.contains("Invalid qos"), "got {}", err.message);
7191    }
7192
7193    #[test]
7194    fn channel_invalid_lifetime_rejected() {
7195        let err = parse("channel C { message: T lifetime: eternal }").unwrap_err();
7196        assert!(
7197            err.message.contains("Invalid lifetime"),
7198            "got {}",
7199            err.message
7200        );
7201    }
7202
7203    #[test]
7204    fn channel_invalid_persistence_rejected() {
7205        let err = parse("channel C { message: T persistence: forever }").unwrap_err();
7206        assert!(
7207            err.message.contains("Invalid persistence"),
7208            "got {}",
7209            err.message
7210        );
7211    }
7212
7213    #[test]
7214    fn emit_value_parses() {
7215        let src = "flow f() -> Out { emit C(payload) }";
7216        let prog = parse(src).expect("parse");
7217        if let Declaration::Flow(f) = &prog.declarations[0] {
7218            match &f.body[0] {
7219                FlowStep::Emit(e) => {
7220                    assert_eq!(e.channel_ref, "C");
7221                    assert_eq!(e.value_ref, "payload");
7222                }
7223                other => panic!("expected Emit, got {:?}", other),
7224            }
7225        } else {
7226            panic!("expected Flow");
7227        }
7228    }
7229
7230    #[test]
7231    fn publish_within_shield_parses() {
7232        let src = "flow f() -> Cap { publish C within Gate }";
7233        let prog = parse(src).expect("parse");
7234        if let Declaration::Flow(f) = &prog.declarations[0] {
7235            match &f.body[0] {
7236                FlowStep::Publish(p) => {
7237                    assert_eq!(p.channel_ref, "C");
7238                    assert_eq!(p.shield_ref, "Gate");
7239                }
7240                other => panic!("expected Publish, got {:?}", other),
7241            }
7242        } else {
7243            panic!("expected Flow");
7244        }
7245    }
7246
7247    #[test]
7248    fn discover_with_alias_parses() {
7249        let src = "flow f() -> Out { discover C as ch }";
7250        let prog = parse(src).expect("parse");
7251        if let Declaration::Flow(f) = &prog.declarations[0] {
7252            match &f.body[0] {
7253                FlowStep::Discover(d) => {
7254                    assert_eq!(d.capability_ref, "C");
7255                    assert_eq!(d.alias, "ch");
7256                }
7257                other => panic!("expected Discover, got {:?}", other),
7258            }
7259        } else {
7260            panic!("expected Flow");
7261        }
7262    }
7263
7264    #[test]
7265    fn listen_typed_ref_sets_flag_true() {
7266        let src = "daemon D() { goal: \"x\" listen C as ev { } }";
7267        let prog = parse(src).expect("parse");
7268        if let Declaration::Daemon(d) = &prog.declarations[0] {
7269            assert_eq!(d.listeners.len(), 1);
7270            assert_eq!(d.listeners[0].channel, "C");
7271            assert!(d.listeners[0].channel_is_ref, "typed ref ⇒ true");
7272        } else {
7273            panic!("expected Daemon");
7274        }
7275    }
7276
7277    #[test]
7278    fn listen_string_topic_legacy_flag_false() {
7279        let src = "daemon D() { goal: \"x\" listen \"orders\" as ev { } }";
7280        let prog = parse(src).expect("parse");
7281        if let Declaration::Daemon(d) = &prog.declarations[0] {
7282            assert_eq!(d.listeners.len(), 1);
7283            assert_eq!(d.listeners[0].channel, "orders");
7284            assert!(!d.listeners[0].channel_is_ref, "string topic ⇒ false");
7285        } else {
7286            panic!("expected Daemon");
7287        }
7288    }
7289
7290    // ── Fase 13.i — emit value_ref accepts dotted access ───────────
7291
7292    fn extract_first_emit(prog: &Program) -> &EmitStatement {
7293        if let Declaration::Flow(f) = &prog.declarations[0] {
7294            if let FlowStep::Emit(e) = &f.body[0] {
7295                return e;
7296            }
7297        }
7298        panic!("expected emit statement at flow body[0]");
7299    }
7300
7301    #[test]
7302    fn emit_accepts_bare_identifier_value_ref() {
7303        // Pre-13.i baseline — must keep working.
7304        let prog = parse("flow f() -> Out { emit Hello(payload) }").expect("parse");
7305        let emit = extract_first_emit(&prog);
7306        assert_eq!(emit.channel_ref, "Hello");
7307        assert_eq!(emit.value_ref, "payload");
7308    }
7309
7310    #[test]
7311    fn emit_accepts_two_segment_dotted_value_ref() {
7312        // The exact case adopters reported as broken before 13.i.
7313        let prog = parse("flow f() -> Out { emit Hello(Build.output) }").expect("parse");
7314        let emit = extract_first_emit(&prog);
7315        assert_eq!(emit.value_ref, "Build.output");
7316    }
7317
7318    #[test]
7319    fn emit_accepts_three_segment_nested_dotted_value_ref() {
7320        let prog = parse("flow f() -> Out { emit Score(Analyze.result.score) }").expect("parse");
7321        let emit = extract_first_emit(&prog);
7322        assert_eq!(emit.value_ref, "Analyze.result.score");
7323    }
7324
7325    #[test]
7326    fn emit_dotted_with_trailing_dot_fails() {
7327        // Trailing `.` must still error — every '.' demands an identifier.
7328        let result = parse("flow f() -> Out { emit Hello(Build.) }");
7329        assert!(result.is_err(), "expected parse error for trailing dot");
7330    }
7331}
7332
7333// ── §Fase 14.a — declaration_trivia parallel channel tests ──────────────────
7334
7335#[cfg(test)]
7336mod fase14a_declaration_trivia_tests {
7337    use super::*;
7338    use crate::lexer::Lexer;
7339    use crate::tokens::TriviaKind;
7340
7341    fn parse(src: &str) -> Program {
7342        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
7343        Parser::new(toks).parse().expect("parse")
7344    }
7345
7346    #[test]
7347    fn no_comments_means_empty_trivia_per_decl() {
7348        let prog = parse("flow F() -> Out { }");
7349        assert_eq!(prog.declarations.len(), 1);
7350        assert_eq!(prog.declaration_trivia.len(), 1);
7351        assert!(prog.declaration_trivia[0].leading.is_empty());
7352        assert!(prog.declaration_trivia[0].trailing.is_empty());
7353    }
7354
7355    #[test]
7356    fn doc_line_comment_attaches_as_leading() {
7357        let prog = parse("/// Documents F\nflow F() -> Out { }");
7358        let triv = &prog.declaration_trivia[0];
7359        assert_eq!(triv.leading.len(), 1);
7360        assert_eq!(triv.leading[0].kind, TriviaKind::DocLine);
7361        assert!(triv.leading[0].is_doc());
7362        assert_eq!(triv.leading[0].text, "/// Documents F");
7363    }
7364
7365    #[test]
7366    fn regular_line_comment_attaches_as_leading() {
7367        let prog = parse("// header\nflow F() -> Out { }");
7368        let triv = &prog.declaration_trivia[0];
7369        assert_eq!(triv.leading.len(), 1);
7370        assert_eq!(triv.leading[0].kind, TriviaKind::Line);
7371        assert!(!triv.leading[0].is_doc());
7372    }
7373
7374    #[test]
7375    fn block_doc_comment_attaches_as_leading() {
7376        let prog = parse("/** Doc block */\nflow F() -> Out { }");
7377        let triv = &prog.declaration_trivia[0];
7378        assert_eq!(triv.leading[0].kind, TriviaKind::DocBlock);
7379        assert!(triv.leading[0].is_doc());
7380    }
7381
7382    #[test]
7383    fn multiple_comments_collected_in_source_order() {
7384        let src = "/// First\n/// Second\nflow F() -> Out { }";
7385        let prog = parse(src);
7386        let triv = &prog.declaration_trivia[0];
7387        assert_eq!(triv.leading.len(), 2);
7388        assert_eq!(triv.leading[0].text, "/// First");
7389        assert_eq!(triv.leading[1].text, "/// Second");
7390    }
7391
7392    #[test]
7393    fn three_decls_each_get_own_leading() {
7394        let src = "/// for A\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }\n/// for C\nflow C() -> Out { }";
7395        let prog = parse(src);
7396        assert_eq!(prog.declarations.len(), 3);
7397        assert_eq!(prog.declaration_trivia.len(), 3);
7398        for (idx, name) in ["A", "B", "C"].iter().enumerate() {
7399            let triv = &prog.declaration_trivia[idx];
7400            assert_eq!(triv.leading.len(), 1);
7401            assert_eq!(triv.leading[0].text, format!("/// for {name}"));
7402        }
7403    }
7404
7405    #[test]
7406    fn trailing_comment_attaches_to_last_token_of_decl() {
7407        // Comment on the same line as the decl's closing brace.
7408        let prog = parse("flow F() -> Out { } // tail");
7409        let triv = &prog.declaration_trivia[0];
7410        assert_eq!(triv.trailing.len(), 1);
7411        assert_eq!(triv.trailing[0].text, "// tail");
7412    }
7413
7414    #[test]
7415    fn mixed_doc_and_regular_preserve_order_between_decls() {
7416        let src = "/// doc for A\nflow A() -> Out { }\n\n// header line\n/// doc for B\nflow B() -> Out { }";
7417        let prog = parse(src);
7418        assert_eq!(prog.declarations.len(), 2);
7419        // A: just the doc comment.
7420        assert_eq!(prog.declaration_trivia[0].leading.len(), 1);
7421        // B: header + doc, in source order.
7422        assert_eq!(prog.declaration_trivia[1].leading.len(), 2);
7423        assert_eq!(prog.declaration_trivia[1].leading[0].text, "// header line");
7424        assert_eq!(prog.declaration_trivia[1].leading[1].text, "/// doc for B");
7425    }
7426
7427    #[test]
7428    fn parser_unaffected_by_comments_in_grammar_path() {
7429        // The parser must accept comments interleaved between every
7430        // legal token without affecting the AST shape it produces.
7431        // This is the regression guard for "lossless lexing must not
7432        // change parsing semantics."
7433        let src =
7434            "// before flow\nflow /* between flow and name */ F() -> Out {\n  // body comment\n}";
7435        let prog = parse(src);
7436        assert_eq!(prog.declarations.len(), 1);
7437        if let Declaration::Flow(f) = &prog.declarations[0] {
7438            assert_eq!(f.name, "F");
7439        } else {
7440            panic!("expected Flow declaration");
7441        }
7442    }
7443}
7444
7445// ── §Fase 14.b — per-struct trivia fields tests ─────────────────────────────
7446//
7447// 14.b spreads `leading_trivia` / `trailing_trivia` into every Declaration
7448// variant struct (FlowDefinition, ChannelDefinition, PersonaDefinition, …).
7449// The Python AST already had this shape since 14.a; 14.b achieves Rust
7450// parity. The side-channel `Program.declaration_trivia` is preserved for
7451// backward compat — these tests verify the new direct access path.
7452
7453#[cfg(test)]
7454mod fase14b_per_struct_trivia_tests {
7455    use super::*;
7456    use crate::lexer::Lexer;
7457    use crate::tokens::TriviaKind;
7458
7459    fn parse(src: &str) -> Program {
7460        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
7461        Parser::new(toks).parse().expect("parse")
7462    }
7463
7464    #[test]
7465    fn flow_definition_carries_leading_trivia_directly() {
7466        let prog = parse("/// documents F\nflow F() -> Out { }");
7467        if let Declaration::Flow(f) = &prog.declarations[0] {
7468            assert_eq!(f.leading_trivia.len(), 1);
7469            assert_eq!(f.leading_trivia[0].kind, TriviaKind::DocLine);
7470            assert_eq!(f.leading_trivia[0].text, "/// documents F");
7471            assert!(f.trailing_trivia.is_empty());
7472        } else {
7473            panic!("expected Flow declaration");
7474        }
7475    }
7476
7477    #[test]
7478    fn flow_definition_carries_trailing_trivia_directly() {
7479        let prog = parse("flow F() -> Out { } // tail comment");
7480        if let Declaration::Flow(f) = &prog.declarations[0] {
7481            assert_eq!(f.trailing_trivia.len(), 1);
7482            assert_eq!(f.trailing_trivia[0].text, "// tail comment");
7483        } else {
7484            panic!("expected Flow declaration");
7485        }
7486    }
7487
7488    #[test]
7489    fn channel_definition_carries_trivia_directly() {
7490        // ChannelDefinition is a Tier-1 declaration; verify per-struct fields
7491        // populate just like FlowDefinition.
7492        let src = concat!(
7493            "/// inbound order events\n",
7494            "channel Orders {\n",
7495            "    message:     Order\n",
7496            "    qos:         at_least_once\n",
7497            "    lifetime:    affine\n",
7498            "    persistence: ephemeral\n",
7499            "    shield:      Broker\n",
7500            "}",
7501        );
7502        let prog = parse(src);
7503        if let Declaration::Channel(ch) = &prog.declarations[0] {
7504            assert_eq!(ch.leading_trivia.len(), 1);
7505            assert!(ch.leading_trivia[0].is_doc());
7506            assert_eq!(ch.leading_trivia[0].text, "/// inbound order events");
7507        } else {
7508            panic!("expected Channel declaration");
7509        }
7510    }
7511
7512    #[test]
7513    fn per_struct_fields_match_side_channel() {
7514        // 14.a side-channel and 14.b per-struct fields must hold identical
7515        // data — they are populated by the same parser pass.
7516        let src = "/// for A\n// header for B\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }";
7517        let prog = parse(src);
7518        for (idx, decl) in prog.declarations.iter().enumerate() {
7519            let side = &prog.declaration_trivia[idx];
7520            let (per_lead, per_trail) = match decl {
7521                Declaration::Flow(f) => (&f.leading_trivia, &f.trailing_trivia),
7522                _ => panic!("unexpected variant"),
7523            };
7524            assert_eq!(per_lead.len(), side.leading.len());
7525            assert_eq!(per_trail.len(), side.trailing.len());
7526            for (a, b) in per_lead.iter().zip(side.leading.iter()) {
7527                assert_eq!(a.text, b.text);
7528                assert_eq!(a.kind, b.kind);
7529            }
7530        }
7531    }
7532
7533    #[test]
7534    fn comment_free_program_yields_empty_per_struct_fields() {
7535        let prog = parse("flow F() -> Out { }");
7536        if let Declaration::Flow(f) = &prog.declarations[0] {
7537            assert!(f.leading_trivia.is_empty());
7538            assert!(f.trailing_trivia.is_empty());
7539        } else {
7540            panic!("expected Flow declaration");
7541        }
7542    }
7543}
7544
7545// ── §Fase 14.c — inner doc comments (//!, /*!) ──────────────────────────────
7546//
7547// Inner doc comments document the *enclosing* item rather than the next
7548// sibling. Today they flow through the trivia channel like any other
7549// comment; downstream consumers (axon doc, LSP) decide how to interpret
7550// `is_inner_doc()`. These tests verify the lexer→parser pipeline preserves
7551// the inner-doc discriminator end-to-end.
7552
7553#[cfg(test)]
7554mod fase14c_inner_doc_tests {
7555    use super::*;
7556    use crate::lexer::Lexer;
7557    use crate::tokens::TriviaKind;
7558
7559    fn parse(src: &str) -> Program {
7560        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
7561        Parser::new(toks).parse().expect("parse")
7562    }
7563
7564    #[test]
7565    fn inner_doc_line_reaches_leading_trivia() {
7566        let src = "//! file-level docs\nflow F() -> Out { }";
7567        let prog = parse(src);
7568        let triv = &prog.declaration_trivia[0];
7569        assert_eq!(triv.leading.len(), 1);
7570        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocLine);
7571        assert!(triv.leading[0].is_doc());
7572        assert!(triv.leading[0].is_inner_doc());
7573        assert_eq!(triv.leading[0].text, "//! file-level docs");
7574        assert_eq!(triv.leading[0].stripped_text(), " file-level docs");
7575    }
7576
7577    #[test]
7578    fn inner_doc_block_reaches_leading_trivia() {
7579        let src = "/*! module-level docs */\nflow F() -> Out { }";
7580        let prog = parse(src);
7581        let triv = &prog.declaration_trivia[0];
7582        assert_eq!(triv.leading.len(), 1);
7583        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocBlock);
7584        assert!(triv.leading[0].is_inner_doc());
7585        assert_eq!(triv.leading[0].stripped_text(), " module-level docs ");
7586    }
7587
7588    #[test]
7589    fn outer_and_inner_doc_can_coexist() {
7590        // File-level inner doc on top, then an outer doc for the
7591        // declaration. Both reach the trivia channel and remain
7592        // distinguishable via `is_inner_doc()`.
7593        let src = "//! file docs\n/// docs F\nflow F() -> Out { }";
7594        let prog = parse(src);
7595        let triv = &prog.declaration_trivia[0];
7596        assert_eq!(triv.leading.len(), 2);
7597        assert!(triv.leading[0].is_inner_doc());
7598        assert!(triv.leading[1].is_doc());
7599        assert!(!triv.leading[1].is_inner_doc());
7600    }
7601
7602    #[test]
7603    fn inner_doc_reaches_per_struct_fields() {
7604        // Same data must be visible via the per-struct fields (Fase 14.b).
7605        let src = "//! intro\nflow F() -> Out { }";
7606        let prog = parse(src);
7607        if let Declaration::Flow(f) = &prog.declarations[0] {
7608            assert_eq!(f.leading_trivia.len(), 1);
7609            assert!(f.leading_trivia[0].is_inner_doc());
7610        } else {
7611            panic!("expected Flow declaration");
7612        }
7613    }
7614}
7615
7616// ── §Fase 28.c — Parser error recovery test pack ─────────────────────────────
7617//
7618// Mirror of `tests/test_fase28_parser_recovery.py` (Python side, 28.b).
7619// The test classes here line up 1-1 with the Python ones so the cross-
7620// stack drift gate (28.i) can compare error-list shapes input-for-input.
7621//
7622// Test classes:
7623//   - backwards_compat: existing `parse()` API unchanged
7624//   - single_error_recovery: one bad decl → one error, rest parse OK
7625//   - multi_error_recovery: N independent errors → N entries
7626//   - sync_points: every top-level keyword resyncs correctly
7627//   - parse_result_api: `has_errors`, `is_clean`
7628//   - edge_cases: EOF mid-error, brace imbalance, only-bad-tokens
7629//   - robustness_fuzz: 1000 deterministic-seeded mutations never crash
7630//   - no_ghost_errors: single broken field produces exactly 1 error
7631//   - integration_with_colon_diagnostic: v1.19.4 hint preserved under
7632//     recovery mode
7633#[cfg(test)]
7634mod fase28_recovery_tests {
7635    use super::*;
7636    use crate::lexer::Lexer;
7637
7638    /// Lex a source and return tokens for the parser to consume.
7639    /// Mirrors the Python `_parse_recovery` helper.
7640    fn lex(src: &str) -> Vec<Token> {
7641        Lexer::new(src, "<test>").tokenize().expect("lex")
7642    }
7643
7644    /// Parse with recovery mode. Returns `(program, errors)` so call
7645    /// sites read like the Python helper.
7646    fn recover(src: &str) -> ParseResult {
7647        Parser::new(lex(src)).parse_with_recovery()
7648    }
7649
7650    /// Strict parse. Mirrors the Python `_parse_strict` helper.
7651    fn strict(src: &str) -> Result<Program, ParseError> {
7652        Parser::new(lex(src)).parse()
7653    }
7654
7655    // ── backwards_compat ─────────────────────────────────────────
7656
7657    #[test]
7658    fn strict_parse_unchanged_for_clean_source() {
7659        // The existing `parse()` API must continue to succeed
7660        // verbatim on every well-formed input — D9.
7661        let src = "intent I {}";
7662        let prog = strict(src).expect("clean parse");
7663        assert_eq!(prog.declarations.len(), 1);
7664    }
7665
7666    #[test]
7667    fn strict_parse_still_raises_on_first_error() {
7668        // D9 + D8: opt-in to recovery via `parse_with_recovery`;
7669        // strict mode must still bubble the first error.
7670        // (Using a parse-time error rather than a lex error — `@@@`
7671        // would be rejected by the lexer, which is out of scope.)
7672        let src = "flow F() { } not_a_keyword flow G() { }";
7673        let _ = strict(src).expect_err("must error fast in strict mode");
7674    }
7675
7676    #[test]
7677    fn recovery_clean_source_yields_no_errors() {
7678        let src = "flow F() { } flow G() { }";
7679        let pr = recover(src);
7680        assert!(pr.is_clean(), "errors: {:?}", pr.errors);
7681        assert_eq!(pr.program.declarations.len(), 2);
7682    }
7683
7684    // ── single_error_recovery ────────────────────────────────────
7685
7686    #[test]
7687    fn single_unknown_top_level_token_recovers() {
7688        // One garbage token at top level; rest must parse.
7689        let src = "garbage_token flow F() { } flow G() { }";
7690        let pr = recover(src);
7691        assert_eq!(pr.errors.len(), 1, "errors: {:?}", pr.errors);
7692        assert_eq!(pr.program.declarations.len(), 2);
7693    }
7694
7695    #[test]
7696    fn error_in_first_decl_does_not_block_second() {
7697        // `flow F` body refers to non-keyword `nope`; the error
7698        // recovery must skip to the next top-level keyword.
7699        let src = "flow F() { not_a_step nope } flow G() { }";
7700        let pr = recover(src);
7701        assert!(pr.has_errors(), "expected at least one error");
7702        // The second flow must be reachable.
7703        let names: Vec<&str> = pr
7704            .program
7705            .declarations
7706            .iter()
7707            .filter_map(|d| match d {
7708                Declaration::Flow(f) => Some(f.name.as_str()),
7709                _ => None,
7710            })
7711            .collect();
7712        assert!(names.contains(&"G"), "G not found among {names:?}");
7713    }
7714
7715    #[test]
7716    fn malformed_declaration_then_clean_intent_recovers() {
7717        let src = "flow @ () { } intent I {}";
7718        let pr = recover(src);
7719        assert!(pr.has_errors());
7720        let kinds: Vec<&str> = pr
7721            .program
7722            .declarations
7723            .iter()
7724            .map(|d| match d {
7725                Declaration::Intent(_) => "intent",
7726                Declaration::Flow(_) => "flow",
7727                _ => "other",
7728            })
7729            .collect();
7730        assert!(kinds.contains(&"intent"), "kinds: {kinds:?}");
7731    }
7732
7733    #[test]
7734    fn recovery_does_not_double_count_a_single_error() {
7735        // Regression for the "ghost error" pathology that surfaced
7736        // during 28.b dev: a nested-decl error must not also fire
7737        // an "Unexpected token at top level" from the outer loop.
7738        // The Rust grammar has stricter intra-flow requirements
7739        // than Python; the invariant we assert here is that the
7740        // outer loop emits zero "Unexpected token at top level"
7741        // errors after an inner step-shape error.
7742        let src = "flow F() { not_a_step }";
7743        let pr = recover(src);
7744        let outer_ghosts = pr
7745            .errors
7746            .iter()
7747            .filter(|e| e.message.contains("at top level"))
7748            .count();
7749        assert_eq!(outer_ghosts, 0, "ghost errors: {:?}", pr.errors);
7750    }
7751
7752    // ── multi_error_recovery ─────────────────────────────────────
7753
7754    #[test]
7755    fn three_independent_errors_yield_three_entries() {
7756        let src =
7757            "garbage1 flow F() { } garbage2 flow G() { } garbage3 flow H() { }";
7758        let pr = recover(src);
7759        assert_eq!(pr.errors.len(), 3, "errors: {:?}", pr.errors);
7760        assert_eq!(pr.program.declarations.len(), 3);
7761    }
7762
7763    #[test]
7764    fn all_errors_no_valid_declarations() {
7765        let src = "foo bar baz qux";
7766        let pr = recover(src);
7767        assert!(pr.has_errors());
7768        assert!(pr.program.declarations.is_empty());
7769    }
7770
7771    #[test]
7772    fn errors_recorded_in_source_order() {
7773        let src = "x flow A() { } y flow B() { } z flow C() { }";
7774        let pr = recover(src);
7775        assert_eq!(pr.errors.len(), 3);
7776        let lines: Vec<u32> = pr.errors.iter().map(|e| e.line).collect();
7777        // Same source-line means we compare by column ordering;
7778        // either way they must be non-decreasing.
7779        assert!(
7780            lines.windows(2).all(|w| w[0] <= w[1]),
7781            "errors out of order: {lines:?}"
7782        );
7783    }
7784
7785    // ── sync_points ──────────────────────────────────────────────
7786
7787    #[test]
7788    fn sync_to_flow_keyword() {
7789        let src = "garbage flow F() { }";
7790        let pr = recover(src);
7791        assert_eq!(pr.program.declarations.len(), 1);
7792    }
7793
7794    #[test]
7795    fn sync_to_intent_keyword() {
7796        let src = "garbage intent I {}";
7797        let pr = recover(src);
7798        assert_eq!(pr.program.declarations.len(), 1);
7799    }
7800
7801    #[test]
7802    fn sync_to_persona_keyword() {
7803        let src = "garbage persona P { name: \"P\" role: \"R\" }";
7804        let pr = recover(src);
7805        assert!(
7806            pr.program
7807                .declarations
7808                .iter()
7809                .any(|d| matches!(d, Declaration::Persona(_))),
7810            "persona not recovered: decls = {:?}",
7811            pr.program.declarations.len()
7812        );
7813    }
7814
7815    #[test]
7816    fn sync_to_run_keyword() {
7817        let src = "garbage run R { input: { user_message: \"hi\" } }";
7818        let pr = recover(src);
7819        // Either Run was parsed, or recovery still produced ≥1 err.
7820        assert!(pr.has_errors());
7821    }
7822
7823    // ── parse_result_api ─────────────────────────────────────────
7824
7825    #[test]
7826    fn parse_result_has_errors_and_is_clean_invert() {
7827        let pr_clean = recover("flow F() { }");
7828        assert!(pr_clean.is_clean());
7829        assert!(!pr_clean.has_errors());
7830
7831        let pr_err = recover("garbage");
7832        assert!(!pr_err.is_clean());
7833        assert!(pr_err.has_errors());
7834    }
7835
7836    #[test]
7837    fn parse_result_program_field_holds_partial_program() {
7838        let pr = recover("garbage flow F() { }");
7839        assert!(!pr.program.declarations.is_empty());
7840    }
7841
7842    #[test]
7843    fn parse_result_errors_carry_line_and_column() {
7844        let pr = recover("garbage");
7845        assert!(!pr.errors.is_empty());
7846        let e = &pr.errors[0];
7847        assert!(e.line >= 1);
7848        // Column may be 0-based or 1-based depending on lexer;
7849        // accept anything ≥ 0.
7850        let _ = e.column;
7851        assert!(!e.message.is_empty());
7852    }
7853
7854    #[test]
7855    fn parse_result_debug_renders() {
7856        let pr = recover("flow F() { }");
7857        let s = format!("{pr:?}");
7858        assert!(s.contains("ParseResult"));
7859    }
7860
7861    // ── edge_cases ───────────────────────────────────────────────
7862
7863    #[test]
7864    fn empty_source_is_clean() {
7865        let pr = recover("");
7866        assert!(pr.is_clean());
7867        assert!(pr.program.declarations.is_empty());
7868    }
7869
7870    #[test]
7871    fn whitespace_only_source_is_clean() {
7872        let pr = recover("   \n\n\t  \n");
7873        assert!(pr.is_clean());
7874        assert!(pr.program.declarations.is_empty());
7875    }
7876
7877    #[test]
7878    fn only_garbage_does_not_crash() {
7879        // Lex-clean garbage tokens (avoids AxonLexerError).
7880        let pr = recover("foo bar baz { qux quux } corge { grault }");
7881        assert!(pr.has_errors());
7882    }
7883
7884    #[test]
7885    fn unbalanced_close_brace_does_not_crash() {
7886        let pr = recover("} flow F() { }");
7887        // Recovery must keep walking past stray `}`.
7888        let names: Vec<&str> = pr
7889            .program
7890            .declarations
7891            .iter()
7892            .filter_map(|d| match d {
7893                Declaration::Flow(f) => Some(f.name.as_str()),
7894                _ => None,
7895            })
7896            .collect();
7897        assert!(names.contains(&"F"), "F not recovered: {names:?}");
7898    }
7899
7900    #[test]
7901    fn error_at_eof_does_not_loop() {
7902        // Truncated declaration. Must terminate; finite errors.
7903        let pr = recover("flow F() { ");
7904        // Either errored or somehow accepted — but must terminate.
7905        let _ = pr.errors.len();
7906    }
7907
7908    #[test]
7909    fn nested_braces_inside_error_still_balance() {
7910        // Walker must respect brace depth so a `}` inside a malformed
7911        // block does not prematurely sync.
7912        let src = "flow F() { not_a_step { inner } } flow G() { }";
7913        let pr = recover(src);
7914        let names: Vec<&str> = pr
7915            .program
7916            .declarations
7917            .iter()
7918            .filter_map(|d| match d {
7919                Declaration::Flow(f) => Some(f.name.as_str()),
7920                _ => None,
7921            })
7922            .collect();
7923        assert!(names.contains(&"G"), "G not recovered: {names:?}");
7924    }
7925
7926    // ── robustness_fuzz ──────────────────────────────────────────
7927    //
7928    // Deterministic-seeded mutator (xorshift). 100 buckets ×
7929    // 10 mutations = 1000 iterations, byte-bounded so fuzz time
7930    // stays under 1 s on a release build. Recovery must NEVER crash;
7931    // lexer-level errors are out of scope (lexer recovery is its own
7932    // sub-fase). 28.b mirrors this with the same structure.
7933
7934    #[derive(Clone, Copy)]
7935    struct Xorshift(u64);
7936    impl Xorshift {
7937        fn next(&mut self) -> u64 {
7938            let mut x = self.0;
7939            x ^= x << 13;
7940            x ^= x >> 7;
7941            x ^= x << 17;
7942            self.0 = x;
7943            x
7944        }
7945        fn pick<T: Copy>(&mut self, slice: &[T]) -> T {
7946            slice[(self.next() as usize) % slice.len()]
7947        }
7948    }
7949
7950    fn mutate(src: &str, rng: &mut Xorshift) -> String {
7951        let mut bytes: Vec<u8> = src.bytes().collect();
7952        if bytes.is_empty() {
7953            return src.to_string();
7954        }
7955        let op = rng.next() % 4;
7956        let pos = (rng.next() as usize) % bytes.len();
7957        // Stick to ASCII-safe printable bytes to keep input lex-able
7958        // most of the time. AxonLexerError is still possible and is
7959        // tolerated by the recovery contract.
7960        let safe: &[u8] = b"abcdefghijklmnopqrstuvwxyz {}();:,_0123456789";
7961        match op {
7962            0 => {
7963                bytes.remove(pos);
7964            }
7965            1 => {
7966                let b = rng.pick(safe);
7967                bytes.insert(pos, b);
7968            }
7969            2 if pos + 1 < bytes.len() => {
7970                bytes.swap(pos, pos + 1);
7971            }
7972            _ => {
7973                let b = rng.pick(safe);
7974                bytes[pos] = b;
7975            }
7976        }
7977        // Lossy decode: mutator may have produced invalid UTF-8;
7978        // strip non-ASCII before handing to the lexer.
7979        bytes.retain(|b| b.is_ascii());
7980        String::from_utf8_lossy(&bytes).into_owned()
7981    }
7982
7983    #[test]
7984    fn fuzz_recovery_never_crashes() {
7985        let seed_bases = [
7986            "flow F() { }",
7987            "intent I { }",
7988            "persona P { name: \"P\" role: \"R\" }",
7989            "intent J { ask: \"a\" }",
7990            "type T = String",
7991        ];
7992        // 100 buckets × 10 mutations = 1000 iterations, deterministic.
7993        for (bucket, base) in (0..100u64).zip(seed_bases.iter().cycle()) {
7994            let mut rng = Xorshift(0x1234_5678_9abc_def0_u64.wrapping_add(bucket));
7995            let mut current = (*base).to_string();
7996            for _ in 0..10 {
7997                current = mutate(&current, &mut rng);
7998                // Lexer may reject; that's outside parser-recovery
7999                // scope (28.b/c). Skip those iterations.
8000                let toks = match Lexer::new(&current, "<fuzz>").tokenize() {
8001                    Ok(t) => t,
8002                    Err(_) => continue,
8003                };
8004                // Recovery must not panic on any well-lexed input.
8005                let _pr = Parser::new(toks).parse_with_recovery();
8006            }
8007        }
8008    }
8009
8010    // ── integration_with_v1_19_4_colon_diagnostic ────────────────
8011
8012    #[test]
8013    fn missing_colon_hint_preserved_under_recovery() {
8014        // The Rust frontend's strict `parse()` carries the same
8015        // colon diagnostic shape as the Python side. Recovery mode
8016        // must not erase it.
8017        let src = "flow F() { run R { input { user_message: \"hi\" } } }";
8018        let pr = recover(src);
8019        // Either the parser accepts this (some shape may be valid)
8020        // or it errors — but if it errors, the message must surface
8021        // the diagnostic content.
8022        if !pr.errors.is_empty() {
8023            let any_msg = pr.errors.iter().any(|e| !e.message.is_empty());
8024            assert!(any_msg);
8025        }
8026    }
8027
8028    // ── recovery preserves declaration ordering ──────────────────
8029
8030    #[test]
8031    fn recovered_declarations_appear_in_source_order() {
8032        let src = "flow A() { } garbage flow B() { } garbage flow C() { }";
8033        let pr = recover(src);
8034        let names: Vec<&str> = pr
8035            .program
8036            .declarations
8037            .iter()
8038            .filter_map(|d| match d {
8039                Declaration::Flow(f) => Some(f.name.as_str()),
8040                _ => None,
8041            })
8042            .collect();
8043        assert_eq!(names, vec!["A", "B", "C"]);
8044    }
8045}
8046
8047// ── §Fase 28.d — Source-context diagnostic block test pack ───────────────────
8048//
8049// Mirror of `tests/test_fase28_source_context.py` (Python side, 28.d).
8050// The render output must be byte-identical to the Python `SourceSnippet.render`
8051// on the same input — D7 ratified (cross-stack drift gate). Golden strings
8052// in `golden_*` tests are duplicated verbatim in the Python pack; edits
8053// here MUST be mirrored on the Python side and vice versa.
8054#[cfg(test)]
8055mod fase28_source_context_tests {
8056    use super::*;
8057    use crate::lexer::Lexer;
8058
8059    fn snippet(source: &str, line: u32, column: u32, filename: &str) -> String {
8060        SourceSnippet::new(
8061            source.to_string(),
8062            line,
8063            column,
8064            filename.to_string(),
8065        )
8066        .render()
8067    }
8068
8069    // ── Pure rendering ──────────────────────────────────────────
8070
8071    #[test]
8072    fn rustc_style_block_for_middle_line() {
8073        let src = "line one\nline two\nline three\nline four\nline five";
8074        let out = snippet(src, 3, 6, "x.axon");
8075        assert!(out.contains("--> x.axon:3:6"));
8076        assert!(out.contains("1 | line one"));
8077        assert!(out.contains("2 | line two"));
8078        assert!(out.contains("3 | line three"));
8079        assert!(out.contains("4 | line four"));
8080        assert!(out.contains("5 | line five"));
8081        // Caret col 6 → 5-space pad. Empty gutter is 1 space (gutter=1).
8082        assert!(out.contains("\n  |      ^"), "out:\n{out}");
8083    }
8084
8085    #[test]
8086    fn caret_column_one_renders_correctly() {
8087        let out = snippet("abc\n", 1, 1, "<source>");
8088        assert!(out.contains("\n  | ^"));
8089    }
8090
8091    #[test]
8092    fn first_line_clamps_context_before_to_zero() {
8093        let src = "first\nsecond\nthird\nfourth\nfifth";
8094        let out = snippet(src, 1, 1, "<source>");
8095        assert!(out.contains("1 | first"));
8096        assert!(out.contains("2 | second"));
8097        assert!(out.contains("3 | third"));
8098        assert!(!out.contains("4 | fourth"));
8099    }
8100
8101    #[test]
8102    fn last_line_clamps_context_after_to_eof() {
8103        let src = "first\nsecond\nthird\nfourth\nfifth";
8104        let out = snippet(src, 5, 2, "<source>");
8105        assert!(out.contains("5 | fifth"));
8106        assert!(out.contains("3 | third"));
8107        assert!(out.contains("4 | fourth"));
8108        assert!(!out.contains("2 | second"));
8109    }
8110
8111    #[test]
8112    fn gutter_width_grows_with_line_count() {
8113        let src: String = (1..=12).map(|i| format!("line{i}")).collect::<Vec<_>>().join("\n");
8114        let out = snippet(&src, 12, 1, "<source>");
8115        assert!(out.contains("12 | line12"));
8116        assert!(out.contains("10 | line10"));
8117    }
8118
8119    // ── Edge cases ──────────────────────────────────────────────
8120
8121    #[test]
8122    fn empty_source_returns_empty() {
8123        assert_eq!(snippet("", 1, 1, "<source>"), "");
8124    }
8125
8126    #[test]
8127    fn zero_line_returns_empty() {
8128        assert_eq!(snippet("hi", 0, 1, "<source>"), "");
8129    }
8130
8131    #[test]
8132    fn out_of_range_line_returns_empty() {
8133        assert_eq!(snippet("hi", 99, 1, "<source>"), "");
8134    }
8135
8136    #[test]
8137    fn caret_clamps_past_eol() {
8138        let out = snippet("hello", 1, 50, "<source>");
8139        assert!(out.contains("\n  |      ^"), "out:\n{out}");
8140    }
8141
8142    #[test]
8143    fn unicode_codepoint_count_for_caret_clamp() {
8144        // "héllo" = 5 codepoints; column past EOL clamps to 6.
8145        let out = snippet("héllo", 1, 99, "<source>");
8146        assert!(out.contains("\n  |      ^"), "out:\n{out}");
8147    }
8148
8149    #[test]
8150    fn trailing_newline_does_not_create_phantom_last_line() {
8151        let out = snippet("first\nsecond\n", 2, 1, "<source>");
8152        assert!(!out.contains("3 |"));
8153        assert!(out.contains("2 | second"));
8154    }
8155
8156    // ── Parser attach plumbing ──────────────────────────────────
8157
8158    fn lex(src: &str) -> Vec<Token> {
8159        Lexer::new(src, "<test>").tokenize().expect("lex")
8160    }
8161
8162    #[test]
8163    fn strict_parse_attaches_snippet_when_source_given() {
8164        let src = "garbage_token\nflow F() { }";
8165        let err = Parser::new(lex(src))
8166            .with_source(src, "x.axon")
8167            .parse()
8168            .expect_err("must error");
8169        assert!(err.source_snippet.is_some());
8170        let display = format!("{err}");
8171        assert!(display.contains("--> x.axon:"), "display: {display}");
8172    }
8173
8174    #[test]
8175    fn strict_parse_no_snippet_when_no_source() {
8176        let src = "garbage_token";
8177        let err = Parser::new(lex(src)).parse().expect_err("must error");
8178        assert!(err.source_snippet.is_none());
8179        let display = format!("{err}");
8180        assert!(!display.contains("\n  -->"));
8181    }
8182
8183    #[test]
8184    fn every_recovered_error_has_snippet() {
8185        let src = "garbage1\nflow F() { }\ngarbage2\nflow G() { }";
8186        let result = Parser::new(lex(src))
8187            .with_source(src, "multi.axon")
8188            .parse_with_recovery();
8189        assert!(!result.errors.is_empty());
8190        for err in &result.errors {
8191            assert!(err.source_snippet.is_some());
8192            let display = format!("{err}");
8193            assert!(
8194                display.contains("--> multi.axon:"),
8195                "display: {display}"
8196            );
8197        }
8198    }
8199
8200    #[test]
8201    fn recovery_no_snippet_when_no_source() {
8202        let src = "garbage1 garbage2";
8203        let result = Parser::new(lex(src)).parse_with_recovery();
8204        for err in &result.errors {
8205            assert!(err.source_snippet.is_none());
8206        }
8207    }
8208
8209    #[test]
8210    fn snippet_points_at_correct_line_for_each_error() {
8211        let src = "garbage_a\nflow F() { }\ngarbage_b\nflow G() { }";
8212        let result = Parser::new(lex(src))
8213            .with_source(src, "x")
8214            .parse_with_recovery();
8215        for err in &result.errors {
8216            let sn = err.source_snippet.as_ref().expect("snippet");
8217            assert_eq!(sn.line, err.line);
8218        }
8219    }
8220
8221    // ── Backwards-compat ────────────────────────────────────────
8222
8223    #[test]
8224    fn legacy_constructor_still_works() {
8225        let src = "flow F() { }";
8226        let prog = Parser::new(lex(src)).parse().expect("clean");
8227        assert_eq!(prog.declarations.len(), 1);
8228    }
8229
8230    #[test]
8231    fn attach_source_idempotent() {
8232        let err = ParseError {
8233            message: "bad".to_string(),
8234            line: 2,
8235            column: 3,
8236            ..Default::default()
8237        };
8238        let err2 = err.clone().attach_source("a\nb\nc\n", "f.axon");
8239        let first = format!("{err2}");
8240        let err3 = err.attach_source("a\nb\nc\n", "f.axon");
8241        let second = format!("{err3}");
8242        assert_eq!(first, second);
8243    }
8244
8245    #[test]
8246    fn attach_source_noop_when_line_zero() {
8247        let err = ParseError {
8248            message: "bad".to_string(),
8249            line: 0,
8250            column: 0,
8251            ..Default::default()
8252        };
8253        let err = err.attach_source("a\nb\nc\n", "f.axon");
8254        assert!(err.source_snippet.is_none());
8255    }
8256
8257    // ── Cross-stack golden parity ───────────────────────────────
8258    // These golden strings are duplicated verbatim in the Python
8259    // test pack at `tests/test_fase28_source_context.py::TestRustParityShape`.
8260    // Edits here MUST be mirrored in the Python pack — D7.
8261
8262    #[test]
8263    fn golden_simple_three_line_block() {
8264        let src = "alpha\nbeta\ngamma";
8265        let out = snippet(src, 2, 3, "g.axon");
8266        // Note: gutter=1, so empty_gutter=" " (one space). The
8267        // " --> ..." line therefore starts with two spaces ("<empty>"
8268        // + literal " --> ...").
8269        let expected = concat!(
8270            "  --> g.axon:2:3\n",
8271            "  |\n",
8272            "1 | alpha\n",
8273            "2 | beta\n",
8274            "  |   ^\n",
8275            "3 | gamma",
8276        );
8277        assert_eq!(out, expected);
8278    }
8279
8280    #[test]
8281    fn golden_first_line_caret() {
8282        let src = "abc\ndef\n";
8283        let out = snippet(src, 1, 1, "x");
8284        let expected = concat!(
8285            "  --> x:1:1\n",
8286            "  |\n",
8287            "1 | abc\n",
8288            "  | ^\n",
8289            "2 | def",
8290        );
8291        assert_eq!(out, expected);
8292    }
8293
8294    #[test]
8295    fn golden_two_digit_gutter() {
8296        let src: String = (1..=11)
8297            .map(|i| format!("L{i}"))
8298            .collect::<Vec<_>>()
8299            .join("\n");
8300        let out = snippet(&src, 10, 2, "big");
8301        let expected = concat!(
8302            "   --> big:10:2\n",
8303            "   |\n",
8304            " 8 | L8\n",
8305            " 9 | L9\n",
8306            "10 | L10\n",
8307            "   |  ^\n",
8308            "11 | L11",
8309        );
8310        assert_eq!(out, expected);
8311    }
8312}
8313
8314// ── §Fase 28.e — Parser integration tests for smart-suggest ──────────────────
8315//
8316// Mirror of `tests/test_fase28_smart_suggest.py::TestParserIntegration`.
8317// Verifies that the parser actually wires `suggest_for` into the
8318// unknown-keyword diagnostic at both error sites — top-level and
8319// flow-body.
8320#[cfg(test)]
8321mod fase28_smart_suggest_parser_tests {
8322    use super::*;
8323    use crate::lexer::Lexer;
8324
8325    fn lex(src: &str) -> Vec<Token> {
8326        Lexer::new(src, "<test>").tokenize().expect("lex")
8327    }
8328
8329    #[test]
8330    fn top_level_typo_suggests_flow() {
8331        let src = "flwo F() { }";
8332        let err = Parser::new(lex(src)).parse().expect_err("must error");
8333        assert!(
8334            err.message.contains("Did you mean `flow`?"),
8335            "msg: {}",
8336            err.message
8337        );
8338    }
8339
8340    #[test]
8341    fn top_level_unknown_far_no_suggestion() {
8342        let src = "qwerty F() { }";
8343        let err = Parser::new(lex(src)).parse().expect_err("must error");
8344        assert!(
8345            !err.message.contains("Did you mean"),
8346            "msg: {}",
8347            err.message
8348        );
8349    }
8350
8351    #[test]
8352    fn flow_body_typo_suggests_step() {
8353        let src = "flow F() { stepp S {} }";
8354        let err = Parser::new(lex(src)).parse().expect_err("must error");
8355        assert!(
8356            err.message.contains("Did you mean `step`"),
8357            "msg: {}",
8358            err.message
8359        );
8360    }
8361
8362    #[test]
8363    fn flow_body_typo_suggests_reason() {
8364        let src = "flow F() { reasn R {} }";
8365        let err = Parser::new(lex(src)).parse().expect_err("must error");
8366        assert!(
8367            err.message.contains("Did you mean `reason`?"),
8368            "msg: {}",
8369            err.message
8370        );
8371    }
8372
8373    #[test]
8374    fn recovery_mode_carries_hint() {
8375        let src = "flwo F() { }";
8376        let result = Parser::new(lex(src)).parse_with_recovery();
8377        assert!(
8378            result
8379                .errors
8380                .iter()
8381                .any(|e| e.message.contains("Did you mean `flow`?")),
8382            "errors: {:?}",
8383            result.errors
8384        );
8385    }
8386}
8387
8388// ── §Fase 35.m — mutate / purge where-clause capture ────────────────
8389
8390#[cfg(test)]
8391mod fase35m_mutate_purge_where_tests {
8392    use super::*;
8393
8394    fn parse(src: &str) -> Program {
8395        let tokens = crate::lexer::Lexer::new(src, "<test>")
8396            .tokenize()
8397            .expect("lex");
8398        Parser::new(tokens).parse().expect("parse")
8399    }
8400
8401    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
8402        for d in &prog.declarations {
8403            if let Declaration::Flow(f) = d {
8404                if f.name == flow {
8405                    return f.body.first().expect("flow has at least one step");
8406                }
8407            }
8408        }
8409        panic!("flow `{flow}` not found");
8410    }
8411
8412    #[test]
8413    fn mutate_captures_its_where_clause() {
8414        // Pre-35.m the `{ where: }` block was skipped — every mutate
8415        // ran whole-store. It must now reach `where_expr`.
8416        let prog =
8417            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
8418        match first_step(&prog, "F") {
8419            FlowStep::Mutate(m) => {
8420                assert_eq!(m.store_name, "accounts");
8421                assert_eq!(m.where_expr, "id = 1");
8422            }
8423            other => panic!("expected Mutate, got {other:?}"),
8424        }
8425    }
8426
8427    #[test]
8428    fn purge_captures_its_where_clause() {
8429        let prog =
8430            parse("flow F() -> Unit { purge logs { where: \"ts < 100\" } }");
8431        match first_step(&prog, "F") {
8432            FlowStep::Purge(p) => {
8433                assert_eq!(p.store_name, "logs");
8434                assert_eq!(p.where_expr, "ts < 100");
8435            }
8436            other => panic!("expected Purge, got {other:?}"),
8437        }
8438    }
8439
8440    #[test]
8441    fn mutate_without_a_where_block_is_a_whole_store_op() {
8442        // No `{ where: }` → an empty filter → the runtime renders
8443        // `WHERE TRUE` (every row). A valid, intentional op.
8444        let prog = parse("flow F() -> Unit { mutate accounts }");
8445        match first_step(&prog, "F") {
8446            FlowStep::Mutate(m) => {
8447                assert_eq!(m.store_name, "accounts");
8448                assert_eq!(m.where_expr, "");
8449            }
8450            other => panic!("expected Mutate, got {other:?}"),
8451        }
8452    }
8453}
8454
8455// ── §Fase 35.o — persist field-block capture ────────────────────────
8456
8457#[cfg(test)]
8458mod fase35o_persist_fields_tests {
8459    use super::*;
8460
8461    fn parse(src: &str) -> Program {
8462        let tokens = crate::lexer::Lexer::new(src, "<test>")
8463            .tokenize()
8464            .expect("lex");
8465        Parser::new(tokens).parse().expect("parse")
8466    }
8467
8468    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
8469        for d in &prog.declarations {
8470            if let Declaration::Flow(f) = d {
8471                if f.name == flow {
8472                    return f.body.first().expect("flow has at least one step");
8473                }
8474            }
8475        }
8476        panic!("flow `{flow}` not found");
8477    }
8478
8479    #[test]
8480    fn persist_captures_its_field_block() {
8481        // Pre-35.o the `{ col: value }` block was skipped — every
8482        // persist wrote the whole binding context. It must now reach
8483        // `fields`, in source order, with value expressions raw.
8484        let prog = parse(
8485            "flow F() -> Unit { persist into chat_history { \
8486             session_id: \"${session_id}\" sender: \"user\" \
8487             content: \"${message}\" } }",
8488        );
8489        match first_step(&prog, "F") {
8490            FlowStep::Persist(p) => {
8491                assert_eq!(p.store_name, "chat_history");
8492                assert_eq!(
8493                    p.fields,
8494                    vec![
8495                        ("session_id".to_string(), "${session_id}".to_string()),
8496                        ("sender".to_string(), "user".to_string()),
8497                        ("content".to_string(), "${message}".to_string()),
8498                    ]
8499                );
8500            }
8501            other => panic!("expected Persist, got {other:?}"),
8502        }
8503    }
8504
8505    #[test]
8506    fn persist_without_a_block_keeps_the_user_bindings_fallback() {
8507        // No `{ }` → empty `fields` → the runtime falls back to the
8508        // v1.30.0 user-bindings row. Backward-compatible.
8509        let prog = parse("flow F() -> Unit { persist events }");
8510        match first_step(&prog, "F") {
8511            FlowStep::Persist(p) => {
8512                assert_eq!(p.store_name, "events");
8513                assert!(p.fields.is_empty());
8514            }
8515            other => panic!("expected Persist, got {other:?}"),
8516        }
8517    }
8518
8519    #[test]
8520    fn persist_accepts_the_optional_into_connector() {
8521        // `persist into X` and `persist X` resolve to the SAME store
8522        // name — pre-35.o `into` was captured AS the store name.
8523        let with =
8524            parse("flow F() -> Unit { persist into accounts { id: \"1\" } }");
8525        let without =
8526            parse("flow F() -> Unit { persist accounts { id: \"1\" } }");
8527        for prog in [&with, &without] {
8528            match first_step(prog, "F") {
8529                FlowStep::Persist(p) => assert_eq!(p.store_name, "accounts"),
8530                other => panic!("expected Persist, got {other:?}"),
8531            }
8532        }
8533    }
8534
8535    #[test]
8536    fn persist_into_without_a_block_resolves_the_store_name() {
8537        // `persist into events` — the `into` connector is skipped, the
8538        // store name is `events` (not `into`). Lateral bug closed.
8539        let prog = parse("flow F() -> Unit { persist into events }");
8540        match first_step(&prog, "F") {
8541            FlowStep::Persist(p) => {
8542                assert_eq!(p.store_name, "events");
8543                assert!(p.fields.is_empty());
8544            }
8545            other => panic!("expected Persist, got {other:?}"),
8546        }
8547    }
8548
8549    #[test]
8550    fn persist_fields_lower_into_the_ir() {
8551        // The IR generator must carry `fields` onto `IRPersistStep`
8552        // so the runtime reads exactly the declared columns.
8553        let prog = parse(
8554            "flow F() -> Unit { persist into chat { content: \"${msg}\" } }",
8555        );
8556        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
8557        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
8558        match flow.steps.first().expect("one step") {
8559            crate::ir_nodes::IRFlowNode::Persist(p) => {
8560                assert_eq!(p.store_name, "chat");
8561                assert_eq!(
8562                    p.fields,
8563                    vec![("content".to_string(), "${msg}".to_string())]
8564                );
8565            }
8566            other => panic!("expected IRFlowNode::Persist, got {other:?}"),
8567        }
8568    }
8569}
8570
8571// ── §Fase 35.p — mutate SET-field-block capture ─────────────────────
8572
8573#[cfg(test)]
8574mod fase35p_mutate_fields_tests {
8575    use super::*;
8576
8577    fn parse(src: &str) -> Program {
8578        let tokens = crate::lexer::Lexer::new(src, "<test>")
8579            .tokenize()
8580            .expect("lex");
8581        Parser::new(tokens).parse().expect("parse")
8582    }
8583
8584    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
8585        for d in &prog.declarations {
8586            if let Declaration::Flow(f) = d {
8587                if f.name == flow {
8588                    return f.body.first().expect("flow has at least one step");
8589                }
8590            }
8591        }
8592        panic!("flow `{flow}` not found");
8593    }
8594
8595    #[test]
8596    fn mutate_captures_its_set_field_block() {
8597        // Pre-35.p every key but `where:` was skipped — the runtime
8598        // SET every flow binding. The SET columns must now reach
8599        // `fields`, in source order, with `where:` still captured.
8600        let prog = parse(
8601            "flow F() -> Unit { mutate accounts { where: \"id = ${id}\" \
8602             balance: \"${new_balance}\" status: \"active\" } }",
8603        );
8604        match first_step(&prog, "F") {
8605            FlowStep::Mutate(m) => {
8606                assert_eq!(m.store_name, "accounts");
8607                assert_eq!(m.where_expr, "id = ${id}");
8608                assert_eq!(
8609                    m.fields,
8610                    vec![
8611                        ("balance".to_string(), "${new_balance}".to_string()),
8612                        ("status".to_string(), "active".to_string()),
8613                    ]
8614                );
8615            }
8616            other => panic!("expected Mutate, got {other:?}"),
8617        }
8618    }
8619
8620    #[test]
8621    fn mutate_where_only_block_has_no_set_fields() {
8622        // A `{ where: }`-only block → empty `fields` → the runtime
8623        // falls back to the v1.31.0 user-bindings SET.
8624        let prog =
8625            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
8626        match first_step(&prog, "F") {
8627            FlowStep::Mutate(m) => {
8628                assert_eq!(m.where_expr, "id = 1");
8629                assert!(m.fields.is_empty());
8630            }
8631            other => panic!("expected Mutate, got {other:?}"),
8632        }
8633    }
8634
8635    #[test]
8636    fn mutate_with_no_block_is_a_whole_store_op() {
8637        // No block at all → empty where + empty fields (a whole-store
8638        // UPDATE from user bindings) — unchanged from 35.m.
8639        let prog = parse("flow F() -> Unit { mutate accounts }");
8640        match first_step(&prog, "F") {
8641            FlowStep::Mutate(m) => {
8642                assert_eq!(m.store_name, "accounts");
8643                assert_eq!(m.where_expr, "");
8644                assert!(m.fields.is_empty());
8645            }
8646            other => panic!("expected Mutate, got {other:?}"),
8647        }
8648    }
8649
8650    #[test]
8651    fn mutate_fields_lower_into_the_ir() {
8652        let prog = parse(
8653            "flow F() -> Unit { mutate t { where: \"id = 1\" v: \"${x}\" } }",
8654        );
8655        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
8656        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
8657        match flow.steps.first().expect("one step") {
8658            crate::ir_nodes::IRFlowNode::Mutate(m) => {
8659                assert_eq!(m.where_expr, "id = 1");
8660                assert_eq!(
8661                    m.fields,
8662                    vec![("v".to_string(), "${x}".to_string())]
8663                );
8664            }
8665            other => panic!("expected IRFlowNode::Mutate, got {other:?}"),
8666        }
8667    }
8668}
8669