Skip to main content

axon_frontend/
ir_generator.rs

1//! AXON IR Generator — AST → IR transformation.
2//!
3//! Direct port of axon/compiler/ir_generator.py (Tier 1 subset).
4//!
5//! Tier 1 constructs produce fully typed IR nodes.
6//! Tier 2+ GenericDeclarations are emitted as generic JSON objects.
7//! Flow data edges and execution levels are computed.
8
9use std::collections::HashMap;
10
11use crate::ast::*;
12use crate::ir_nodes::*;
13use crate::store_schema::{StoreColumn, StoreColumnSchema};
14
15/// §Fase 38.b (D1) — lower the parsed AST schema declaration to its
16/// IR mirror. Pure + total — every AST variant has an IR variant.
17fn lower_column_schema(s: &StoreColumnSchema) -> IRStoreColumnSchema {
18    match s {
19        StoreColumnSchema::Inline { columns, .. } => IRStoreColumnSchema::Inline {
20            columns: columns.iter().map(lower_column).collect(),
21        },
22        StoreColumnSchema::ManifestRef { qualified_name, .. } => {
23            IRStoreColumnSchema::ManifestRef {
24                qualified_name: qualified_name.clone(),
25            }
26        }
27        StoreColumnSchema::EnvVar { var_name, .. } => IRStoreColumnSchema::EnvVar {
28            var_name: var_name.clone(),
29        },
30    }
31}
32
33fn lower_column(c: &StoreColumn) -> IRStoreColumn {
34    IRStoreColumn {
35        name: c.name.clone(),
36        // The IR carries the canonical PascalCase name; the alias the
37        // adopter wrote in source is already normalised at parse time.
38        col_type: c.col_type.canonical_name().to_string(),
39        primary_key: c.primary_key,
40        auto_increment: c.auto_increment,
41        not_null: c.not_null,
42        unique: c.unique,
43        default_value: c.default_value.clone(),
44        // §Fase 38.x.c (D2) — round-trip the IDENTITY marker through IR.
45        identity: c.identity,
46    }
47}
48
49pub struct IRGenerator {
50    personas: HashMap<String, IRPersona>,
51    contexts: HashMap<String, IRContext>,
52    anchors: HashMap<String, IRAnchor>,
53    flows: HashMap<String, IRFlow>,
54    lambda_data_specs: HashMap<String, IRLambdaData>,
55    /// §λ-L-E Fase 1 (Free Monad root) — Manifests / Observes, in
56    /// declaration order, become nodes the Handler layer will interpret.
57    intention_ops: Vec<IRIntentionOperation>,
58    /// Anchor for the intention tree's own source position.
59    program_line: u32,
60    program_column: u32,
61    /// §λ-L-E Fase 13 — channel registry for mobility detection at lowering.
62    /// Names of declared channels are recorded as they're visited so
63    /// `visit_emit` can pre-resolve `value_is_channel` without re-scanning
64    /// the AST (parity with the Python `IREmit.value_is_channel` flag).
65    channel_names: std::collections::HashSet<String>,
66}
67
68impl IRGenerator {
69    pub fn new() -> Self {
70        IRGenerator {
71            personas: HashMap::new(),
72            contexts: HashMap::new(),
73            anchors: HashMap::new(),
74            flows: HashMap::new(),
75            lambda_data_specs: HashMap::new(),
76            intention_ops: Vec::new(),
77            program_line: 1,
78            program_column: 1,
79            channel_names: std::collections::HashSet::new(),
80        }
81    }
82
83    pub fn generate(mut self, program: &Program) -> IRProgram {
84        let mut ir = IRProgram::new();
85        self.program_line = program.loc.line;
86        self.program_column = program.loc.column;
87
88        // Phase 1: visit all declarations
89        for decl in &program.declarations {
90            self.visit_declaration(decl, &mut ir);
91        }
92
93        // Phase 2: resolve run cross-references
94        for run in &mut ir.runs {
95            if let Some(flow) = self.flows.get(&run.flow_name) {
96                run.resolved_flow = Some(flow.clone());
97            }
98            if let Some(persona) = self.personas.get(&run.persona_name) {
99                run.resolved_persona = Some(persona.clone());
100            }
101            if let Some(context) = self.contexts.get(&run.context_name) {
102                run.resolved_context = Some(context.clone());
103            }
104            for anchor_name in &run.anchor_names {
105                if let Some(anchor) = self.anchors.get(anchor_name) {
106                    run.resolved_anchors.push(anchor.clone());
107                }
108            }
109        }
110
111        // Phase 3 (§8.2.h.2): assemble the intention tree if the program
112        // declared any Fase-1 cognitive-I/O operations. Empty ⇒ `None`
113        // (JSON `null`), matching Python's reference behaviour.
114        if !self.intention_ops.is_empty() {
115            ir.intention_tree = Some(IRIntentionTree {
116                node_type: "intention_tree",
117                source_line: self.program_line,
118                source_column: self.program_column,
119                operations: std::mem::take(&mut self.intention_ops),
120            });
121        }
122
123        // Phase 4 (§Fase 53.b, founder refinement B): deterministic
124        // extension order. Declarations across multiple `import`ed files
125        // arrive in file+source order; sorting by the extension
126        // identifier makes `ir.extensions` a pure function of the
127        // declared set, so the proof-bundle hash (§53.d) is stable
128        // regardless of declaration order. Stable sort preserves the
129        // (already-deterministic, single-file) member order within each
130        // extension.
131        ir.extensions.sort_by(|a, b| a.name.cmp(&b.name));
132
133        ir
134    }
135
136    fn visit_declaration(&mut self, decl: &Declaration, ir: &mut IRProgram) {
137        match decl {
138            Declaration::Import(n) => ir.imports.push(self.visit_import(n)),
139            Declaration::Persona(n) => {
140                let node = self.visit_persona(n);
141                self.personas.insert(node.name.clone(), node.clone());
142                ir.personas.push(node);
143            }
144            Declaration::Context(n) => {
145                let node = self.visit_context(n);
146                self.contexts.insert(node.name.clone(), node.clone());
147                ir.contexts.push(node);
148            }
149            Declaration::Anchor(n) => {
150                let node = self.visit_anchor(n);
151                self.anchors.insert(node.name.clone(), node.clone());
152                ir.anchors.push(node);
153            }
154            Declaration::Memory(n) => ir.memories.push(self.visit_memory(n)),
155            Declaration::Tool(n) => ir.tools.push(self.visit_tool(n)),
156            Declaration::Type(n) => ir.types.push(self.visit_type(n)),
157            Declaration::Flow(n) => {
158                let node = self.visit_flow(n);
159                self.flows.insert(node.name.clone(), node.clone());
160                ir.flows.push(node);
161            }
162            Declaration::Intent(_) => {} // intent is inlined into steps
163            Declaration::Run(n) => ir.runs.push(self.visit_run(n)),
164            Declaration::LambdaData(n) => {
165                let node = self.visit_lambda_data(n);
166                self.lambda_data_specs
167                    .insert(node.name.clone(), node.clone());
168                ir.lambda_data_specs.push(node);
169            }
170            Declaration::Agent(n) => ir.agents.push(self.visit_agent(n)),
171            Declaration::Shield(n) => ir.shields.push(self.visit_shield(n)),
172            Declaration::Pix(n) => ir.pix_specs.push(self.visit_pix(n)),
173            Declaration::Psyche(n) => ir.psyche_specs.push(self.visit_psyche(n)),
174            Declaration::Corpus(n) => ir.corpus_specs.push(self.visit_corpus(n)),
175            Declaration::Dataspace(n) => ir.dataspace_specs.push(self.visit_dataspace(n)),
176            Declaration::Ots(n) => ir.ots_specs.push(self.visit_ots(n)),
177            Declaration::Mandate(n) => ir.mandate_specs.push(self.visit_mandate(n)),
178            Declaration::Compute(n) => ir.compute_specs.push(self.visit_compute(n)),
179            Declaration::Daemon(n) => ir.daemons.push(self.visit_daemon(n)),
180            Declaration::AxonStore(n) => ir.axonstore_specs.push(self.visit_axonstore(n)),
181            Declaration::AxonEndpoint(n) => ir.endpoints.push(self.visit_axonendpoint(n)),
182            // §Fase 53.b — lower the `extension` declaration into the IR.
183            // Deterministic ordering is applied once, at the end of
184            // `generate` (Phase 4), not here.
185            Declaration::Extension(n) => ir.extensions.push(self.visit_extension(n)),
186            Declaration::Resource(n) => ir.resources.push(self.visit_resource(n)),
187            Declaration::Fabric(n) => ir.fabrics.push(self.visit_fabric(n)),
188            Declaration::Manifest(n) => {
189                let m = self.visit_manifest(n);
190                // §λ-L-E Fase 1 — manifest is a provisioning intention
191                // (goes to the Free-Monad tree for the Handler layer).
192                self.intention_ops
193                    .push(IRIntentionOperation::Manifest(m.clone()));
194                ir.manifests.push(m);
195            }
196            Declaration::Observe(n) => {
197                let o = self.visit_observe(n);
198                // §λ-L-E Fase 1 — observations are intentions too.
199                self.intention_ops
200                    .push(IRIntentionOperation::Observe(o.clone()));
201                ir.observations.push(o);
202            }
203            Declaration::Reconcile(n) => ir.reconciles.push(self.visit_reconcile(n)),
204            Declaration::Lease(n) => ir.leases.push(self.visit_lease(n)),
205            Declaration::Ensemble(n) => ir.ensembles.push(self.visit_ensemble(n)),
206            Declaration::Session(n) => ir.sessions.push(self.visit_session(n)),
207            Declaration::Topology(n) => ir.topologies.push(self.visit_topology(n)),
208            Declaration::Socket(n) => ir.sockets.push(self.visit_socket(n)),
209            Declaration::Immune(n) => ir.immunes.push(self.visit_immune(n)),
210            Declaration::Reflex(n) => ir.reflexes.push(self.visit_reflex(n)),
211            Declaration::Heal(n) => ir.heals.push(self.visit_heal(n)),
212            Declaration::Component(n) => ir.components.push(self.visit_component(n)),
213            Declaration::View(n) => ir.views.push(self.visit_view(n)),
214            // §λ-L-E Fase 13 — Mobile typed channels (paper §3, §4).
215            // Record the channel name BEFORE visiting subsequent flow
216            // bodies so `IREmit.value_is_channel` resolves correctly for
217            // mobility uses appearing after this declaration in source
218            // order (matches Python `_channels` dict semantics).
219            Declaration::Channel(n) => {
220                self.channel_names.insert(n.name.clone());
221                ir.channels.push(self.visit_channel(n));
222            }
223            Declaration::Epistemic(eb) => {
224                for child in &eb.body {
225                    self.visit_declaration(child, ir);
226                }
227            }
228            Declaration::Let(_) => {}
229            Declaration::Generic(g) => {
230                // Emit as generic JSON in the appropriate collection
231                let val = serde_json::json!({
232                    "node_type": g.keyword,
233                    "source_line": g.loc.line,
234                    "source_column": g.loc.column,
235                    "name": g.name,
236                });
237                // Tier 3+ generic fallback — no typed IR collection
238                let _ = val; // suppress unused warning
239            }
240        }
241    }
242
243    // ── Visitors ─────────────────────────────────────────────────
244
245    fn visit_import(&self, n: &ImportNode) -> IRImport {
246        IRImport {
247            node_type: "import",
248            source_line: n.loc.line,
249            source_column: n.loc.column,
250            module_path: n.module_path.clone(),
251            names: n.names.clone(),
252        }
253    }
254
255    fn visit_persona(&self, n: &PersonaDefinition) -> IRPersona {
256        IRPersona {
257            node_type: "persona",
258            source_line: n.loc.line,
259            source_column: n.loc.column,
260            name: n.name.clone(),
261            domain: n.domain.clone(),
262            tone: n.tone.clone(),
263            confidence_threshold: n.confidence_threshold,
264            cite_sources: n.cite_sources,
265            refuse_if: n.refuse_if.clone(),
266            language: n.language.clone(),
267            description: n.description.clone(),
268        }
269    }
270
271    fn visit_context(&self, n: &ContextDefinition) -> IRContext {
272        IRContext {
273            node_type: "context",
274            source_line: n.loc.line,
275            source_column: n.loc.column,
276            name: n.name.clone(),
277            memory_scope: n.memory_scope.clone(),
278            language: n.language.clone(),
279            depth: n.depth.clone(),
280            max_tokens: n.max_tokens,
281            temperature: n.temperature,
282            cite_sources: n.cite_sources,
283        }
284    }
285
286    fn visit_anchor(&self, n: &AnchorConstraint) -> IRAnchor {
287        IRAnchor {
288            node_type: "anchor",
289            source_line: n.loc.line,
290            source_column: n.loc.column,
291            name: n.name.clone(),
292            description: n.description.clone(),
293            require: n.require.clone(),
294            reject: n.reject.clone(),
295            enforce: n.enforce.clone(),
296            confidence_floor: n.confidence_floor,
297            unknown_response: n.unknown_response.clone(),
298            on_violation: n.on_violation.clone(),
299            on_violation_target: n.on_violation_target.clone(),
300        }
301    }
302
303    fn visit_memory(&self, n: &MemoryDefinition) -> IRMemory {
304        IRMemory {
305            node_type: "memory",
306            source_line: n.loc.line,
307            source_column: n.loc.column,
308            name: n.name.clone(),
309            store: n.store.clone(),
310            backend: n.backend.clone(),
311            retrieval: n.retrieval.clone(),
312            decay: n.decay.clone(),
313        }
314    }
315
316    fn visit_tool(&self, n: &ToolDefinition) -> IRToolSpec {
317        let effect_row = match &n.effects {
318            Some(eff) => {
319                let mut row = eff.effects.clone();
320                if !eff.epistemic_level.is_empty() {
321                    row.push(format!("epistemic:{}", eff.epistemic_level));
322                }
323                row
324            }
325            None => Vec::new(),
326        };
327
328        IRToolSpec {
329            node_type: "tool_spec",
330            source_line: n.loc.line,
331            source_column: n.loc.column,
332            name: n.name.clone(),
333            provider: n.provider.clone(),
334            max_results: n.max_results,
335            filter_expr: n.filter_expr.clone(),
336            timeout: n.timeout.clone(),
337            runtime: n.runtime.clone(),
338            sandbox: n.sandbox,
339            input_schema: Vec::new(),
340            output_schema: String::new(),
341            // §Fase 58.c — carry the typed input schema + output type into the
342            // IR (the §32 input_schema/output_schema above stay the validation
343            // hints; these are the D1 type contract).
344            parameters: n
345                .parameters
346                .iter()
347                .map(|p| {
348                    let mut type_name = p.type_expr.name.clone();
349                    if !p.type_expr.generic_param.is_empty() {
350                        type_name.push('<');
351                        type_name.push_str(&p.type_expr.generic_param);
352                        type_name.push('>');
353                    }
354                    crate::ir_nodes::IRToolParam {
355                        name: p.name.clone(),
356                        type_name,
357                        optional: p.type_expr.optional,
358                    }
359                })
360                .collect(),
361            output_type: n.output_type.clone(),
362            effect_row,
363        }
364    }
365
366    fn visit_type(&self, n: &TypeDefinition) -> IRType {
367        let fields = n
368            .fields
369            .iter()
370            .map(|f| IRTypeField {
371                node_type: "type_field",
372                source_line: f.loc.line,
373                source_column: f.loc.column,
374                name: f.name.clone(),
375                type_name: f.type_expr.name.clone(),
376                generic_param: f.type_expr.generic_param.clone(),
377                optional: f.type_expr.optional,
378            })
379            .collect();
380
381        let (range_min, range_max) = match &n.range_constraint {
382            Some(rc) => (Some(rc.min_value), Some(rc.max_value)),
383            None => (None, None),
384        };
385
386        let where_expression = match &n.where_clause {
387            Some(wc) => wc.expression.clone(),
388            None => String::new(),
389        };
390
391        IRType {
392            node_type: "type_def",
393            source_line: n.loc.line,
394            source_column: n.loc.column,
395            name: n.name.clone(),
396            fields,
397            range_min,
398            range_max,
399            where_expression,
400            compliance: n.compliance.clone(),
401        }
402    }
403
404    fn visit_flow(&self, n: &FlowDefinition) -> IRFlow {
405        let parameters: Vec<IRParameter> = n
406            .parameters
407            .iter()
408            .map(|p| IRParameter {
409                node_type: "parameter",
410                source_line: p.loc.line,
411                source_column: p.loc.column,
412                name: p.name.clone(),
413                type_name: p.type_expr.name.clone(),
414                generic_param: p.type_expr.generic_param.clone(),
415                optional: p.type_expr.optional,
416            })
417            .collect();
418
419        let (return_type_name, return_type_generic, return_type_optional) = match &n.return_type {
420            Some(rt) => (rt.name.clone(), rt.generic_param.clone(), rt.optional),
421            None => (String::new(), String::new(), false),
422        };
423
424        // Collect all flow body nodes as typed IR
425        let steps: Vec<IRFlowNode> = n.body.iter().map(|fs| self.visit_flow_step(fs)).collect();
426
427        // Compute data edges from Step nodes: if step B's given references "A.output", create edge A → B
428        let mut edges: Vec<IRDataEdge> = Vec::new();
429        let step_names: Vec<String> = steps
430            .iter()
431            .filter_map(|n| {
432                if let IRFlowNode::Step(s) = n {
433                    Some(s.name.clone())
434                } else {
435                    None
436                }
437            })
438            .collect();
439        for node in &steps {
440            if let IRFlowNode::Step(step) = node {
441                if !step.given.is_empty() {
442                    let given_root = step.given.split('.').next().unwrap_or("");
443                    if step_names.contains(&given_root.to_string()) && given_root != step.name {
444                        edges.push(IRDataEdge {
445                            node_type: "data_edge",
446                            source_line: step.source_line,
447                            source_column: step.source_column,
448                            source_step: given_root.to_string(),
449                            target_step: step.name.clone(),
450                            type_name: "Any".to_string(),
451                        });
452                    }
453                }
454            }
455        }
456
457        // Compute execution levels (topological ordering) — Step nodes only
458        let execution_levels = self.compute_execution_levels(&steps, &edges);
459
460        IRFlow {
461            node_type: "flow",
462            source_line: n.loc.line,
463            source_column: n.loc.column,
464            name: n.name.clone(),
465            parameters,
466            return_type_name,
467            return_type_generic,
468            return_type_optional,
469            steps,
470            edges,
471            execution_levels,
472        }
473    }
474
475    fn visit_flow_step(&self, fs: &FlowStep) -> IRFlowNode {
476        match fs {
477            FlowStep::Step(s) => IRFlowNode::Step(IRStep {
478                node_type: "step",
479                source_line: s.loc.line,
480                source_column: s.loc.column,
481                name: s.name.clone(),
482                persona_ref: s.persona_ref.clone(),
483                given: s.given.clone(),
484                ask: s.ask.clone(),
485                use_tool: None,
486                probe: None,
487                reason: None,
488                weave: None,
489                output_type: s.output_type.clone(),
490                confidence_floor: s.confidence_floor,
491                navigate_ref: s.navigate_ref.clone(),
492                apply_ref: s.apply_ref.clone(),
493                body: Vec::new(),
494            }),
495            FlowStep::Probe(s) => IRFlowNode::Probe(IRProbe {
496                node_type: "probe",
497                source_line: s.loc.line,
498                source_column: s.loc.column,
499                target: s.target.clone(),
500            }),
501            FlowStep::Reason(s) => IRFlowNode::Reason(IRReasonStep {
502                node_type: "reason",
503                source_line: s.loc.line,
504                source_column: s.loc.column,
505                strategy: s.strategy.clone(),
506                target: s.target.clone(),
507            }),
508            FlowStep::Validate(s) => IRFlowNode::Validate(IRValidateStep {
509                node_type: "validate",
510                source_line: s.loc.line,
511                source_column: s.loc.column,
512                target: s.target.clone(),
513                rule: s.rule.clone(),
514            }),
515            FlowStep::Refine(s) => IRFlowNode::Refine(IRRefineStep {
516                node_type: "refine",
517                source_line: s.loc.line,
518                source_column: s.loc.column,
519                target: s.target.clone(),
520                strategy: s.strategy.clone(),
521            }),
522            FlowStep::Weave(s) => IRFlowNode::Weave(IRWeaveStep {
523                node_type: "weave",
524                source_line: s.loc.line,
525                source_column: s.loc.column,
526                sources: s.sources.clone(),
527                target: s.target.clone(),
528                format_type: s.format_type.clone(),
529                priority: s.priority.clone(),
530                style: s.style.clone(),
531            }),
532            FlowStep::UseTool(s) => IRFlowNode::UseTool(IRUseToolStep {
533                node_type: "use_tool",
534                source_line: s.loc.line,
535                source_column: s.loc.column,
536                tool_name: s.tool_name.clone(),
537                // §Fase 58.b — `LegacyPositional` projects its string verbatim
538                // (D5, unchanged IR). `Named` keeps the legacy `argument` empty
539                // and carries its pairs in `named_args` below (§58.c).
540                argument: s.args.legacy_argument(),
541                // §Fase 58.c — structured keyword args survive to the IR.
542                named_args: match &s.args {
543                    UseArgs::Named(pairs) => pairs
544                        .iter()
545                        .map(|(name, value, value_kind)| crate::ir_nodes::IRNamedArg {
546                            name: name.clone(),
547                            value: value.clone(),
548                            value_kind: value_kind.clone(),
549                        })
550                        .collect(),
551                    UseArgs::LegacyPositional(_) => Vec::new(),
552                },
553            }),
554            FlowStep::Remember(s) => IRFlowNode::Remember(IRRememberStep {
555                node_type: "remember",
556                source_line: s.loc.line,
557                source_column: s.loc.column,
558                expression: s.expression.clone(),
559                memory_target: s.memory_target.clone(),
560            }),
561            FlowStep::Recall(s) => IRFlowNode::Recall(IRRecallStep {
562                node_type: "recall",
563                source_line: s.loc.line,
564                source_column: s.loc.column,
565                query: s.query.clone(),
566                memory_source: s.memory_source.clone(),
567            }),
568            FlowStep::If(s) => IRFlowNode::Conditional(IRConditional {
569                node_type: "conditional",
570                source_line: s.loc.line,
571                source_column: s.loc.column,
572                condition: s.condition.clone(),
573                comparison_op: s.comparison_op.clone(),
574                comparison_value: s.comparison_value.clone(),
575                then_body: s
576                    .then_body
577                    .iter()
578                    .map(|fs| self.visit_flow_step(fs))
579                    .collect(),
580                else_body: s
581                    .else_body
582                    .iter()
583                    .map(|fs| self.visit_flow_step(fs))
584                    .collect(),
585                conditions: s.conditions.clone(),
586                conjunctor: s.conjunctor.clone(),
587            }),
588            FlowStep::ForIn(s) => IRFlowNode::ForIn(IRForIn {
589                node_type: "for_in",
590                source_line: s.loc.line,
591                source_column: s.loc.column,
592                variable: s.variable.clone(),
593                iterable: s.iterable.clone(),
594                body: s.body.iter().map(|fs| self.visit_flow_step(fs)).collect(),
595            }),
596            FlowStep::Let(s) => IRFlowNode::Let(IRLetBinding {
597                node_type: "let_binding",
598                source_line: s.loc.line,
599                source_column: s.loc.column,
600                target: s.identifier.clone(),
601                value: s.value_expr.clone(),
602                value_kind: if s.value_kind.is_empty() {
603                    "literal".to_string()
604                } else {
605                    s.value_kind.clone()
606                },
607            }),
608            FlowStep::Return(s) => IRFlowNode::Return(IRReturnStep {
609                node_type: "return",
610                source_line: s.loc.line,
611                source_column: s.loc.column,
612                value_expr: s.value_expr.clone(),
613            }),
614            // Fase 19.e — break / continue. Both are payload-free at
615            // both AST and IR level; the runner translates them into
616            // sentinel exceptions caught by the enclosing for-in loop.
617            FlowStep::Break(s) => IRFlowNode::Break(IRBreakStep {
618                node_type: "break",
619                source_line: s.loc.line,
620                source_column: s.loc.column,
621            }),
622            FlowStep::Continue(s) => IRFlowNode::Continue(IRContinueStep {
623                node_type: "continue",
624                source_line: s.loc.line,
625                source_column: s.loc.column,
626            }),
627            FlowStep::LambdaDataApply(s) => IRFlowNode::LambdaDataApply(IRLambdaDataApply {
628                node_type: "lambda_data_apply",
629                source_line: s.loc.line,
630                source_column: s.loc.column,
631                lambda_data_name: s.lambda_data_name.clone(),
632                target: s.target.clone(),
633                output_type: s.output_type.clone(),
634            }),
635            FlowStep::Par(s) => IRFlowNode::Par(IRParallelBlock {
636                node_type: "parallel_block",
637                source_line: s.loc.line,
638                source_column: s.loc.column,
639            }),
640            FlowStep::Hibernate(s) => IRFlowNode::Hibernate(IRHibernateStep {
641                node_type: "hibernate",
642                source_line: s.loc.line,
643                source_column: s.loc.column,
644                event_name: s.event_name.clone(),
645                timeout: s.timeout.clone(),
646            }),
647            FlowStep::Deliberate(s) => IRFlowNode::Deliberate(IRDeliberateBlock {
648                node_type: "deliberate",
649                source_line: s.loc.line,
650                source_column: s.loc.column,
651            }),
652            FlowStep::Consensus(s) => IRFlowNode::Consensus(IRConsensusBlock {
653                node_type: "consensus",
654                source_line: s.loc.line,
655                source_column: s.loc.column,
656            }),
657            FlowStep::Forge(s) => IRFlowNode::Forge(IRForgeBlock {
658                node_type: "forge",
659                source_line: s.loc.line,
660                source_column: s.loc.column,
661            }),
662            FlowStep::Focus(s) => IRFlowNode::Focus(IRFocusStep {
663                node_type: "focus",
664                source_line: s.loc.line,
665                source_column: s.loc.column,
666                expression: s.expression.clone(),
667            }),
668            FlowStep::Associate(s) => IRFlowNode::Associate(IRAssociateStep {
669                node_type: "associate",
670                source_line: s.loc.line,
671                source_column: s.loc.column,
672                left: s.left.clone(),
673                right: s.right.clone(),
674                using_field: s.using_field.clone(),
675            }),
676            FlowStep::Aggregate(s) => IRFlowNode::Aggregate(IRAggregateStep {
677                node_type: "aggregate",
678                source_line: s.loc.line,
679                source_column: s.loc.column,
680                target: s.target.clone(),
681                group_by: s.group_by.clone(),
682                alias: s.alias.clone(),
683            }),
684            FlowStep::ExploreStep(s) => IRFlowNode::Explore(IRExploreStep {
685                node_type: "explore",
686                source_line: s.loc.line,
687                source_column: s.loc.column,
688                target: s.target.clone(),
689                limit: s.limit,
690            }),
691            FlowStep::Ingest(s) => IRFlowNode::Ingest(IRIngestStep {
692                node_type: "ingest",
693                source_line: s.loc.line,
694                source_column: s.loc.column,
695                source: s.source.clone(),
696                target: s.target.clone(),
697            }),
698            FlowStep::ShieldApply(s) => IRFlowNode::ShieldApply(IRShieldApplyStep {
699                node_type: "shield_apply",
700                source_line: s.loc.line,
701                source_column: s.loc.column,
702                shield_name: s.shield_name.clone(),
703                target: s.target.clone(),
704                output_type: s.output_type.clone(),
705            }),
706            FlowStep::Stream(s) => IRFlowNode::Stream(IRStreamBlock {
707                node_type: "stream",
708                source_line: s.loc.line,
709                source_column: s.loc.column,
710            }),
711            FlowStep::Navigate(s) => IRFlowNode::Navigate(IRNavigateStep {
712                node_type: "navigate",
713                source_line: s.loc.line,
714                source_column: s.loc.column,
715                pix_ref: s.pix_name.clone(),
716                corpus_ref: s.corpus_name.clone(),
717                query: s.query_expr.clone(),
718                trail_enabled: s.trail_enabled,
719                output_name: s.output_name.clone(),
720            }),
721            FlowStep::Drill(s) => IRFlowNode::Drill(IRDrillStep {
722                node_type: "drill",
723                source_line: s.loc.line,
724                source_column: s.loc.column,
725                pix_ref: s.pix_name.clone(),
726                subtree_path: s.subtree_path.clone(),
727                query: s.query_expr.clone(),
728                output_name: s.output_name.clone(),
729            }),
730            FlowStep::Trail(s) => IRFlowNode::Trail(IRTrailStep {
731                node_type: "trail",
732                source_line: s.loc.line,
733                source_column: s.loc.column,
734                navigate_ref: s.navigate_ref.clone(),
735            }),
736            FlowStep::Corroborate(s) => IRFlowNode::Corroborate(IRCorroborateStep {
737                node_type: "corroborate",
738                source_line: s.loc.line,
739                source_column: s.loc.column,
740                navigate_ref: s.navigate_ref.clone(),
741                output_name: s.output_name.clone(),
742            }),
743            FlowStep::OtsApply(s) => IRFlowNode::OtsApply(IROtsApplyStep {
744                node_type: "ots_apply",
745                source_line: s.loc.line,
746                source_column: s.loc.column,
747                ots_name: s.ots_name.clone(),
748                target: s.target.clone(),
749                output_type: s.output_type.clone(),
750            }),
751            FlowStep::MandateApply(s) => IRFlowNode::MandateApply(IRMandateApplyStep {
752                node_type: "mandate_apply",
753                source_line: s.loc.line,
754                source_column: s.loc.column,
755                mandate_name: s.mandate_name.clone(),
756                target: s.target.clone(),
757                output_type: s.output_type.clone(),
758            }),
759            FlowStep::ComputeApply(s) => IRFlowNode::ComputeApply(IRComputeApplyStep {
760                node_type: "compute_apply",
761                source_line: s.loc.line,
762                source_column: s.loc.column,
763                compute_name: s.compute_name.clone(),
764                arguments: s.arguments.clone(),
765                output_name: s.output_name.clone(),
766            }),
767            FlowStep::Listen(s) => IRFlowNode::Listen(IRListenStep {
768                node_type: "listen",
769                source_line: s.loc.line,
770                source_column: s.loc.column,
771                channel: s.channel.clone(),
772                channel_is_ref: s.channel_is_ref,
773                event_alias: s.event_alias.clone(),
774            }),
775            // §λ-L-E Fase 13 — Mobile typed channel reductions.
776            FlowStep::Emit(s) => IRFlowNode::Emit(IREmit {
777                node_type: "emit",
778                source_line: s.loc.line,
779                source_column: s.loc.column,
780                channel_ref: s.channel_ref.clone(),
781                value_ref: s.value_ref.clone(),
782                value_is_channel: self.channel_names.contains(&s.value_ref),
783            }),
784            FlowStep::Publish(s) => IRFlowNode::Publish(IRPublish {
785                node_type: "publish",
786                source_line: s.loc.line,
787                source_column: s.loc.column,
788                channel_ref: s.channel_ref.clone(),
789                shield_ref: s.shield_ref.clone(),
790            }),
791            FlowStep::Discover(s) => IRFlowNode::Discover(IRDiscover {
792                node_type: "discover",
793                source_line: s.loc.line,
794                source_column: s.loc.column,
795                capability_ref: s.capability_ref.clone(),
796                alias: s.alias.clone(),
797            }),
798            FlowStep::DaemonStep(s) => IRFlowNode::DaemonStep(IRDaemonStepNode {
799                node_type: "daemon",
800                source_line: s.loc.line,
801                source_column: s.loc.column,
802                daemon_ref: s.daemon_ref.clone(),
803            }),
804            FlowStep::Persist(s) => IRFlowNode::Persist(IRPersistStep {
805                node_type: "persist",
806                source_line: s.loc.line,
807                source_column: s.loc.column,
808                store_name: s.store_name.clone(),
809                fields: s.fields.clone(),
810            }),
811            FlowStep::Retrieve(s) => IRFlowNode::Retrieve(IRRetrieveStep {
812                node_type: "retrieve",
813                source_line: s.loc.line,
814                source_column: s.loc.column,
815                store_name: s.store_name.clone(),
816                where_expr: s.where_expr.clone(),
817                alias: s.alias.clone(),
818            }),
819            FlowStep::Mutate(s) => IRFlowNode::Mutate(IRMutateStep {
820                node_type: "mutate",
821                source_line: s.loc.line,
822                source_column: s.loc.column,
823                store_name: s.store_name.clone(),
824                where_expr: s.where_expr.clone(),
825                fields: s.fields.clone(),
826            }),
827            FlowStep::Purge(s) => IRFlowNode::Purge(IRPurgeStep {
828                node_type: "purge",
829                source_line: s.loc.line,
830                source_column: s.loc.column,
831                store_name: s.store_name.clone(),
832                where_expr: s.where_expr.clone(),
833            }),
834            FlowStep::Transact(s) => IRFlowNode::Transact(IRTransactBlock {
835                node_type: "transact",
836                source_line: s.loc.line,
837                source_column: s.loc.column,
838            }),
839            FlowStep::GenericStep(_) => {
840                // Should not occur — all flow steps have dedicated handlers
841                IRFlowNode::Step(IRStep {
842                    node_type: "step",
843                    source_line: 0,
844                    source_column: 0,
845                    name: String::new(),
846                    persona_ref: String::new(),
847                    given: String::new(),
848                    ask: String::new(),
849                    use_tool: None,
850                    probe: None,
851                    reason: None,
852                    weave: None,
853                    output_type: String::new(),
854                    confidence_floor: None,
855                    navigate_ref: String::new(),
856                    apply_ref: String::new(),
857                    body: Vec::new(),
858                })
859            }
860        }
861    }
862
863    fn compute_execution_levels(
864        &self,
865        steps: &[IRFlowNode],
866        edges: &[IRDataEdge],
867    ) -> Vec<Vec<String>> {
868        // Extract Step-only names for DAG computation
869        let step_nodes: Vec<&IRStep> = steps
870            .iter()
871            .filter_map(|n| {
872                if let IRFlowNode::Step(s) = n {
873                    Some(s)
874                } else {
875                    None
876                }
877            })
878            .collect();
879
880        if step_nodes.is_empty() {
881            return Vec::new();
882        }
883
884        // Build dependency map
885        let mut deps: HashMap<String, Vec<String>> = HashMap::new();
886        for step in &step_nodes {
887            deps.insert(step.name.clone(), Vec::new());
888        }
889        for edge in edges {
890            deps.entry(edge.target_step.clone())
891                .or_default()
892                .push(edge.source_step.clone());
893        }
894
895        let mut levels: Vec<Vec<String>> = Vec::new();
896        let mut placed: Vec<String> = Vec::new();
897
898        loop {
899            let mut level: Vec<String> = Vec::new();
900            for step in &step_nodes {
901                if placed.contains(&step.name) {
902                    continue;
903                }
904                let step_deps = deps.get(&step.name).cloned().unwrap_or_default();
905                if step_deps.iter().all(|d| placed.contains(d)) {
906                    level.push(step.name.clone());
907                }
908            }
909            if level.is_empty() {
910                break;
911            }
912            placed.extend(level.clone());
913            levels.push(level);
914        }
915
916        levels
917    }
918
919    // ── Tier 2 visitors ───────────────────────────────────────────
920
921    fn visit_agent(&self, n: &AgentDefinition) -> IRAgent {
922        IRAgent {
923            node_type: "agent",
924            source_line: n.loc.line,
925            source_column: n.loc.column,
926            name: n.name.clone(),
927            goal: n.goal.clone(),
928            tools: n.tools.clone(),
929            memory_ref: n.memory_ref.clone(),
930            strategy: n.strategy.clone(),
931            on_stuck: n.on_stuck.clone(),
932            shield_ref: n.shield_ref.clone(),
933            max_iterations: n.max_iterations,
934            max_tokens: n.max_tokens,
935            max_time: n.max_time.clone(),
936            max_cost: n.max_cost,
937        }
938    }
939
940    fn visit_shield(&self, n: &ShieldDefinition) -> IRShield {
941        // §8.2.h — Python parity: strategy defaults "pattern"; Option<T> collapses to concrete zeros.
942        let strategy = if n.strategy.is_empty() {
943            "pattern".to_string()
944        } else {
945            n.strategy.clone()
946        };
947        IRShield {
948            node_type: "shield",
949            source_line: n.loc.line,
950            source_column: n.loc.column,
951            name: n.name.clone(),
952            scan: n.scan.clone(),
953            strategy,
954            on_breach: n.on_breach.clone(),
955            severity: n.severity.clone(),
956            quarantine: n.quarantine.clone(),
957            max_retries: n.max_retries.unwrap_or(0),
958            confidence_threshold: n.confidence_threshold.unwrap_or(0.0),
959            allow_tools: n.allow_tools.clone(),
960            deny_tools: n.deny_tools.clone(),
961            sandbox: n.sandbox.unwrap_or(false),
962            redact: n.redact.clone(),
963            log: n.log.clone(),
964            deflect_message: n.deflect_message.clone(),
965            taint: n.taint.clone(),
966            compliance: n.compliance.clone(),
967        }
968    }
969
970    fn visit_pix(&self, n: &PixDefinition) -> IRPix {
971        IRPix {
972            node_type: "pix",
973            source_line: n.loc.line,
974            source_column: n.loc.column,
975            name: n.name.clone(),
976            source: n.source.clone(),
977            depth: n.depth,
978            branching: n.branching,
979            model: n.model.clone(),
980        }
981    }
982
983    fn visit_psyche(&self, n: &PsycheDefinition) -> IRPsyche {
984        IRPsyche {
985            node_type: "psyche",
986            source_line: n.loc.line,
987            source_column: n.loc.column,
988            name: n.name.clone(),
989            dimensions: n.dimensions.clone(),
990            manifold_noise: n.manifold_noise,
991            manifold_momentum: n.manifold_momentum,
992            safety_constraints: n.safety_constraints.clone(),
993            quantum_enabled: n.quantum_enabled,
994            inference_mode: n.inference_mode.clone(),
995        }
996    }
997
998    fn visit_corpus(&self, n: &CorpusDefinition) -> IRCorpus {
999        IRCorpus {
1000            node_type: "corpus",
1001            source_line: n.loc.line,
1002            source_column: n.loc.column,
1003            name: n.name.clone(),
1004            documents: n.documents.clone(),
1005            mcp_server: n.mcp_server.clone(),
1006            mcp_resource_uri: n.mcp_resource_uri.clone(),
1007        }
1008    }
1009
1010    fn visit_dataspace(&self, n: &DataspaceDefinition) -> IRDataspace {
1011        IRDataspace {
1012            node_type: "dataspace",
1013            source_line: n.loc.line,
1014            source_column: n.loc.column,
1015            name: n.name.clone(),
1016        }
1017    }
1018
1019    fn visit_ots(&self, n: &OtsDefinition) -> IROts {
1020        IROts {
1021            node_type: "ots",
1022            source_line: n.loc.line,
1023            source_column: n.loc.column,
1024            name: n.name.clone(),
1025            teleology: n.teleology.clone(),
1026            homotopy_search: n.homotopy_search.clone(),
1027            loss_function: n.loss_function.clone(),
1028        }
1029    }
1030
1031    fn visit_mandate(&self, n: &MandateDefinition) -> IRMandate {
1032        IRMandate {
1033            node_type: "mandate",
1034            source_line: n.loc.line,
1035            source_column: n.loc.column,
1036            name: n.name.clone(),
1037            constraint: n.constraint.clone(),
1038            kp: n.kp,
1039            ki: n.ki,
1040            kd: n.kd,
1041            tolerance: n.tolerance,
1042            max_steps: n.max_steps,
1043            on_violation: n.on_violation.clone(),
1044        }
1045    }
1046
1047    fn visit_compute(&self, n: &ComputeDefinition) -> IRCompute {
1048        IRCompute {
1049            node_type: "compute",
1050            source_line: n.loc.line,
1051            source_column: n.loc.column,
1052            name: n.name.clone(),
1053            shield_ref: n.shield_ref.clone(),
1054        }
1055    }
1056
1057    fn visit_daemon(&self, n: &DaemonDefinition) -> IRDaemon {
1058        IRDaemon {
1059            node_type: "daemon",
1060            source_line: n.loc.line,
1061            source_column: n.loc.column,
1062            name: n.name.clone(),
1063            goal: n.goal.clone(),
1064            tools: n.tools.clone(),
1065            memory_ref: n.memory_ref.clone(),
1066            strategy: n.strategy.clone(),
1067            on_stuck: n.on_stuck.clone(),
1068            shield_ref: n.shield_ref.clone(),
1069            max_tokens: n.max_tokens,
1070            max_time: n.max_time.clone(),
1071            max_cost: n.max_cost,
1072        }
1073    }
1074
1075    fn visit_axonstore(&self, n: &AxonStoreDefinition) -> IRAxonStore {
1076        IRAxonStore {
1077            node_type: "axonstore",
1078            source_line: n.loc.line,
1079            source_column: n.loc.column,
1080            name: n.name.clone(),
1081            backend: n.backend.clone(),
1082            connection: n.connection.clone(),
1083            confidence_floor: n.confidence_floor,
1084            isolation: n.isolation.clone(),
1085            on_breach: n.on_breach.clone(),
1086            capability: n.capability.clone(),
1087            // §Fase 38.b (D1) — thread the parsed column-schema
1088            // declaration (if any) through to the IR. The IR mirror
1089            // preserves the tagged-union shape (inline / manifest_ref /
1090            // env_var) and the canonical PascalCase column-type name.
1091            column_schema: n.column_schema.as_ref().map(lower_column_schema),
1092        }
1093    }
1094
1095    /// §Fase 53.b — lower an `extension` declaration to its IR mirror.
1096    /// Pure structural lowering; the category/no-shadowing/provenance
1097    /// invariants are enforced by the §53.c type-checker before this IR
1098    /// is consumed by §53.d PCC.
1099    fn visit_extension(&self, n: &crate::ast::ExtensionDefinition) -> crate::ir_nodes::IRExtension {
1100        crate::ir_nodes::IRExtension {
1101            node_type: "extension",
1102            source_line: n.loc.line,
1103            source_column: n.loc.column,
1104            name: n.name.clone(),
1105            category: n.category.clone(),
1106            members: n
1107                .members
1108                .iter()
1109                .map(|m| crate::ir_nodes::IRExtensionMember {
1110                    name: m.name.clone(),
1111                    semantics: m.semantics.clone(),
1112                    default_confidence: m.default_confidence,
1113                })
1114                .collect(),
1115        }
1116    }
1117
1118    fn visit_axonendpoint(&self, n: &AxonEndpointDefinition) -> IRAxonEndpoint {
1119        // §8.2.h — Python emits `node_type: "endpoint"`; retries collapses Option<i64> → i64.
1120        IRAxonEndpoint {
1121            node_type: "endpoint",
1122            source_line: n.loc.line,
1123            source_column: n.loc.column,
1124            name: n.name.clone(),
1125            method: n.method.clone(),
1126            path: n.path.clone(),
1127            body_type: n.body_type.clone(),
1128            execute_flow: n.execute_flow.clone(),
1129            output_type: n.output_type.clone(),
1130            shield_ref: n.shield_ref.clone(),
1131            retries: n.retries.unwrap_or(0),
1132            timeout: n.timeout.clone(),
1133            compliance: n.compliance.clone(),
1134            // §Fase 37.y (D1) — IR mirror of `AxonEndpointDefinition.path_params`.
1135            // Direct clone (Vec<String>); the IR JSON omits the field
1136            // when the path has no placeholders (D5 backwards-compat).
1137            path_params: n.path_params.clone(),
1138            // §Fase 37.y (D2) — Lower each AST `TypeField` to an
1139            // `IRTypeField`. The catalog validation already happened
1140            // at parse time; the IR layer just shape-translates.
1141            query_params: n
1142                .query_params
1143                .iter()
1144                .map(|f| crate::ir_nodes::IRTypeField {
1145                    node_type: "type_field",
1146                    source_line: f.loc.line,
1147                    source_column: f.loc.column,
1148                    name: f.name.clone(),
1149                    type_name: f.type_expr.name.clone(),
1150                    generic_param: f.type_expr.generic_param.clone(),
1151                    optional: f.type_expr.optional,
1152                })
1153                .collect(),
1154            // §Fase 51.x — lower the `requires:` capability scopes into
1155            // the IR so the PCC CapabilityContainment property can read
1156            // them. Direct clone; IR JSON omits the field when empty
1157            // (D5 backwards-compat).
1158            requires_capabilities: n.requires_capabilities.clone(),
1159        }
1160    }
1161
1162    /// §λ-L-E Fase 1 — Resource IR lowering.
1163    fn visit_resource(&self, n: &ResourceDefinition) -> IRResource {
1164        IRResource {
1165            node_type: "resource",
1166            source_line: n.loc.line,
1167            source_column: n.loc.column,
1168            name: n.name.clone(),
1169            kind: n.kind.clone(),
1170            endpoint: n.endpoint.clone(),
1171            capacity: n.capacity,
1172            lifetime: n.lifetime.clone(),
1173            certainty_floor: n.certainty_floor,
1174            shield_ref: n.shield_ref.clone(),
1175        }
1176    }
1177
1178    /// §λ-L-E Fase 1 — Fabric IR lowering.
1179    fn visit_fabric(&self, n: &FabricDefinition) -> IRFabric {
1180        IRFabric {
1181            node_type: "fabric",
1182            source_line: n.loc.line,
1183            source_column: n.loc.column,
1184            name: n.name.clone(),
1185            provider: n.provider.clone(),
1186            region: n.region.clone(),
1187            zones: n.zones,
1188            ephemeral: n.ephemeral,
1189            shield_ref: n.shield_ref.clone(),
1190        }
1191    }
1192
1193    /// §λ-L-E Fase 1 — Manifest IR lowering.
1194    fn visit_manifest(&self, n: &ManifestDefinition) -> IRManifest {
1195        IRManifest {
1196            node_type: "manifest",
1197            source_line: n.loc.line,
1198            source_column: n.loc.column,
1199            name: n.name.clone(),
1200            resources: n.resources.clone(),
1201            fabric_ref: n.fabric_ref.clone(),
1202            region: n.region.clone(),
1203            zones: n.zones,
1204            compliance: n.compliance.clone(),
1205        }
1206    }
1207
1208    /// §λ-L-E Fase 1 — Observe IR lowering.
1209    fn visit_observe(&self, n: &ObserveDefinition) -> IRObserve {
1210        IRObserve {
1211            node_type: "observe",
1212            source_line: n.loc.line,
1213            source_column: n.loc.column,
1214            name: n.name.clone(),
1215            target: n.target.clone(),
1216            sources: n.sources.clone(),
1217            quorum: n.quorum,
1218            timeout: n.timeout.clone(),
1219            on_partition: if n.on_partition.is_empty() {
1220                "fail".to_string()
1221            } else {
1222                n.on_partition.clone()
1223            },
1224            certainty_floor: n.certainty_floor,
1225        }
1226    }
1227
1228    /// §λ-L-E Fase 3 — Reconcile IR lowering.
1229    fn visit_reconcile(&self, n: &ReconcileDefinition) -> IRReconcile {
1230        IRReconcile {
1231            node_type: "reconcile",
1232            source_line: n.loc.line,
1233            source_column: n.loc.column,
1234            name: n.name.clone(),
1235            observe_ref: n.observe_ref.clone(),
1236            threshold: n.threshold,
1237            tolerance: n.tolerance,
1238            on_drift: if n.on_drift.is_empty() {
1239                "provision".to_string()
1240            } else {
1241                n.on_drift.clone()
1242            },
1243            shield_ref: n.shield_ref.clone(),
1244            mandate_ref: n.mandate_ref.clone(),
1245            max_retries: n.max_retries,
1246        }
1247    }
1248
1249    /// §λ-L-E Fase 3 — Lease IR lowering.
1250    fn visit_lease(&self, n: &LeaseDefinition) -> IRLease {
1251        IRLease {
1252            node_type: "lease",
1253            source_line: n.loc.line,
1254            source_column: n.loc.column,
1255            name: n.name.clone(),
1256            resource_ref: n.resource_ref.clone(),
1257            duration: n.duration.clone(),
1258            acquire: if n.acquire.is_empty() {
1259                "on_start".to_string()
1260            } else {
1261                n.acquire.clone()
1262            },
1263            on_expire: if n.on_expire.is_empty() {
1264                "anchor_breach".to_string()
1265            } else {
1266                n.on_expire.clone()
1267            },
1268        }
1269    }
1270
1271    /// §λ-L-E Fase 5 — Immune IR lowering.
1272    fn visit_immune(&self, n: &ImmuneDefinition) -> IRImmune {
1273        IRImmune {
1274            node_type: "immune",
1275            source_line: n.loc.line,
1276            source_column: n.loc.column,
1277            name: n.name.clone(),
1278            watch: n.watch.clone(),
1279            sensitivity: n.sensitivity,
1280            baseline: if n.baseline.is_empty() {
1281                "learned".to_string()
1282            } else {
1283                n.baseline.clone()
1284            },
1285            window: n.window,
1286            scope: n.scope.clone(),
1287            tau: n.tau.clone(),
1288            decay: if n.decay.is_empty() {
1289                "exponential".to_string()
1290            } else {
1291                n.decay.clone()
1292            },
1293        }
1294    }
1295
1296    /// §λ-L-E Fase 5 — Reflex IR lowering.
1297    fn visit_reflex(&self, n: &ReflexDefinition) -> IRReflex {
1298        IRReflex {
1299            node_type: "reflex",
1300            source_line: n.loc.line,
1301            source_column: n.loc.column,
1302            name: n.name.clone(),
1303            trigger: n.trigger.clone(),
1304            on_level: if n.on_level.is_empty() {
1305                "doubt".to_string()
1306            } else {
1307                n.on_level.clone()
1308            },
1309            action: n.action.clone(),
1310            scope: n.scope.clone(),
1311            sla: n.sla.clone(),
1312        }
1313    }
1314
1315    /// §λ-L-E Fase 5 — Heal IR lowering.
1316    fn visit_heal(&self, n: &HealDefinition) -> IRHeal {
1317        IRHeal {
1318            node_type: "heal",
1319            source_line: n.loc.line,
1320            source_column: n.loc.column,
1321            name: n.name.clone(),
1322            source: n.source.clone(),
1323            on_level: if n.on_level.is_empty() {
1324                "doubt".to_string()
1325            } else {
1326                n.on_level.clone()
1327            },
1328            mode: if n.mode.is_empty() {
1329                "human_in_loop".to_string()
1330            } else {
1331                n.mode.clone()
1332            },
1333            scope: n.scope.clone(),
1334            review_sla: n.review_sla.clone(),
1335            shield_ref: n.shield_ref.clone(),
1336            max_patches: n.max_patches,
1337        }
1338    }
1339
1340    /// §λ-L-E Fase 9 — Component IR lowering.
1341    fn visit_component(&self, n: &ComponentDefinition) -> IRComponent {
1342        IRComponent {
1343            node_type: "component",
1344            source_line: n.loc.line,
1345            source_column: n.loc.column,
1346            name: n.name.clone(),
1347            renders: n.renders.clone(),
1348            via_shield: n.via_shield.clone(),
1349            on_interact: n.on_interact.clone(),
1350            render_hint: if n.render_hint.is_empty() {
1351                "custom".to_string()
1352            } else {
1353                n.render_hint.clone()
1354            },
1355        }
1356    }
1357
1358    /// §λ-L-E Fase 9 — View IR lowering.
1359    fn visit_view(&self, n: &ViewDefinition) -> IRView {
1360        IRView {
1361            node_type: "view",
1362            source_line: n.loc.line,
1363            source_column: n.loc.column,
1364            name: n.name.clone(),
1365            title: n.title.clone(),
1366            components: n.components.clone(),
1367            route: n.route.clone(),
1368        }
1369    }
1370
1371    /// §λ-L-E Fase 4 — Session IR lowering.
1372    fn visit_session(&self, n: &SessionDefinition) -> IRSession {
1373        let roles = n
1374            .roles
1375            .iter()
1376            .map(|r| IRSessionRole {
1377                node_type: "session_role",
1378                source_line: r.loc.line,
1379                source_column: r.loc.column,
1380                name: r.name.clone(),
1381                steps: r
1382                    .steps
1383                    .iter()
1384                    .map(|s| self.lower_session_step_ir(s))
1385                    .collect(),
1386            })
1387            .collect();
1388        IRSession {
1389            node_type: "session",
1390            source_line: n.loc.line,
1391            source_column: n.loc.column,
1392            name: n.name.clone(),
1393            roles,
1394        }
1395    }
1396
1397    /// §Fase 41.b — recursively lower a session step, including the nested
1398    /// `select`/`branch` choice sub-protocols (each branch is its own ordered
1399    /// step sequence). Mirrors the AST `SessionStep`/`SessionBranch` shape.
1400    fn lower_session_step_ir(&self, s: &SessionStep) -> IRSessionStep {
1401        IRSessionStep {
1402            node_type: "session_step",
1403            source_line: s.loc.line,
1404            source_column: s.loc.column,
1405            op: s.op.clone(),
1406            message_type: s.message_type.clone(),
1407            branches: s
1408                .branches
1409                .iter()
1410                .map(|b| IRSessionBranch {
1411                    node_type: "session_branch",
1412                    label: b.label.clone(),
1413                    steps: b
1414                        .steps
1415                        .iter()
1416                        .map(|st| self.lower_session_step_ir(st))
1417                        .collect(),
1418                })
1419                .collect(),
1420        }
1421    }
1422
1423    /// §λ-L-E Fase 4 — Topology IR lowering.
1424    fn visit_topology(&self, n: &TopologyDefinition) -> IRTopology {
1425        IRTopology {
1426            node_type: "topology",
1427            source_line: n.loc.line,
1428            source_column: n.loc.column,
1429            name: n.name.clone(),
1430            nodes: n.nodes.clone(),
1431            edges: n
1432                .edges
1433                .iter()
1434                .map(|e| IRTopologyEdge {
1435                    node_type: "topology_edge",
1436                    source_line: e.loc.line,
1437                    source_column: e.loc.column,
1438                    source: e.source.clone(),
1439                    target: e.target.clone(),
1440                    session_ref: e.session_ref.clone(),
1441                })
1442                .collect(),
1443        }
1444    }
1445
1446    /// §λ-L-E Fase 3 — Ensemble IR lowering.
1447    fn visit_ensemble(&self, n: &EnsembleDefinition) -> IREnsemble {
1448        IREnsemble {
1449            node_type: "ensemble",
1450            source_line: n.loc.line,
1451            source_column: n.loc.column,
1452            name: n.name.clone(),
1453            observations: n.observations.clone(),
1454            quorum: n.quorum,
1455            aggregation: if n.aggregation.is_empty() {
1456                "majority".to_string()
1457            } else {
1458                n.aggregation.clone()
1459            },
1460            certainty_mode: if n.certainty_mode.is_empty() {
1461                "min".to_string()
1462            } else {
1463                n.certainty_mode.clone()
1464            },
1465        }
1466    }
1467
1468    fn visit_lambda_data(&self, n: &LambdaDataDefinition) -> IRLambdaData {
1469        IRLambdaData {
1470            node_type: "lambda_data",
1471            source_line: n.loc.line,
1472            source_column: n.loc.column,
1473            name: n.name.clone(),
1474            ontology: n.ontology.clone(),
1475            certainty: n.certainty,
1476            temporal_frame_start: n.temporal_frame_start.clone(),
1477            temporal_frame_end: n.temporal_frame_end.clone(),
1478            provenance: n.provenance.clone(),
1479            derivation: n.derivation.clone(),
1480        }
1481    }
1482
1483    fn visit_run(&self, n: &RunStatement) -> IRRun {
1484        IRRun {
1485            node_type: "run",
1486            source_line: n.loc.line,
1487            source_column: n.loc.column,
1488            flow_name: n.flow_name.clone(),
1489            arguments: n.arguments.clone(),
1490            persona_name: n.persona.clone(),
1491            context_name: n.context.clone(),
1492            anchor_names: n.anchors.clone(),
1493            on_failure: n.on_failure.clone(),
1494            on_failure_params: n
1495                .on_failure_params
1496                .iter()
1497                .map(|(k, v)| vec![k.clone(), v.clone()])
1498                .collect(),
1499            output_to: n.output_to.clone(),
1500            effort: n.effort.clone(),
1501            resolved_flow: None,
1502            resolved_persona: None,
1503            resolved_context: None,
1504            resolved_anchors: Vec::new(),
1505        }
1506    }
1507
1508    // ──────────────────────────────────────────────────────────────────
1509    //  §λ-L-E Fase 13 — Mobile Typed Channels (paper_mobile_channels.md)
1510    //  Declarative channels lower to IRChannel; emit/publish/discover
1511    //  are step-level reductions handled in `visit_flow_step`.
1512    // ──────────────────────────────────────────────────────────────────
1513
1514    fn visit_channel(&self, n: &ChannelDefinition) -> IRChannel {
1515        IRChannel {
1516            node_type: "channel",
1517            source_line: n.loc.line,
1518            source_column: n.loc.column,
1519            name: n.name.clone(),
1520            message: n.message.clone(),
1521            qos: n.qos.clone(),
1522            lifetime: n.lifetime.clone(),
1523            persistence: n.persistence.clone(),
1524            shield_ref: n.shield_ref.clone(),
1525        }
1526    }
1527
1528    /// §Fase 41.b — compile a `socket` to its IR (the typed-WS transport
1529    /// binding; axon-rs realises the endpoint from this).
1530    fn visit_socket(&self, n: &SocketDefinition) -> IRSocket {
1531        IRSocket {
1532            node_type: "socket",
1533            source_line: n.loc.line,
1534            source_column: n.loc.column,
1535            name: n.name.clone(),
1536            protocol: n.protocol.clone(),
1537            backpressure_credit: n.backpressure_credit,
1538            reconnect: n.reconnect,
1539            legal_basis: n.legal_basis.clone(),
1540        }
1541    }
1542}
1543
1544// ── §λ-L-E Fase 13 — Mobile Typed Channels IR generator tests ───────────────
1545
1546#[cfg(test)]
1547mod fase13_ir_tests {
1548    use super::*;
1549    use crate::lexer::Lexer;
1550    use crate::parser::Parser;
1551
1552    fn compile(src: &str) -> IRProgram {
1553        let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
1554        let prog = Parser::new(tokens).parse().expect("parse");
1555        IRGenerator::new().generate(&prog)
1556    }
1557
1558    #[test]
1559    fn channel_lowered_with_all_fields() {
1560        let src = r#"
1561            type Order { id: String }
1562            shield Gate { scan: [pii_leak] }
1563            channel C { message: Order qos: at_least_once lifetime: affine persistence: ephemeral shield: Gate }
1564        "#;
1565        let ir = compile(src);
1566        assert_eq!(ir.channels.len(), 1);
1567        let c = &ir.channels[0];
1568        assert_eq!(c.name, "C");
1569        assert_eq!(c.message, "Order");
1570        assert_eq!(c.qos, "at_least_once");
1571        assert_eq!(c.lifetime, "affine");
1572        assert_eq!(c.persistence, "ephemeral");
1573        assert_eq!(c.shield_ref, "Gate");
1574    }
1575
1576    #[test]
1577    fn channel_second_order_message_preserved() {
1578        let ir = compile(
1579            r#"
1580            type Order { id: String }
1581            channel C1 { message: Order }
1582            channel C2 { message: Channel<Order> }
1583            channel C3 { message: Channel<Channel<Order>> }
1584        "#,
1585        );
1586        let names_to_msgs: std::collections::HashMap<_, _> = ir
1587            .channels
1588            .iter()
1589            .map(|c| (c.name.clone(), c.message.clone()))
1590            .collect();
1591        assert_eq!(names_to_msgs.get("C1"), Some(&"Order".to_string()));
1592        assert_eq!(names_to_msgs.get("C2"), Some(&"Channel<Order>".to_string()));
1593        assert_eq!(
1594            names_to_msgs.get("C3"),
1595            Some(&"Channel<Channel<Order>>".to_string())
1596        );
1597    }
1598
1599    #[test]
1600    fn emit_value_is_channel_resolves_at_lowering() {
1601        let ir = compile(
1602            r#"
1603            type Order { id: String }
1604            channel Inner { message: Order }
1605            channel Outer { message: Channel<Order> }
1606            flow f() -> O { emit Outer(Inner) }
1607        "#,
1608        );
1609        let flow = &ir.flows[0];
1610        match &flow.steps[0] {
1611            IRFlowNode::Emit(e) => {
1612                assert_eq!(e.channel_ref, "Outer");
1613                assert_eq!(e.value_ref, "Inner");
1614                assert!(e.value_is_channel, "Inner is a registered channel");
1615            }
1616            other => panic!("expected Emit, got {:?}", other),
1617        }
1618    }
1619
1620    #[test]
1621    fn emit_scalar_payload_value_is_channel_false() {
1622        let ir = compile(
1623            r#"
1624            type Order { id: String }
1625            channel Out { message: Order }
1626            flow f() -> O { emit Out(payload) }
1627        "#,
1628        );
1629        let flow = &ir.flows[0];
1630        match &flow.steps[0] {
1631            IRFlowNode::Emit(e) => {
1632                assert!(!e.value_is_channel, "scalar payload");
1633            }
1634            other => panic!("expected Emit, got {:?}", other),
1635        }
1636    }
1637
1638    #[test]
1639    fn publish_lowered_with_shield_ref() {
1640        let ir = compile(
1641            r#"
1642            type Order { id: String }
1643            shield Gate { scan: [pii_leak] }
1644            channel C { message: Order shield: Gate }
1645            flow f() -> Cap { publish C within Gate }
1646        "#,
1647        );
1648        match &ir.flows[0].steps[0] {
1649            IRFlowNode::Publish(p) => {
1650                assert_eq!(p.channel_ref, "C");
1651                assert_eq!(p.shield_ref, "Gate");
1652            }
1653            other => panic!("expected Publish, got {:?}", other),
1654        }
1655    }
1656
1657    #[test]
1658    fn discover_lowered_with_alias() {
1659        let ir = compile(
1660            r#"
1661            type Order { id: String }
1662            shield Gate { scan: [pii_leak] }
1663            channel C { message: Order shield: Gate }
1664            flow f() -> O { discover C as ch }
1665        "#,
1666        );
1667        match &ir.flows[0].steps[0] {
1668            IRFlowNode::Discover(d) => {
1669                assert_eq!(d.capability_ref, "C");
1670                assert_eq!(d.alias, "ch");
1671            }
1672            other => panic!("expected Discover, got {:?}", other),
1673        }
1674    }
1675
1676    #[test]
1677    fn json_serialization_works() {
1678        let ir = compile(
1679            r#"
1680            type Order { id: String }
1681            channel C { message: Order }
1682            flow f() -> O { emit C(payload) }
1683        "#,
1684        );
1685        let json = serde_json::to_string(&ir).expect("serialize");
1686        assert!(json.contains(r#""node_type":"channel""#));
1687        assert!(json.contains(r#""node_type":"emit""#));
1688        assert!(json.contains(r#""value_is_channel":false"#));
1689    }
1690}
1691
1692#[cfg(test)]
1693mod fase19_ir_tests {
1694    //! Fase 19.e — Rust mirror of break/continue keywords. The Python
1695    //! frontend already lowers BreakStatement → IRBreak and
1696    //! ContinueStatement → IRContinue (see Fase 19.e Python commit);
1697    //! these tests guard the Rust side at the IR-generator boundary
1698    //! so cross-stack parity goldens (Fase 19.h) compare on aligned
1699    //! shapes.
1700
1701    use super::*;
1702    use crate::ir_nodes::IRFlowNode;
1703    use crate::lexer::Lexer;
1704    use crate::parser::Parser;
1705
1706    /// Compile a minimal flow whose for-in body is the supplied
1707    /// snippet, and return the body's IR list.
1708    fn for_body_ir(body_src: &str) -> Vec<IRFlowNode> {
1709        let src = format!(
1710            "flow Probe() -> Out {{ for x in items.list {{ {body_src} }} }}"
1711        );
1712        let tokens = Lexer::new(&src, "<test>").tokenize().expect("lex");
1713        let prog = Parser::new(tokens).parse().expect("parse");
1714        let ir = IRGenerator::new().generate(&prog);
1715        let flow = ir
1716            .flows
1717            .iter()
1718            .find(|f| f.name == "Probe")
1719            .expect("flow Probe in IR");
1720        match flow.steps.first().expect("flow has at least one step") {
1721            IRFlowNode::ForIn(inner) => inner.body.clone(),
1722            other => panic!("expected ForIn, got {other:?}"),
1723        }
1724    }
1725
1726    #[test]
1727    fn break_keyword_lowers_to_ir_break() {
1728        let body = for_body_ir("break");
1729        assert_eq!(body.len(), 1);
1730        match &body[0] {
1731            IRFlowNode::Break(b) => assert_eq!(b.node_type, "break"),
1732            other => panic!("expected IRFlowNode::Break, got {other:?}"),
1733        }
1734    }
1735
1736    #[test]
1737    fn continue_keyword_lowers_to_ir_continue() {
1738        let body = for_body_ir("continue");
1739        assert_eq!(body.len(), 1);
1740        match &body[0] {
1741            IRFlowNode::Continue(c) => assert_eq!(c.node_type, "continue"),
1742            other => panic!("expected IRFlowNode::Continue, got {other:?}"),
1743        }
1744    }
1745
1746    #[test]
1747    fn break_outside_loop_rejected_by_parser() {
1748        // A flow with `break` at the top level (not inside a for-in)
1749        // must fail to parse — the loop_depth scope check rejects it.
1750        let src = "flow F() -> Out { break }";
1751        let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
1752        let result = Parser::new(tokens).parse();
1753        assert!(result.is_err(), "parser must reject break outside loop");
1754    }
1755
1756    #[test]
1757    fn continue_outside_loop_rejected_by_parser() {
1758        let src = "flow F() -> Out { continue }";
1759        let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
1760        let result = Parser::new(tokens).parse();
1761        assert!(result.is_err(), "parser must reject continue outside loop");
1762    }
1763
1764    #[test]
1765    fn break_continue_serialize_with_node_type_field() {
1766        let body = for_body_ir("break\ncontinue");
1767        let json = serde_json::to_string(&body).expect("serialize");
1768        assert!(json.contains(r#""node_type":"break""#));
1769        assert!(json.contains(r#""node_type":"continue""#));
1770    }
1771}
1772
1773// ════════════════════════════════════════════════════════════════════
1774//  §Fase 37.y.3 — IR mirror for path_params + query_params + D5
1775//  IR-JSON byte-identity backwards-compat.
1776// ════════════════════════════════════════════════════════════════════
1777
1778#[cfg(test)]
1779mod fase37y_ir_mirror_tests {
1780    use super::*;
1781    use crate::lexer::Lexer;
1782    use crate::parser::Parser;
1783
1784    fn lower_endpoint(src: &str) -> crate::ir_nodes::IRAxonEndpoint {
1785        let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
1786        let prog = Parser::new(tokens).parse().expect("parse");
1787        let ir = IRGenerator::new().generate(&prog);
1788        ir.endpoints
1789            .into_iter()
1790            .next()
1791            .expect("at least one endpoint in IR")
1792    }
1793
1794    #[test]
1795    fn ir_carries_path_params_from_ast() {
1796        let src = r#"
1797            axonendpoint write {
1798                method: POST
1799                path: "/api/tenants/{tenant_id}/secrets/{secret_name}"
1800                body: SecretWriteRequest
1801                execute: Write
1802            }
1803        "#;
1804        let ep = lower_endpoint(src);
1805        assert_eq!(
1806            ep.path_params,
1807            vec!["tenant_id".to_string(), "secret_name".to_string()],
1808            "IR.path_params mirrors AST.path_params 1:1"
1809        );
1810    }
1811
1812    #[test]
1813    fn ir_carries_query_params_with_type_field_shape() {
1814        let src = r#"
1815            axonendpoint list {
1816                method: GET
1817                path: "/api/users"
1818                query: { status: Text, limit: Int?, after: Uuid? }
1819                execute: ListUsers
1820            }
1821        "#;
1822        let ep = lower_endpoint(src);
1823        assert_eq!(ep.query_params.len(), 3);
1824        assert_eq!(ep.query_params[0].name, "status");
1825        assert_eq!(ep.query_params[0].type_name, "Text");
1826        assert!(!ep.query_params[0].optional);
1827        assert_eq!(ep.query_params[1].name, "limit");
1828        assert_eq!(ep.query_params[1].type_name, "Int");
1829        assert!(ep.query_params[1].optional);
1830        assert_eq!(ep.query_params[2].name, "after");
1831        assert_eq!(ep.query_params[2].type_name, "Uuid");
1832        assert!(ep.query_params[2].optional);
1833        // node_type stays canonical for downstream JSON consumers.
1834        assert_eq!(ep.query_params[0].node_type, "type_field");
1835    }
1836
1837    #[test]
1838    fn d5_byte_identity_when_no_path_or_query() {
1839        // The load-bearing D5 backwards-compat assertion: an endpoint
1840        // with no path placeholders AND no query block produces IR
1841        // JSON byte-identical to the pre-v1.38.5 output. The new
1842        // fields use `skip_serializing_if = Vec::is_empty` so they
1843        // simply don't appear in the JSON.
1844        let src = r#"
1845            axonendpoint hello {
1846                method: GET
1847                path: "/api/hello"
1848                body: HelloRequest
1849                execute: Hello
1850            }
1851        "#;
1852        let ep = lower_endpoint(src);
1853        let json = serde_json::to_string(&ep).expect("serialize");
1854        assert!(
1855            !json.contains("path_params"),
1856            "D5: absent `path_params` key when empty. Got: {json}"
1857        );
1858        assert!(
1859            !json.contains("query_params"),
1860            "D5: absent `query_params` key when empty. Got: {json}"
1861        );
1862    }
1863
1864    #[test]
1865    fn ir_json_emits_path_params_when_present() {
1866        let src = r#"
1867            axonendpoint x {
1868                method: GET
1869                path: "/api/users/{id}"
1870                execute: X
1871            }
1872        "#;
1873        let ep = lower_endpoint(src);
1874        let json = serde_json::to_string(&ep).expect("serialize");
1875        assert!(
1876            json.contains(r#""path_params":["id"]"#),
1877            "path_params present in JSON. Got: {json}"
1878        );
1879    }
1880
1881    #[test]
1882    fn ir_json_emits_query_params_as_type_field_array() {
1883        let src = r#"
1884            axonendpoint x {
1885                method: GET
1886                path: "/api/x"
1887                query: { status: Text? }
1888                execute: X
1889            }
1890        "#;
1891        let ep = lower_endpoint(src);
1892        let json = serde_json::to_string(&ep).expect("serialize");
1893        assert!(json.contains("query_params"), "key present: {json}");
1894        assert!(json.contains(r#""name":"status""#), "field name: {json}");
1895        assert!(json.contains(r#""type_name":"Text""#), "type_name: {json}");
1896        assert!(json.contains(r#""optional":true"#), "optional: {json}");
1897    }
1898
1899    #[test]
1900    fn ir_round_trips_kivi_corpus() {
1901        // The end-to-end combined corpus from the kivi adopter report —
1902        // both 37.y.1 (path) and 37.y.2 (query) round-trip through IR.
1903        let src = r#"
1904            axonendpoint write_secret {
1905                method: POST
1906                path: "/api/tenants/{tenant_id}/secrets/{secret_name}"
1907                query: { dry_run: Bool?, overwrite: Bool? }
1908                body: SecretWriteRequest
1909                execute: WriteSecret
1910            }
1911        "#;
1912        let ep = lower_endpoint(src);
1913        assert_eq!(ep.path_params, vec!["tenant_id", "secret_name"]);
1914        assert_eq!(ep.query_params.len(), 2);
1915        assert_eq!(ep.query_params[0].name, "dry_run");
1916        assert!(ep.query_params[0].optional);
1917        assert_eq!(ep.body_type, "SecretWriteRequest");
1918        assert_eq!(ep.method, "POST");
1919    }
1920}