Skip to main content

axon_frontend/
ir_nodes.rs

1//! AXON IR node definitions — direct port of axon/compiler/ir_nodes.py.
2//!
3//! All nodes serialize to JSON matching the Python IR output format exactly.
4
5#![allow(dead_code)]
6
7use serde::Serialize;
8
9// ── Program root ─────────────────────────────────────────────────────────────
10
11/// §Fase 112.b — `Clone` is additive and every member IR type already derives it.
12/// The Cognitive-I/O supervisor owns the compiled program it drives (it outlives
13/// the deploy call that built it), and a lifetime-bound supervisor would have to be
14/// threaded through `ServerState` for no benefit.
15#[derive(Debug, Clone, Serialize)]
16pub struct IRProgram {
17    pub node_type: &'static str,
18    pub source_line: u32,
19    pub source_column: u32,
20    pub personas: Vec<IRPersona>,
21    pub contexts: Vec<IRContext>,
22    pub anchors: Vec<IRAnchor>,
23    pub tools: Vec<IRToolSpec>,
24    pub memories: Vec<IRMemory>,
25    pub types: Vec<IRType>,
26    pub flows: Vec<IRFlow>,
27    pub runs: Vec<IRRun>,
28    pub imports: Vec<IRImport>,
29    pub agents: Vec<IRAgent>,
30    pub shields: Vec<IRShield>,
31    /// §Fase 71.a — temporal execution-window guards.
32    pub windows: Vec<IRWindow>,
33    /// §Fase 114.a — top-level `budget` declarations. A daemon's anonymous budget
34    /// stays on the daemon; these govern EVERY flow that calls the tools they
35    /// name, including the HTTP endpoints adopters actually deploy.
36    #[serde(default, skip_serializing_if = "Vec::is_empty")]
37    pub budgets: Vec<IRBudget>,
38    pub daemons: Vec<IRDaemon>,
39    pub ots_specs: Vec<IROts>,
40    pub pix_specs: Vec<IRPix>,
41    /// §Fase 62.0 — audit-chain (`ledger`) declarations. Distinct from
42    /// `pix_specs` (the retrieval navigator); a ledger binds a hash-linked
43    /// recorder to an audited surface.
44    pub ledger_specs: Vec<IRLedger>,
45    pub corpus_specs: Vec<IRCorpus>,
46    pub psyche_specs: Vec<IRPsyche>,
47    pub mandate_specs: Vec<IRMandate>,
48    pub lambda_data_specs: Vec<IRLambdaData>,
49    pub compute_specs: Vec<IRCompute>,
50    pub axonstore_specs: Vec<IRAxonStore>,
51    pub endpoints: Vec<IRAxonEndpoint>,
52    /// §Fase 53 — closed-catalog extension declarations (compiled).
53    /// `#[serde(skip)]` so the field is NOT emitted into the IR JSON —
54    /// this keeps the static IR-JSON drift-gate fixtures green without
55    /// regenerating them (the pattern `dataspace_specs` also used until
56    /// §108.b un-skipped it).
57    /// The in-memory field feeds the §53.c type-checker + §53.d PCC (both
58    /// read `&IRProgram`); soundness invariant #1 holds via SOURCE
59    /// re-derivation — both the prover and the verifier read the
60    /// source-derived IR, which carries the extensions. §53.x hardening
61    /// (optional): un-skip + regenerate fixtures + bind extensions into
62    /// the PCC `artifact_digest` (today the digest omits them; the
63    /// witness still binds them by re-derivation). Deterministically
64    /// sorted by `name` at the end of IR generation (§53.b founder
65    /// refinement B) so multi-file declaration order can never perturb
66    /// the proof-bundle hash.
67    #[serde(skip)]
68    pub extensions: Vec<IRExtension>,
69    /// §Fase 108.b — the compiled dataspace schemas, SERIALIZED into the
70    /// IR JSON (un-skipped). History: this field was `#[serde(skip)]` for
71    /// byte-identical parity with the retired Python reference frontend
72    /// (§8.2.h.1) — which meant the runtime literally could not see a
73    /// declared dataspace (the §108 ground-truth finding). The parity
74    /// constraint is gone; the deploy hook walks this field to
75    /// instantiate the deterministic columnar engine's stores. Additive
76    /// for consumers: no IR deserializer uses `deny_unknown_fields`
77    /// (verified 2026-07-12), and `IRProgram` is `Serialize`-only —
78    /// consumers re-derive from source.
79    pub dataspace_specs: Vec<IRDataspace>,
80    /// §λ-L-E Fase 1 — I/O cognitivo primitives (compiled).
81    pub resources: Vec<IRResource>,
82    pub fabrics: Vec<IRFabric>,
83    pub manifests: Vec<IRManifest>,
84    pub observations: Vec<IRObserve>,
85    /// §λ-L-E Fase 1 (Free Monad root) — populated when the program
86    /// declares manifests/observes. `None` ⇒ serialises as `null`
87    /// (matches Python when the field is `None`).
88    pub intention_tree: Option<IRIntentionTree>,
89    /// §λ-L-E Fase 3 — Control cognitivo primitives (compiled).
90    pub reconciles: Vec<IRReconcile>,
91    pub leases: Vec<IRLease>,
92    pub ensembles: Vec<IREnsemble>,
93    /// §λ-L-E Fase 4 — Topology + Session (compiled).
94    pub sessions: Vec<IRSession>,
95    pub topologies: Vec<IRTopology>,
96    /// §λ-L-E Fase 5 — Immune system (compiled).
97    pub immunes: Vec<IRImmune>,
98    pub reflexes: Vec<IRReflex>,
99    pub heals: Vec<IRHeal>,
100    /// §λ-L-E Fase 9 — UI cognitiva declarativa (compiled).
101    pub components: Vec<IRComponent>,
102    pub views: Vec<IRView>,
103    /// §λ-L-E Fase 13 — Mobile typed channels (compiled).
104    pub channels: Vec<IRChannel>,
105    /// §Fase 41.b — typed WebSocket transports (compiled). Each carries its
106    /// referenced `session` protocol + the credit-window backpressure so
107    /// axon-rs can realise the typed endpoint over a `tokio` WebSocket.
108    pub sockets: Vec<IRSocket>,
109    /// §Fase 51.c.2 — Pauli-sum observable declarations (compiled). Each carries
110    /// its real-coefficient × Pauli-string terms so axon-rs can build the
111    /// Hermitian measurement operator `M = Σ cₖ Pₖ` a `quant` block measures
112    /// against. `#[serde(skip)]` (like `extensions` / `dataspace_specs`) so the
113    /// static IR-JSON drift fixtures stay green; the in-memory field feeds the
114    /// §51.c.2 checker + the §51.d/e runtime. The checker resolves
115    /// `quant(observable: …)` against the AST symbol table, not this field.
116    #[serde(skip)]
117    pub observables: Vec<IRObservable>,
118    /// §Fase 69.a — Advantage-Witness declarations. `skip_serializing_if = empty`
119    /// keeps a witness-less program's IR JSON byte-identical (zero IR-SHA drift,
120    /// the §52/§67 pattern); when present it rides the IR to the enterprise
121    /// deploy/runtime evaluator (§69.b+).
122    #[serde(default, skip_serializing_if = "Vec::is_empty")]
123    pub witnesses: Vec<IRWitness>,
124    /// §Fase 80.b — outbound vendor connections (compiled). Each carries its
125    /// axon-facing session binding (`protocol`/`role`), the per-tenant config
126    /// keys (`resolve`/`secret`), the auth handshake, the total wire↔session
127    /// projection (`map`) and the reconnect/overflow policies, so axon-rs can
128    /// dial + transcode without vendor-specific code. `skip_serializing_if =
129    /// empty` keeps an upstream-less program's IR JSON byte-identical (zero
130    /// IR-SHA drift — the standing §76.d discipline).
131    #[serde(default, skip_serializing_if = "Vec::is_empty")]
132    pub upstreams: Vec<IRUpstream>,
133    /// §Fase 83.a — named, referenced browser-origin policies. `skip_serializing_if
134    /// = empty` keeps a cors-less program's IR JSON byte-identical (zero IR-SHA
135    /// drift — the standing §76.d discipline).
136    #[serde(default, skip_serializing_if = "Vec::is_empty")]
137    pub cors_policies: Vec<IRCors>,
138    /// §Fase 85.b — named, referenced result-memoization policies. Same
139    /// `skip_serializing_if = empty` IR-SHA discipline as `cors_policies`.
140    #[serde(default, skip_serializing_if = "Vec::is_empty")]
141    pub caches: Vec<IRCache>,
142    /// §Fase 92.a — ephemeral-credential contracts (`credential { ttl:
143    /// grants: }`), minted at runtime by the `mint` flow verb under the
144    /// attenuation law (`authority_only_attenuates`). Same
145    /// `skip_serializing_if = empty` IR-SHA discipline.
146    #[serde(default, skip_serializing_if = "Vec::is_empty")]
147    pub credentials: Vec<IRCredential>,
148    /// §Fase 87.a — long-horizon autonomous research primitives (compiled). Each
149    /// carries its domain, cognition params, memory binding, compute budget and
150    /// mandates so the enterprise engine (§87.h+) can drive the active-inference
151    /// loop. Same `skip_serializing_if = empty` IR-SHA discipline as `caches`
152    /// (a savant-less program's IR JSON stays byte-identical — zero drift).
153    #[serde(default, skip_serializing_if = "Vec::is_empty")]
154    pub savants: Vec<IRSavant>,
155    /// §Fase 99.b — compiled document declarations. Same
156    /// `skip_serializing_if = empty` IR-SHA discipline (a document-less
157    /// program's IR JSON stays byte-identical). Consumed by the runtime
158    /// `DocumentRenderer` tool + the `DocumentProvenanceSoundness` PCC class.
159    #[serde(default, skip_serializing_if = "Vec::is_empty")]
160    pub documents: Vec<IRDocument>,
161    /// §Fase 105 — compiled CRM delivery declarations. Same
162    /// `skip_serializing_if = empty` IR-SHA discipline (a delivery-less program's
163    /// IR JSON stays byte-identical). Consumed by the runtime delivery dispatch
164    /// (`axon::delivery`) + the `DeliveryProvenanceSoundness` PCC class (T920).
165    #[serde(default, skip_serializing_if = "Vec::is_empty")]
166    pub deliveries: Vec<IRDeliver>,
167    /// §Fase 110 — governed human notifications (the third egress dual).
168    pub notifications: Vec<IRNotify>,
169    /// §Fase 87.d — dynamic tool-synthesis policies (compiled). Same
170    /// `skip_serializing_if = empty` IR-SHA discipline as `savants`.
171    #[serde(default, skip_serializing_if = "Vec::is_empty")]
172    pub synths: Vec<IRSynth>,
173    /// §Fase 88.a — authorization-scope policies (compiled). Same
174    /// `skip_serializing_if = empty` IR-SHA discipline as `synths`.
175    #[serde(default, skip_serializing_if = "Vec::is_empty")]
176    pub scopes: Vec<IRScope>,
177    /// §Fase 23 — algebraic effect declarations (compiled).
178    /// Each declared effect persists into IR so axon-rs can build the
179    /// per-effect operation table at startup. The CPS state graph for
180    /// perform/handle sites lives inline within IRFlow.steps (each
181    /// IRPerform / IRHandlerFrame carries its assigned state_id /
182    /// frame_id).
183    ///
184    /// **§Fase 120 — THIS FIELD IS NOW POPULATED, from `.axon` source.**
185    ///
186    /// It used to be a mirror of the retired Python frontend's
187    /// `IRProgram.effects` field, kept only so a byte-identical structural
188    /// parity gate stayed green against an empty `effects: []`. The Python
189    /// frontend is gone (the 0-`.py` north star) and the field outlived it
190    /// carrying nothing — `axon-frontend` never emitted a single entry, which
191    /// is why `EffectRuntime` was constructible only from its own tests.
192    ///
193    /// §Fase 120 gives it its declarations, from `effect E { … }` in adopter
194    /// source. It is the CLOSED catalog the dispatcher validates a perform
195    /// site's arity against, and the set D120.2 resolves a bare
196    /// `perform Emit(x)` over.
197    ///
198    /// ⚠️ Reusing this field rather than adding one was a deliberate correction
199    /// mid-fase: a parallel `effect_specs` would have made TWO compiled
200    /// catalogs of one concept in one artifact — the §119.m.2 defect (a third
201    /// blame vocabulary) committed knowingly. Ask what already reads a concept
202    /// BEFORE giving it a new home.
203    pub effects: Vec<IREffectDeclaration>,
204    /// §Fase 115.e — per-module provenance of a LINKED program: for every
205    /// module the linker merged, its path, origin file, both EMS hashes and
206    /// the virtual-line window its declarations occupy (the driver renumbers
207    /// each module's lines by a base offset so diagnostics and IR
208    /// `source_line`s stay globally unambiguous — map back with
209    /// `line − line_base`). `skip_serializing_if = empty` keeps every
210    /// single-file program's IR JSON byte-identical (zero IR-SHA drift —
211    /// the standing §76.d discipline).
212    #[serde(default, skip_serializing_if = "Vec::is_empty")]
213    pub modules: Vec<IRModuleProvenance>,
214}
215
216/// §Fase 115.e — one linked module's provenance record: the audit chain
217/// from a deployed multi-module artifact back to its sources.
218#[derive(Debug, Serialize, Clone)]
219pub struct IRModuleProvenance {
220    /// Dotted module path (`axon.security`).
221    pub module: String,
222    /// Display origin (file path or bundle key).
223    pub origin: String,
224    /// SHA-256 of the module's source bytes.
225    pub content_hash: String,
226    /// SHA-256 of the module's `.axi` interface (comment-stable).
227    pub interface_hash: String,
228    /// First virtual line assigned to this module by the link renumbering.
229    pub line_base: u32,
230    /// Number of source lines the module occupies.
231    pub line_count: u32,
232    /// The module's exported declaration names (deterministic order).
233    pub declarations: Vec<String>,
234}
235
236impl IRProgram {
237    pub fn new() -> Self {
238        IRProgram {
239            node_type: "program",
240            source_line: 1,
241            source_column: 1,
242            personas: Vec::new(),
243            contexts: Vec::new(),
244            anchors: Vec::new(),
245            tools: Vec::new(),
246            memories: Vec::new(),
247            types: Vec::new(),
248            flows: Vec::new(),
249            runs: Vec::new(),
250            imports: Vec::new(),
251            agents: Vec::new(),
252            shields: Vec::new(),
253            windows: Vec::new(),
254            budgets: Vec::new(),
255            daemons: Vec::new(),
256            ots_specs: Vec::new(),
257            pix_specs: Vec::new(),
258            ledger_specs: Vec::new(),
259            corpus_specs: Vec::new(),
260            psyche_specs: Vec::new(),
261            mandate_specs: Vec::new(),
262            lambda_data_specs: Vec::new(),
263            compute_specs: Vec::new(),
264            axonstore_specs: Vec::new(),
265            endpoints: Vec::new(),
266            extensions: Vec::new(),
267            dataspace_specs: Vec::new(),
268            resources: Vec::new(),
269            fabrics: Vec::new(),
270            manifests: Vec::new(),
271            observations: Vec::new(),
272            intention_tree: None,
273            reconciles: Vec::new(),
274            leases: Vec::new(),
275            ensembles: Vec::new(),
276            sessions: Vec::new(),
277            topologies: Vec::new(),
278            immunes: Vec::new(),
279            reflexes: Vec::new(),
280            heals: Vec::new(),
281            components: Vec::new(),
282            views: Vec::new(),
283            channels: Vec::new(),
284            sockets: Vec::new(),
285            observables: Vec::new(),
286            witnesses: Vec::new(),
287            upstreams: Vec::new(),
288            cors_policies: Vec::new(),
289            caches: Vec::new(),
290            credentials: Vec::new(),
291            savants: Vec::new(),
292            documents: Vec::new(),
293            deliveries: Vec::new(),
294            notifications: Vec::new(),
295            synths: Vec::new(),
296            scopes: Vec::new(),
297            effects: Vec::new(),
298            modules: Vec::new(),
299        }
300    }
301}
302
303/// §Fase 51.d.2 — IR for the `yield <expr>` measurement point.
304#[derive(Debug, Clone, Serialize)]
305pub struct IRYield {
306    pub node_type: &'static str,
307    pub source_line: u32,
308    pub source_column: u32,
309    pub value_expr: String,
310    pub value_kind: String,
311}
312
313/// §Fase 51.c.2 — one term `cₖ · Pₖ` of a Pauli-sum observable (compiled).
314#[derive(Debug, Clone, Serialize)]
315pub struct IRPauliTerm {
316    pub coefficient: f64,
317    pub pauli: String,
318}
319
320/// §Fase 51.c.2 — IR for a Pauli-sum observable `M = Σ cₖ Pₖ`.
321#[derive(Debug, Clone, Serialize)]
322pub struct IRObservable {
323    pub node_type: &'static str,
324    pub source_line: u32,
325    pub source_column: u32,
326    pub name: String,
327    #[serde(skip_serializing_if = "Option::is_none")]
328    pub qubits: Option<i64>,
329    pub terms: Vec<IRPauliTerm>,
330}
331
332/// §Fase 69.a — IR for an Advantage Witness. The deploy/runtime evaluator reads
333/// `metric` + `threshold` + `baseline`, computes the metric over `data`, and
334/// emits the verdict; a `holds == false` verdict is the honest fail-closed
335/// signal (`axon-W007`/`W008`). `claim`/`data` are references resolved per domain.
336#[derive(Debug, Clone, Serialize)]
337pub struct IRWitness {
338    pub node_type: &'static str,
339    pub source_line: u32,
340    pub source_column: u32,
341    pub name: String,
342    pub claim: String,
343    pub baseline: String,
344    pub metric: String,
345    pub threshold: f64,
346    pub data: String,
347}
348
349// ── §Fase 23 / §Fase 120 — Algebraic effect declarations ────────────────────
350//
351// §Fase 23 declared these as a MIRROR of the Python frontend's dataclasses,
352// emitted by nobody, so a structural-parity gate stayed green against an empty
353// `effects: []`. §Fase 120 makes `axon-frontend` emit them from `.axon` source:
354// `Declaration::Effect` → `IREffectDeclaration`, one per `effect E { … }`.
355//
356// The shape is unchanged on purpose. `axon-rs/src/effects/ir.rs` deserialises
357// exactly these field names (`name` / `operations` / `parameter_names` /
358// `parameter_types` / `return_type`), and keeping the wire identical means the
359// declaration an adopter writes today lands in the same table
360// `EffectRuntime::register_effect` has always read.
361
362#[derive(Debug, Serialize, Default, Clone)]
363pub struct IREffectDeclaration {
364    pub node_type: &'static str,
365    pub source_line: u32,
366    pub source_column: u32,
367    pub name: String,
368    pub operations: Vec<IREffectOperation>,
369}
370
371impl IREffectDeclaration {
372    pub fn new() -> Self {
373        Self {
374            node_type: "effect_declaration",
375            source_line: 0,
376            source_column: 0,
377            name: String::new(),
378            operations: Vec::new(),
379        }
380    }
381}
382
383#[derive(Debug, Serialize, Default, Clone)]
384pub struct IREffectOperation {
385    pub node_type: &'static str,
386    pub source_line: u32,
387    pub source_column: u32,
388    pub name: String,
389    pub type_parameters: Vec<String>,
390    pub parameter_names: Vec<String>,
391    pub parameter_types: Vec<String>,
392    pub return_type: String,
393}
394
395impl IREffectOperation {
396    pub fn new() -> Self {
397        Self {
398            node_type: "effect_operation",
399            source_line: 0,
400            source_column: 0,
401            name: String::new(),
402            type_parameters: Vec::new(),
403            parameter_names: Vec::new(),
404            parameter_types: Vec::new(),
405            return_type: String::new(),
406        }
407    }
408}
409
410// ── §λ-L-E Fase 1 — IRResource ──────────────────────────────────────────────
411
412/// Compiled resource declaration — linear/affine infrastructure token.
413///
414/// Python counterpart: `axon.compiler.ir_nodes.IRResource`.
415#[derive(Debug, Clone, Serialize)]
416pub struct IRResource {
417    pub node_type: &'static str,
418    pub source_line: u32,
419    pub source_column: u32,
420    pub name: String,
421    pub kind: String,
422    pub endpoint: String,
423    /// §Fase 113 — the pool size. Until §113 this was **read by nothing**: every
424    /// `postgresql` axonstore in existence got a hardcoded
425    /// `MAX_POOL_CONNECTIONS = 10` (`store/postgres_backend.rs`), with no env
426    /// var and no source-level knob. `capacity:` is that missing knob, and
427    /// wiring it is what makes `resource` a WIRE rather than a LABEL.
428    pub capacity: Option<i64>,
429    /// §Fase 113 — **how many holders may name this resource** (Linear Logic).
430    ///
431    /// Not "how long the connection lives" — that is `idle_timeout`, an
432    /// operational knob. The Linear-Logic reading is about *sharing*:
433    ///
434    /// - `linear` — **exactly one** holder, and failing to name it is itself a
435    ///   breach (a linear resource must be consumed).
436    /// - `affine` — **at most one** holder. It may go unused; **sharing it is a
437    ///   breach**.
438    /// - `persistent` — the `!` exponential. Freely shared.
439    ///
440    /// Before §113, two stores shared a connection pool by **accidental DSN
441    /// collision** (the registry keys its pool cache on the resolved DSN).
442    /// Sharing is now *declared*, and `axon-T945` checks it.
443    pub lifetime: String,
444    pub certainty_floor: Option<f64>, // c ∈ [0.0, 1.0]
445    pub shield_ref: String,
446    /// §Fase 113 — the `fabric` this resource lives in (`within: Prod`).
447    ///
448    /// **One field, therefore Separation-Logic disjointness is UNREPRESENTABLE
449    /// rather than verified**: a resource cannot be in two fabrics because
450    /// there is no syntax for it. A checked invariant is what you settle for
451    /// when you could not make the bad state unwritable; here we could.
452    ///
453    /// Empty ⇒ no fabric declared. Skip-if-empty ⇒ every pre-§113 program
454    /// serializes byte-identically (IR-SHA stability, the §94.a `class`
455    /// precedent).
456    #[serde(default, skip_serializing_if = "String::is_empty")]
457    pub within: String,
458}
459
460impl IRResource {
461    pub fn new(name: String, line: u32, column: u32) -> Self {
462        IRResource {
463            node_type: "resource",
464            source_line: line,
465            source_column: column,
466            name,
467            kind: String::new(),
468            endpoint: String::new(),
469            capacity: None,
470            lifetime: "affine".to_string(),
471            certainty_floor: None,
472            shield_ref: String::new(),
473            within: String::new(),
474        }
475    }
476}
477
478// ── §λ-L-E Fase 1 — IRFabric ────────────────────────────────────────────────
479
480/// Compiled fabric declaration — topological substrate for resources.
481#[derive(Debug, Clone, Serialize)]
482pub struct IRFabric {
483    pub node_type: &'static str,
484    pub source_line: u32,
485    pub source_column: u32,
486    pub name: String,
487    pub provider: String,
488    pub region: String,
489    pub zones: Option<i64>,
490    pub ephemeral: Option<bool>,
491    pub shield_ref: String,
492}
493
494// ── §λ-L-E Fase 1 — IRManifest ──────────────────────────────────────────────
495
496/// Compiled manifest declaration — declarative belief about desired shape.
497#[derive(Debug, Clone, Serialize)]
498pub struct IRManifest {
499    pub node_type: &'static str,
500    pub source_line: u32,
501    pub source_column: u32,
502    pub name: String,
503    pub resources: Vec<String>,
504    pub fabric_ref: String,
505    pub region: String,
506    pub zones: Option<i64>,
507    pub compliance: Vec<String>,
508}
509
510// ── §λ-L-E Fase 1 — IRObserve ───────────────────────────────────────────────
511
512// ── §λ-L-E Fase 1 — IRIntentionTree (Free Monad root) ──────────────────────
513
514/// A single operation node in the intention tree.
515///
516/// Operations are heterogeneous IR nodes (manifests, observes) that the
517/// Handler layer (Fase 2) interprets via CPS. The enum is `#[serde(untagged)]`
518/// so JSON output is just the inner struct — matching Python's `asdict`
519/// behaviour on a polymorphic `tuple[IRNode, ...]`.
520#[derive(Debug, Clone, Serialize)]
521#[serde(untagged)]
522pub enum IRIntentionOperation {
523    Manifest(IRManifest),
524    Observe(IRObserve),
525}
526
527/// The Free Monad F_Σ(X) — a pure description of I/O intentions. Flat in
528/// Fase 1; nested continuations arrive with handlers + reconcile loops.
529#[derive(Debug, Clone, Serialize)]
530pub struct IRIntentionTree {
531    pub node_type: &'static str,
532    pub source_line: u32,
533    pub source_column: u32,
534    pub operations: Vec<IRIntentionOperation>,
535}
536
537/// Compiled observe declaration — quorum-gated observation with lag τ.
538#[derive(Debug, Clone, Serialize)]
539pub struct IRObserve {
540    pub node_type: &'static str,
541    pub source_line: u32,
542    pub source_column: u32,
543    pub name: String,
544    pub target: String,
545    pub sources: Vec<String>,
546    pub quorum: Option<i64>,
547    pub timeout: String,
548    pub on_partition: String,
549    pub certainty_floor: Option<f64>,
550}
551
552// ── §λ-L-E Fase 3 — IRReconcile / IRLease / IREnsemble ──────────────────────
553
554/// Compiled reconcile declaration — free-energy minimizing control loop.
555#[derive(Debug, Clone, Serialize)]
556pub struct IRReconcile {
557    pub node_type: &'static str,
558    pub source_line: u32,
559    pub source_column: u32,
560    pub name: String,
561    pub observe_ref: String,
562    pub threshold: Option<f64>,
563    pub tolerance: Option<f64>,
564    pub on_drift: String,
565    pub shield_ref: String,
566    pub mandate_ref: String,
567    pub max_retries: i64,
568}
569
570/// Compiled lease declaration — τ-decaying affine resource token.
571#[derive(Debug, Clone, Serialize)]
572pub struct IRLease {
573    pub node_type: &'static str,
574    pub source_line: u32,
575    pub source_column: u32,
576    pub name: String,
577    pub resource_ref: String,
578    pub duration: String,
579    pub acquire: String,
580    pub on_expire: String,
581}
582
583/// Compiled ensemble declaration — Byzantine quorum aggregator.
584#[derive(Debug, Clone, Serialize)]
585pub struct IREnsemble {
586    pub node_type: &'static str,
587    pub source_line: u32,
588    pub source_column: u32,
589    pub name: String,
590    pub observations: Vec<String>,
591    pub quorum: Option<i64>,
592    pub aggregation: String,
593    pub certainty_mode: String,
594}
595
596// ── §λ-L-E Fase 4 — IRSession / IRTopology ──────────────────────────────────
597
598/// One operation in a compiled session protocol
599/// (send / receive / loop / end / select / branch — §Fase 41.b adds the choices).
600#[derive(Debug, Clone, Serialize)]
601pub struct IRSessionStep {
602    pub node_type: &'static str,
603    pub source_line: u32,
604    pub source_column: u32,
605    pub op: String,
606    pub message_type: String,
607    /// §Fase 41.b — labelled branches (only for `op == "select" | "branch"`;
608    /// §Fase 79.b reuses them for `op == "interrupt"`: `body` + `handler` arms).
609    #[serde(skip_serializing_if = "Vec::is_empty", default)]
610    pub branches: Vec<IRSessionBranch>,
611    /// §Fase 79.b — `op == "interrupt"` only: the handler's signal binder
612    /// (`... as <sig> ...`). Skip-if-empty ⇒ zero IR-SHA drift for every
613    /// non-interrupt step (the §76.d/§77.a additive-only discipline).
614    #[serde(skip_serializing_if = "String::is_empty", default)]
615    pub binder: String,
616    /// §Fase 79.b — `op == "interrupt"` only: the block declares a `resumable`
617    /// handler. Skip-if-false ⇒ byte-identical IR for every other op.
618    #[serde(skip_serializing_if = "std::ops::Not::not", default)]
619    pub resumable: bool,
620}
621
622/// §Fase 41.b — one labelled arm of a compiled `select`/`branch` choice.
623#[derive(Debug, Clone, Serialize)]
624pub struct IRSessionBranch {
625    pub node_type: &'static str,
626    pub label: String,
627    pub steps: Vec<IRSessionStep>,
628}
629
630/// A role's name and its ordered protocol steps.
631#[derive(Debug, Clone, Serialize)]
632pub struct IRSessionRole {
633    pub node_type: &'static str,
634    pub source_line: u32,
635    pub source_column: u32,
636    pub name: String,
637    pub steps: Vec<IRSessionStep>,
638}
639
640/// Compiled binary session — exactly two dual roles (verified at type-check).
641#[derive(Debug, Clone, Serialize)]
642pub struct IRSession {
643    pub node_type: &'static str,
644    pub source_line: u32,
645    pub source_column: u32,
646    pub name: String,
647    pub roles: Vec<IRSessionRole>,
648}
649
650/// Directed, session-typed edge between two topology nodes.
651#[derive(Debug, Clone, Serialize)]
652pub struct IRTopologyEdge {
653    pub node_type: &'static str,
654    pub source_line: u32,
655    pub source_column: u32,
656    pub source: String,
657    pub target: String,
658    pub session_ref: String,
659}
660
661/// Compiled topology — typed graph over Axon entities.
662#[derive(Debug, Clone, Serialize)]
663pub struct IRTopology {
664    pub node_type: &'static str,
665    pub source_line: u32,
666    pub source_column: u32,
667    pub name: String,
668    pub nodes: Vec<String>,
669    pub edges: Vec<IRTopologyEdge>,
670}
671
672// ── §λ-L-E Fase 5 — IRImmune / IRReflex / IRHeal ────────────────────────────
673
674/// Compiled immune sensor — KL+FEP anomaly detector descriptor.
675#[derive(Debug, Clone, Serialize)]
676pub struct IRImmune {
677    pub node_type: &'static str,
678    pub source_line: u32,
679    pub source_column: u32,
680    pub name: String,
681    pub watch: Vec<String>,
682    pub sensitivity: Option<f64>,
683    pub baseline: String,
684    pub window: i64,
685    pub scope: String,
686    pub tau: String,
687    pub decay: String,
688}
689
690/// Compiled reflex — deterministic O(1) motor response descriptor.
691#[derive(Debug, Clone, Serialize)]
692pub struct IRReflex {
693    pub node_type: &'static str,
694    pub source_line: u32,
695    pub source_column: u32,
696    pub name: String,
697    pub trigger: String,
698    pub on_level: String,
699    pub action: String,
700    pub scope: String,
701    pub sla: String,
702}
703
704/// Compiled heal — Linear-Logic one-shot patch kernel descriptor.
705#[derive(Debug, Clone, Serialize)]
706pub struct IRHeal {
707    pub node_type: &'static str,
708    pub source_line: u32,
709    pub source_column: u32,
710    pub name: String,
711    pub source: String,
712    pub on_level: String,
713    pub mode: String,
714    pub scope: String,
715    pub review_sla: String,
716    pub shield_ref: String,
717    pub max_patches: i64,
718}
719
720// ── §λ-L-E Fase 9 — IRComponent / IRView ────────────────────────────────────
721
722/// Compiled UI component — reusable fragment over a typed data source.
723#[derive(Debug, Clone, Serialize)]
724pub struct IRComponent {
725    pub node_type: &'static str,
726    pub source_line: u32,
727    pub source_column: u32,
728    pub name: String,
729    pub renders: String,
730    pub via_shield: String,
731    pub on_interact: String,
732    pub render_hint: String,
733}
734
735/// Compiled UI view — top-level screen composing declared components.
736#[derive(Debug, Clone, Serialize)]
737pub struct IRView {
738    pub node_type: &'static str,
739    pub source_line: u32,
740    pub source_column: u32,
741    pub name: String,
742    pub title: String,
743    pub components: Vec<String>,
744    pub route: String,
745}
746
747// ── Import ───────────────────────────────────────────────────────────────────
748
749#[derive(Debug, Serialize, Clone)]
750pub struct IRImport {
751    pub node_type: &'static str,
752    pub source_line: u32,
753    pub source_column: u32,
754    pub module_path: Vec<String>,
755    pub names: Vec<String>,
756    /// §Fase 115.e — `true` iff the EMS resolved this import against a
757    /// module in the compilation (the fields this paper-era struct always
758    /// promised). Skipped when `false` so every pre-§115 program's IR
759    /// JSON stays byte-identical (zero IR-SHA drift).
760    #[serde(default, skip_serializing_if = "ir_import_unresolved")]
761    pub resolved: bool,
762    /// §Fase 115.e — the resolved module's `.axi` interface hash.
763    #[serde(default, skip_serializing_if = "Option::is_none")]
764    pub interface_hash: Option<String>,
765}
766
767/// Serde helper: skip `resolved` while it is `false`.
768fn ir_import_unresolved(resolved: &bool) -> bool {
769    !*resolved
770}
771
772// ── Persona ──────────────────────────────────────────────────────────────────
773
774#[derive(Debug, Clone, Serialize)]
775pub struct IRPersona {
776    pub node_type: &'static str,
777    pub source_line: u32,
778    pub source_column: u32,
779    pub name: String,
780    pub domain: Vec<String>,
781    pub tone: String,
782    pub confidence_threshold: Option<f64>,
783    pub cite_sources: Option<bool>,
784    pub refuse_if: Vec<String>,
785    pub language: String,
786    pub description: String,
787}
788
789// ── Context ──────────────────────────────────────────────────────────────────
790
791#[derive(Debug, Clone, Serialize)]
792pub struct IRContext {
793    pub node_type: &'static str,
794    pub source_line: u32,
795    pub source_column: u32,
796    pub name: String,
797    pub memory_scope: String,
798    pub language: String,
799    pub depth: String,
800    pub max_tokens: Option<i64>,
801    pub temperature: Option<f64>,
802    pub cite_sources: Option<bool>,
803    /// §Fase 91.a — the frame's declared cognitive timezone (IANA name).
804    /// Elided when absent → pre-§91 context IR JSON stays byte-identical.
805    #[serde(default, skip_serializing_if = "Option::is_none")]
806    pub now_tz: Option<String>,
807}
808
809// ── Anchor ───────────────────────────────────────────────────────────────────
810
811#[derive(Debug, Clone, Serialize)]
812pub struct IRAnchor {
813    pub node_type: &'static str,
814    pub source_line: u32,
815    pub source_column: u32,
816    pub name: String,
817    pub description: String,
818    pub require: String,
819    pub reject: Vec<String>,
820    pub enforce: String,
821    pub confidence_floor: Option<f64>,
822    pub unknown_response: String,
823    pub on_violation: String,
824    pub on_violation_target: String,
825}
826
827// ── Tool ─────────────────────────────────────────────────────────────────────
828
829/// §Fase 58.c — one typed parameter of a tool's input schema (the IR mirror of
830/// the AST `Parameter`). `type_name` is the flattened BASE type string
831/// (`String`, `List<String>`); optionality (`T?`) is carried in `optional`, so
832/// `required` is derivable with no parallel bool (§58 D1, single source of
833/// truth). Lossless round-trip is gated in §58.i.
834#[derive(Debug, Clone, Serialize, PartialEq)]
835pub struct IRToolParam {
836    pub name: String,
837    pub type_name: String,
838    pub optional: bool,
839}
840
841/// §Fase 58.c — one bound keyword argument of a `use Tool(k = v, …)` call (the
842/// IR mirror of `UseArgs::Named`). `value` is an expression string (the
843/// frontend has no structured `Expr`). The runtime (§58.e) assembles these into
844/// the structured JSON request body.
845#[derive(Debug, Clone, Serialize, PartialEq)]
846pub struct IRNamedArg {
847    pub name: String,
848    pub value: String,
849    /// §Fase 60 — `"literal"` or `"reference"` (classified by `parse_let_atom`).
850    /// A `"reference"` value (a bare identifier or `Step.output`) is resolved at
851    /// runtime against the bindings (flow-param / `let` / step output), like a
852    /// `let` reference — instead of being passed as the literal name (the pre-60
853    /// bug). `"literal"` values keep `${…}` interpolation + typed coercion.
854    pub value_kind: String,
855}
856
857#[derive(Debug, Serialize, Clone)]
858pub struct IRToolSpec {
859    pub node_type: &'static str,
860    pub source_line: u32,
861    pub source_column: u32,
862    pub name: String,
863    pub provider: String,
864    pub max_results: Option<i64>,
865    pub filter_expr: String,
866    pub timeout: String,
867    pub runtime: String,
868    /// §Fase 114.c — the `resource` this tool's channel runs on. Empty ⇒ legacy
869    /// form. Skip-if-empty ⇒ every pre-§114 tool serializes byte-identically.
870    #[serde(default, skip_serializing_if = "String::is_empty")]
871    pub resource_ref: String,
872    pub sandbox: Option<bool>,
873    pub input_schema: Vec<String>,
874    pub output_schema: String,
875    /// §Fase 58.c — the tool's typed INPUT SCHEMA (D1). Distinct from the §32
876    /// `input_schema`/`output_schema` validation hints (those say HOW to
877    /// validate raw output: JSON/number/…); these are the caller↔tool TYPE
878    /// contract the type-checker enforces (§58.d) and the runtime binds
879    /// structured args against (§58.e). Empty for a schema-less tool (D5).
880    pub parameters: Vec<IRToolParam>,
881    /// §Fase 58.c — the tool's declared OUTPUT type (D8), so `${Step.output}`
882    /// is typed. `None` when undeclared. Single source of truth (lives here,
883    /// not denormalised onto each call site).
884    pub output_type: Option<String>,
885    /// §Fase 116.a (D116.9) — the authorization scopes this tool's operation
886    /// requires: flat capability atoms, the `credential.grants` (§92) /
887    /// `requires_capabilities` (§51.x) vocabulary. `axon-T956` enforces subset
888    /// coverage at compile; the PCC `ScopeCoverage` witness rides the linked
889    /// IR. Elided when empty — every pre-§116 tool serializes byte-identically
890    /// (IR-SHA stability), and stored FlowIr hydrates via `default` (§49.f
891    /// mirror discipline).
892    #[serde(default, skip_serializing_if = "Vec::is_empty")]
893    pub requires: Vec<String>,
894    /// §Fase 94.c — the per-tenant secret KEY injected into every dispatch
895    /// under the reserved `axon_secret` request field (resolved against the
896    /// tenant's custody at `use` time; the flow never touches the value).
897    /// Elided when empty — every pre-§94 tool serializes byte-identically
898    /// (IR-SHA stability).
899    #[serde(default, skip_serializing_if = "String::is_empty")]
900    pub secret: String,
901    /// §Fase 95.a — the `secret_partition:` parameter name whose runtime
902    /// value is appended as a single segment to `secret` at dispatch
903    /// (`selection_without_revelation`). Elided when empty, so every §94 and
904    /// pre-§94 tool serializes byte-identically (IR-SHA stability). The
905    /// class prefix lives in `secret`; this only names the dynamic segment
906    /// source — no value ever rides the IR.
907    #[serde(default, skip_serializing_if = "String::is_empty")]
908    pub secret_partition: String,
909    pub effect_row: Vec<String>,
910    /// §Fase 84.b — Remote Hands. All three fields are `skip_serializing_if`
911    /// so a program using none of them serialises **byte-identically** to the
912    /// pre-§84 IR (the §76.d IR-SHA / additive-only gate — no drift for the
913    /// entire existing corpus).
914    #[serde(skip_serializing_if = "Option::is_none")]
915    pub target: Option<String>,
916    #[serde(skip_serializing_if = "Option::is_none")]
917    pub risk: Option<String>,
918    #[serde(skip_serializing_if = "Vec::is_empty")]
919    pub argv: Vec<String>,
920    /// §Fase 85.b — the cache-policy reference (a declared `cache` name, or the
921    /// `none` opt-out sentinel). Empty ⇒ module-default-governed. Elided when
922    /// empty (IR-SHA stable for cache-less programs).
923    #[serde(default, skip_serializing_if = "String::is_empty")]
924    pub cache: String,
925    /// §Fase 98.b — the closed-catalog web-acquisition config. `None` for
926    /// every non-scrape tool, and `skip_serializing_if` so the entire
927    /// pre-§98 corpus serialises byte-identically (the §76.d IR-SHA /
928    /// additive-only gate). Present ⇒ this tool acquires open-web content
929    /// (born Untrusted, D98.1) and its `effect_row` carries `web`.
930    #[serde(skip_serializing_if = "Option::is_none")]
931    pub scrape: Option<IRScrapeSpec>,
932}
933
934/// §Fase 98.b — the IR mirror of `ast::ScrapeSpec`. Every field is
935/// `skip_serializing_if` on its empty/none form so a minimal `scrape: {}`
936/// and each partially-populated block serialise deterministically with no
937/// null noise, keeping the IR-SHA additive.
938#[derive(Debug, Serialize, Clone)]
939pub struct IRScrapeSpec {
940    pub node_type: &'static str,
941    #[serde(skip_serializing_if = "Option::is_none")]
942    pub engine: Option<String>,
943    #[serde(skip_serializing_if = "Option::is_none")]
944    pub impersonate: Option<String>,
945    #[serde(skip_serializing_if = "Option::is_none")]
946    pub render_wait: Option<String>,
947    #[serde(default, skip_serializing_if = "String::is_empty")]
948    pub proxy: String,
949    #[serde(skip_serializing_if = "Option::is_none")]
950    pub respect_robots: Option<bool>,
951    #[serde(default, skip_serializing_if = "Vec::is_empty")]
952    pub extract: Vec<String>,
953    #[serde(skip_serializing_if = "Option::is_none")]
954    pub adaptive: Option<bool>,
955    #[serde(skip_serializing_if = "Option::is_none")]
956    pub similarity_floor: Option<f64>,
957    #[serde(default, skip_serializing_if = "String::is_empty")]
958    pub follow: String,
959    #[serde(skip_serializing_if = "Option::is_none")]
960    pub max_depth: Option<i64>,
961    #[serde(skip_serializing_if = "Option::is_none")]
962    pub max_pages: Option<i64>,
963    #[serde(skip_serializing_if = "Option::is_none")]
964    pub concurrency: Option<i64>,
965    #[serde(default, skip_serializing_if = "String::is_empty")]
966    pub politeness: String,
967    #[serde(default, skip_serializing_if = "String::is_empty")]
968    pub checkpoint: String,
969}
970
971// ── Memory ───────────────────────────────────────────────────────────────────
972
973#[derive(Debug, Serialize, Clone)]
974pub struct IRMemory {
975    pub node_type: &'static str,
976    pub source_line: u32,
977    pub source_column: u32,
978    pub name: String,
979    pub store: String,
980    pub backend: String,
981    pub retrieval: String,
982    pub decay: String,
983}
984
985// ── Type ─────────────────────────────────────────────────────────────────────
986
987#[derive(Debug, Clone, Serialize)]
988pub struct IRTypeField {
989    pub node_type: &'static str,
990    pub source_line: u32,
991    pub source_column: u32,
992    pub name: String,
993    pub type_name: String,
994    pub generic_param: String,
995    pub optional: bool,
996}
997
998#[derive(Debug, Serialize, Clone)]
999pub struct IRType {
1000    pub node_type: &'static str,
1001    pub source_line: u32,
1002    pub source_column: u32,
1003    pub name: String,
1004    pub fields: Vec<IRTypeField>,
1005    pub range_min: Option<f64>,
1006    pub range_max: Option<f64>,
1007    pub where_expression: String,
1008    /// §ESK Fase 6.1 — κ regulatory class.
1009    pub compliance: Vec<String>,
1010}
1011
1012// ── Flow ─────────────────────────────────────────────────────────────────────
1013
1014#[derive(Debug, Clone, Serialize)]
1015pub struct IRParameter {
1016    pub node_type: &'static str,
1017    pub source_line: u32,
1018    pub source_column: u32,
1019    pub name: String,
1020    pub type_name: String,
1021    pub generic_param: String,
1022    pub optional: bool,
1023}
1024
1025#[derive(Debug, Clone, Serialize)]
1026pub struct IRDataEdge {
1027    pub node_type: &'static str,
1028    pub source_line: u32,
1029    pub source_column: u32,
1030    pub source_step: String,
1031    pub target_step: String,
1032    pub type_name: String,
1033}
1034
1035#[derive(Debug, Clone, Serialize)]
1036pub struct IRStep {
1037    pub node_type: &'static str,
1038    pub source_line: u32,
1039    pub source_column: u32,
1040    pub name: String,
1041    pub persona_ref: String,
1042    pub given: String,
1043    pub ask: String,
1044    pub use_tool: Option<serde_json::Value>,
1045    pub probe: Option<serde_json::Value>,
1046    pub reason: Option<serde_json::Value>,
1047    pub weave: Option<serde_json::Value>,
1048    pub output_type: String,
1049    pub confidence_floor: Option<f64>,
1050    pub navigate_ref: String,
1051    pub apply_ref: String,
1052    /// §Fase 68.b — the step's model-capability requirement (context window in
1053    /// tokens). `skip_serializing_if = Option::is_none` keeps every pre-§68 step's
1054    /// IR JSON byte-identical (no IR-SHA drift, D68.4); a legacy IR deserialises
1055    /// to `None` → the §68.c resolver picks the backend default exactly as today.
1056    #[serde(default, skip_serializing_if = "Option::is_none")]
1057    pub requires_context: Option<u32>,
1058    /// §Fase 91.a — the step's declared cognitive timezone (IANA name). The
1059    /// runtime renders the run's captured instant in this zone into the step's
1060    /// cognitive context. Elided when absent → every pre-§91 step's IR JSON is
1061    /// byte-identical (no IR-SHA drift); legacy IR → `None` → no injection.
1062    #[serde(default, skip_serializing_if = "Option::is_none")]
1063    pub now_tz: Option<String>,
1064    /// §Fase 119 (D119.4) — governance applications scoped to this step.
1065    /// Elided when empty so every pre-§119 program's IR JSON stays
1066    /// byte-identical (no IR-SHA drift — the §68.b/§91.a discipline).
1067    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1068    pub guards: Vec<IRStepGuard>,
1069    /// §Fase 119.f — the PIX verbs written as statements in this step's body
1070    /// (`navigate` / `drill` / `trail` / `validate`), in source order. They
1071    /// are ELEVATIONS: dispatch runs them before the step generates, so each
1072    /// `as:` binding is in scope for the step's `ask:`. Elided when empty so
1073    /// every pre-§119.f program's IR JSON is byte-identical.
1074    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1075    pub pix_ops: Vec<IRFlowNode>,
1076    /// §Fase 119.n — a `stream<T> { … }` written in this step's body. NOT a
1077    /// `pix_ops` entry: those are elevations that run BEFORE generation, while a
1078    /// stream handler runs DURING it and this step's output IS the stream.
1079    /// Elided when absent, so every pre-§119.n program's IR JSON is
1080    /// byte-identical and a legacy IR still deserialises.
1081    #[serde(default, skip_serializing_if = "Option::is_none")]
1082    pub stream: Option<Box<IRStreamBlock>>,
1083    /// §Fase 120 — the `perform Op(args)` statements written in this step's
1084    /// body, in source order. Dispatch runs them AFTER the step generates, with
1085    /// the step's output in scope — NOT as `pix_ops` elevations, because
1086    /// `fase_23` §3.1's `perform Emit(response.token)` performs the step's own
1087    /// result. Elided when empty, so every pre-§120 program's IR JSON stays
1088    /// byte-identical (the §68.b/§91.a/§119.n discipline).
1089    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1090    pub performs: Vec<IREffectPerform>,
1091    pub body: Vec<serde_json::Value>,
1092}
1093
1094/// §Fase 119 (D119.4) — one step-scoped governance application. For a
1095/// `mandate` guard, dispatch must run the §119.b control loop over THIS
1096/// step's generation, with the mandate's declared `(D, L)` obligations.
1097#[derive(Debug, Clone, Serialize)]
1098pub struct IRStepGuard {
1099    pub kind: String,
1100    pub name: String,
1101    pub target: String,
1102    pub binding: String,
1103}
1104
1105#[derive(Debug, Clone, Serialize)]
1106pub struct IRFlow {
1107    pub node_type: &'static str,
1108    pub source_line: u32,
1109    pub source_column: u32,
1110    pub name: String,
1111    pub parameters: Vec<IRParameter>,
1112    pub return_type_name: String,
1113    pub return_type_generic: String,
1114    pub return_type_optional: bool,
1115    pub steps: Vec<IRFlowNode>,
1116    pub edges: Vec<IRDataEdge>,
1117    pub execution_levels: Vec<Vec<String>>,
1118}
1119
1120// ── Run ──────────────────────────────────────────────────────────────────────
1121
1122#[derive(Debug, Clone, Serialize)]
1123pub struct IRRun {
1124    pub node_type: &'static str,
1125    pub source_line: u32,
1126    pub source_column: u32,
1127    pub flow_name: String,
1128    pub arguments: Vec<String>,
1129    pub persona_name: String,
1130    pub context_name: String,
1131    pub anchor_names: Vec<String>,
1132    pub on_failure: String,
1133    pub on_failure_params: Vec<Vec<String>>,
1134    pub output_to: String,
1135    pub effort: String,
1136    pub resolved_flow: Option<IRFlow>,
1137    pub resolved_persona: Option<IRPersona>,
1138    pub resolved_context: Option<IRContext>,
1139    pub resolved_anchors: Vec<IRAnchor>,
1140}
1141
1142// ── Lambda Data (ΛD) — Epistemic State Vectors ─────────────────────────────
1143
1144#[derive(Debug, Clone, Serialize)]
1145pub struct IRLambdaData {
1146    pub node_type: &'static str,
1147    pub source_line: u32,
1148    pub source_column: u32,
1149    pub name: String,
1150    pub ontology: String,             // T — ontological type
1151    pub certainty: f64,               // c ∈ [0,1]
1152    pub temporal_frame_start: String, // τ_start
1153    pub temporal_frame_end: String,   // τ_end
1154    pub provenance: String,           // ρ — EntityRef origin
1155    pub derivation: String,           // δ ∈ Δ
1156}
1157
1158#[derive(Debug, Clone, Serialize)]
1159pub struct IRLambdaDataApply {
1160    pub node_type: &'static str,
1161    pub source_line: u32,
1162    pub source_column: u32,
1163    pub lambda_data_name: String, // reference to declared ΛD
1164    pub target: String,           // expression being bound
1165    pub output_type: String,      // result type after binding
1166}
1167
1168// ── Flow step IR nodes ──────────────────────────────────────────────────────
1169
1170/// Polymorphic flow body node — serializes via #[serde(untagged)] so each
1171/// variant emits its inner struct's JSON (with its own `node_type` field).
1172#[derive(Debug, Clone, Serialize)]
1173#[serde(untagged)]
1174pub enum IRFlowNode {
1175    Step(IRStep),
1176    Probe(IRProbe),
1177    Reason(IRReasonStep),
1178    Validate(IRValidateStep),
1179    Refine(IRRefineStep),
1180    Weave(IRWeaveStep),
1181    UseTool(IRUseToolStep),
1182    Remember(IRRememberStep),
1183    Recall(IRRecallStep),
1184    Conditional(IRConditional),
1185    ForIn(IRForIn),
1186    Let(IRLetBinding),
1187    Return(IRReturnStep),
1188    /// Fase 19.e — exit the enclosing for-in body. Payload-free;
1189    /// the runner translates it into a sentinel that terminates the
1190    /// loop. Parser scope check guarantees this only appears inside
1191    /// a for-in body.
1192    Break(IRBreakStep),
1193    /// Fase 19.e — skip to the next iteration of the enclosing for-in
1194    /// body. Same shape as Break — payload-free, sentinel-driven at
1195    /// runtime.
1196    Continue(IRContinueStep),
1197    LambdaDataApply(IRLambdaDataApply),
1198    Par(IRParallelBlock),
1199    Hibernate(IRHibernateStep),
1200    Deliberate(IRDeliberateBlock),
1201    Consensus(IRConsensusBlock),
1202    Forge(IRForgeBlock),
1203    /// §Fase 109 — the proof-carrying derivative step.
1204    Grad(IRGradStep),
1205    Focus(IRFocusStep),
1206    Associate(IRAssociateStep),
1207    Aggregate(IRAggregateStep),
1208    Explore(IRExploreStep),
1209    Ingest(IRIngestStep),
1210    ShieldApply(IRShieldApplyStep),
1211    Stream(IRStreamBlock),
1212    /// §Fase 120 — `handle E { … } in { … }`. The frame that makes an outer
1213    /// handler able to intercept an inner computation's effects without the
1214    /// inner code knowing — the compositional property nothing in the language
1215    /// could express before.
1216    Handle(IREffectHandle),
1217    /// §Fase 120 — `perform E.Op(args)`.
1218    Perform(IREffectPerform),
1219    /// §Fase 120 — `resume(v)`, the one-shot continuation invocation (D2).
1220    Resume(IREffectResume),
1221    /// §Fase 120 — `abort(v)`.
1222    Abort(IREffectAbort),
1223    /// §Fase 120 — `forward E.Op(args)` (D12).
1224    Forward(IREffectForward),
1225    Navigate(IRNavigateStep),
1226    Drill(IRDrillStep),
1227    Trail(IRTrailStep),
1228    Corroborate(IRCorroborateStep),
1229    OtsApply(IROtsApplyStep),
1230    MandateApply(IRMandateApplyStep),
1231    ComputeApply(IRComputeApplyStep),
1232    /// §Fase 119.m.3 — `<Agent>(arg, …)`. The 46th variant, and the one that
1233    /// makes §119.m.1's bounded control loop reachable from source.
1234    AgentCall(IRAgentCall),
1235    Listen(IRListenStep),
1236    DaemonStep(IRDaemonStepNode),
1237    /// §λ-L-E Fase 13 — π-calc output prefix (Chan-Output / Chan-Mobility).
1238    Emit(IREmit),
1239    /// §Fase 92.b — ephemeral-credential minting (attenuated, TTL-bounded).
1240    Mint(IRMintStep),
1241    /// §Fase 94.b — mediated secret renewal (`rotation_without_revelation`).
1242    Rotate(IRRotateStep),
1243    /// §λ-L-E Fase 13 — capability extrusion (Publish-Ext).
1244    Publish(IRPublish),
1245    /// §λ-L-E Fase 13 — dual of publish (typed handle import).
1246    Discover(IRDiscover),
1247    Persist(IRPersistStep),
1248    Retrieve(IRRetrieveStep),
1249    Mutate(IRMutateStep),
1250    Purge(IRPurgeStep),
1251    Transact(IRTransactBlock),
1252    /// §Fase 88.a — the `warden` adversarial security-analysis block.
1253    Warden(IRWarden),
1254    /// §Fase 51.a — the `quant` cognitive block (Hilbert-space projection).
1255    Quant(IRQuant),
1256    /// §Fase 51.d.2 — the `yield` measurement point inside a `quant` block.
1257    Yield(IRYield),
1258    /// §Fase 52.c — `run <Flow>(args)` flow-step: invoke a declared flow from a
1259    /// body (a daemon listen handler). Reuses [`IRRun`] (the top-level run IR).
1260    Run(IRRun),
1261}
1262
1263#[derive(Debug, Clone, Serialize)]
1264pub struct IRProbe {
1265    pub node_type: &'static str,
1266    pub source_line: u32,
1267    pub source_column: u32,
1268    pub target: String,
1269}
1270
1271#[derive(Debug, Clone, Serialize)]
1272pub struct IRReasonStep {
1273    pub node_type: &'static str,
1274    pub source_line: u32,
1275    pub source_column: u32,
1276    pub strategy: String,
1277    pub target: String,
1278    /// §Fase 119.f.8 — the evidence the deliberation reasons OVER, resolved
1279    /// against the flow bindings at dispatch. Elided when empty so every
1280    /// pre-§119.f.8 program's IR JSON is byte-identical (no IR-SHA drift).
1281    #[serde(default, skip_serializing_if = "String::is_empty")]
1282    pub given: String,
1283    /// §Fase 119.f.8 — the deliberation's question. The README writes it in
1284    /// every one of its sixteen `reason { … }` blocks and, before this fase, it
1285    /// reached the model in none of them.
1286    #[serde(default, skip_serializing_if = "String::is_empty")]
1287    pub ask: String,
1288    /// §Fase 119.f.8 — the declared deliberation depth. Consumed by the
1289    /// dispatch FRAMING, like `strategy`; not a runtime iteration bound.
1290    #[serde(default, skip_serializing_if = "Option::is_none")]
1291    pub depth: Option<u32>,
1292}
1293
1294#[derive(Debug, Clone, Serialize)]
1295pub struct IRValidateStep {
1296    pub node_type: &'static str,
1297    pub source_line: u32,
1298    pub source_column: u32,
1299    pub target: String,
1300    pub rule: String,
1301}
1302
1303#[derive(Debug, Clone, Serialize)]
1304pub struct IRRefineStep {
1305    pub node_type: &'static str,
1306    pub source_line: u32,
1307    pub source_column: u32,
1308    pub target: String,
1309    pub strategy: String,
1310}
1311
1312#[derive(Debug, Clone, Serialize)]
1313pub struct IRWeaveStep {
1314    pub node_type: &'static str,
1315    pub source_line: u32,
1316    pub source_column: u32,
1317    pub sources: Vec<String>,
1318    pub target: String,
1319    pub format_type: String,
1320    pub priority: Vec<String>,
1321    pub style: String,
1322    /// §Fase 119.f.9 — the parts the synthesis must contain
1323    /// (`include: [summary, risks, recommendations]`). Elided when empty so
1324    /// every pre-§119.f.9 program's IR JSON is byte-identical (no IR-SHA drift).
1325    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1326    pub include: Vec<String>,
1327}
1328
1329#[derive(Debug, Clone, Serialize)]
1330pub struct IRUseToolStep {
1331    pub node_type: &'static str,
1332    pub source_line: u32,
1333    pub source_column: u32,
1334    pub tool_name: String,
1335    pub argument: String,
1336    /// §Fase 58.c — the bound keyword args of `use Tool(k = v, …)` (W1: the
1337    /// structured args survive to the IR, no longer collapsed to one opaque
1338    /// string). Empty for the legacy single-`on <arg>` form (`argument`
1339    /// carries that, D5).
1340    pub named_args: Vec<IRNamedArg>,
1341}
1342
1343#[derive(Debug, Clone, Serialize)]
1344pub struct IRRememberStep {
1345    pub node_type: &'static str,
1346    pub source_line: u32,
1347    pub source_column: u32,
1348    pub expression: String,
1349    pub memory_target: String,
1350}
1351
1352#[derive(Debug, Clone, Serialize)]
1353pub struct IRRecallStep {
1354    pub node_type: &'static str,
1355    pub source_line: u32,
1356    pub source_column: u32,
1357    pub query: String,
1358    pub memory_source: String,
1359}
1360
1361/// §Fase 70.a — the lowered form of a pure expression (`Expr`). Carried in the
1362/// IR for conditions the legacy `(condition, op, value)` triple cannot express.
1363/// Operators are canonical lowercase strings so the JSON is stable + readable;
1364/// the runtime evaluator (§70.f) matches on them. Externally-tagged by `kind`.
1365#[derive(Debug, Clone, Serialize)]
1366#[serde(tag = "kind", rename_all = "snake_case")]
1367pub enum IRExpr {
1368    /// A typed literal.
1369    Lit { lit: IRExprLit },
1370    /// A reference to a binding / dotted path.
1371    Ref { path: String },
1372    /// Unary op — `op ∈ {neg, not}`.
1373    Unary { op: String, operand: Box<IRExpr> },
1374    /// Binary op — `op ∈ {add,sub,mul,div,mod,eq,ne,lt,le,gt,ge,and,or}`.
1375    Binary {
1376        op: String,
1377        lhs: Box<IRExpr>,
1378        rhs: Box<IRExpr>,
1379    },
1380    /// §Fase 70.c — a closed-catalog builtin call. `args[0]` is the receiver.
1381    /// `builtin ∈ {length,count,is_empty,is_null,contains,starts_with,ends_with}`.
1382    Call {
1383        builtin: String,
1384        args: Vec<IRExpr>,
1385    },
1386    /// §Fase 70.d — field access on a non-reference base (the JSONB seam).
1387    Field {
1388        base: Box<IRExpr>,
1389        field: String,
1390    },
1391    /// §Fase 70.d — index access `base[index]`.
1392    Index {
1393        base: Box<IRExpr>,
1394        index: Box<IRExpr>,
1395    },
1396    /// §Fase 119.o — `let <name> = <value>` scoped over `<body>`. The lowered
1397    /// form of a `logic { let … return … }` chain: one `Let` per binding,
1398    /// nested, so each bound term is evaluated exactly once and shadowing falls
1399    /// out of the nesting.
1400    Let {
1401        name: String,
1402        value: Box<IRExpr>,
1403        body: Box<IRExpr>,
1404    },
1405}
1406
1407/// §Fase 70.a — a literal inside an [`IRExpr`].
1408#[derive(Debug, Clone, Serialize)]
1409#[serde(tag = "ty", rename_all = "snake_case")]
1410pub enum IRExprLit {
1411    Int { value: i64 },
1412    Float { value: f64 },
1413    Bool { value: bool },
1414    Str { value: String },
1415}
1416
1417#[derive(Debug, Clone, Serialize)]
1418pub struct IRConditional {
1419    pub node_type: &'static str,
1420    pub source_line: u32,
1421    pub source_column: u32,
1422    pub condition: String,
1423    pub comparison_op: String,
1424    pub comparison_value: String,
1425    pub then_body: Vec<IRFlowNode>,
1426    pub else_body: Vec<IRFlowNode>,
1427    pub conditions: Vec<(String, String, String)>,
1428    pub conjunctor: String,
1429    /// §Fase 70.a — the lowered expression form, present only for conditions
1430    /// the legacy triple cannot express. `skip_serializing_if` keeps the IR
1431    /// JSON (and its SHA) byte-identical for every pre-§70 program.
1432    #[serde(skip_serializing_if = "Option::is_none")]
1433    pub cond: Option<IRExpr>,
1434}
1435
1436#[derive(Debug, Clone, Serialize)]
1437pub struct IRForIn {
1438    pub node_type: &'static str,
1439    pub source_line: u32,
1440    pub source_column: u32,
1441    pub variable: String,
1442    pub iterable: String,
1443    pub body: Vec<IRFlowNode>,
1444}
1445
1446#[derive(Debug, Clone, Serialize)]
1447pub struct IRLetBinding {
1448    pub node_type: &'static str,
1449    pub source_line: u32,
1450    pub source_column: u32,
1451    pub target: String,
1452    pub value: String,
1453    /// Fase 17.a — preserves parser tokenization intent.
1454    /// One of "literal" | "reference" | "expression".
1455    pub value_kind: String,
1456    /// §Fase 70.f — the lowered expression form of the value, present only for
1457    /// `value_kind == "expression"`. The runtime evaluates it instead of
1458    /// treating the value string as an opaque literal. `skip_serializing_if`
1459    /// keeps the IR byte-identical for every literal / reference let.
1460    #[serde(skip_serializing_if = "Option::is_none")]
1461    pub value_ast: Option<IRExpr>,
1462}
1463
1464#[derive(Debug, Clone, Serialize)]
1465pub struct IRReturnStep {
1466    pub node_type: &'static str,
1467    pub source_line: u32,
1468    pub source_column: u32,
1469    pub value_expr: String,
1470}
1471
1472/// Fase 19.e — `break` keyword IR node. Payload-free (the runner
1473/// raises a sentinel; no value is carried). Mirrors Python's
1474/// ``IRBreak`` (axon/compiler/ir_nodes.py).
1475#[derive(Debug, Clone, Serialize)]
1476pub struct IRBreakStep {
1477    pub node_type: &'static str,
1478    pub source_line: u32,
1479    pub source_column: u32,
1480}
1481
1482/// Fase 19.e — `continue` keyword IR node. Same shape as
1483/// ``IRBreakStep``; the runner uses a different sentinel type to
1484/// distinguish loop-exit (break) from iteration-skip (continue).
1485#[derive(Debug, Clone, Serialize)]
1486pub struct IRContinueStep {
1487    pub node_type: &'static str,
1488    pub source_line: u32,
1489    pub source_column: u32,
1490}
1491
1492#[derive(Debug, Clone, Serialize)]
1493pub struct IRParallelBlock {
1494    pub node_type: &'static str,
1495    pub source_line: u32,
1496    pub source_column: u32,
1497    /// §Fase 65 — the concurrent branches lowered from the AST `par { … }`.
1498    /// Each branch is a flow-IR body run concurrently by the dispatcher's
1499    /// `run_branches_concurrently`. `skip_serializing_if = "Vec::is_empty"` so a
1500    /// payload-free / empty `par` serializes byte-identically to the pre-§65
1501    /// shape (D5 back-compat); a `par` with real branches carries them.
1502    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1503    pub branches: Vec<Vec<IRFlowNode>>,
1504}
1505
1506#[derive(Debug, Clone, Serialize)]
1507pub struct IRHibernateStep {
1508    pub node_type: &'static str,
1509    pub source_line: u32,
1510    pub source_column: u32,
1511    pub event_name: String,
1512    pub timeout: String,
1513}
1514
1515#[derive(Debug, Clone, Serialize)]
1516pub struct IRDeliberateBlock {
1517    pub node_type: &'static str,
1518    pub source_line: u32,
1519    pub source_column: u32,
1520}
1521
1522#[derive(Debug, Clone, Serialize)]
1523pub struct IRConsensusBlock {
1524    pub node_type: &'static str,
1525    pub source_line: u32,
1526    pub source_column: u32,
1527}
1528
1529/// §Fase 86 — the compiled Directed Creative Synthesis block. This IS the
1530/// "structured IR metadata that the runtime executes as an orchestrated
1531/// pipeline" the README always claimed — pre-§86 it carried only a source
1532/// location. New fields are `skip_serializing_if`-elided so a program with no
1533/// `forge` stays IR-SHA stable.
1534#[derive(Debug, Clone, Serialize, Default)]
1535pub struct IRForgeBlock {
1536    pub node_type: &'static str,
1537    pub source_line: u32,
1538    pub source_column: u32,
1539    #[serde(default, skip_serializing_if = "String::is_empty")]
1540    pub name: String,
1541    #[serde(default, skip_serializing_if = "String::is_empty")]
1542    pub seed: String,
1543    #[serde(default, skip_serializing_if = "String::is_empty")]
1544    pub output_type: String,
1545    #[serde(default, skip_serializing_if = "String::is_empty")]
1546    pub mode: String,
1547    #[serde(default, skip_serializing_if = "is_default_novelty")]
1548    pub novelty: f64,
1549    #[serde(default, skip_serializing_if = "is_one_i64")]
1550    pub depth: i64,
1551    #[serde(default, skip_serializing_if = "is_one_i64")]
1552    pub branches: i64,
1553    #[serde(default, skip_serializing_if = "String::is_empty")]
1554    pub constraints_ref: String,
1555}
1556
1557fn is_default_novelty(v: &f64) -> bool {
1558    (*v - 0.5).abs() < f64::EPSILON
1559}
1560fn is_one_i64(v: &i64) -> bool {
1561    *v == 1
1562}
1563
1564/// §Fase 109.a — the proof-carrying derivative. `original` is the
1565/// differentiated `let`'s expression; `derivatives[i]` = ∂original/∂wrt[i],
1566/// SIMPLIFIED (D109.4) — computed at compile time by the symbolic
1567/// differentiator and re-derived at deploy by PCC `GradientSoundness`.
1568/// `original: None` / empty `derivatives` only in a stale artifact — the
1569/// runtime fails CLOSED on it and the PCC refutes it.
1570#[derive(Debug, Clone, Serialize)]
1571pub struct IRGradStep {
1572    pub node_type: &'static str,
1573    pub source_line: u32,
1574    pub source_column: u32,
1575    /// The prior rich `let` differentiated.
1576    pub target: String,
1577    pub wrt: Vec<String>,
1578    /// Result binding (empty ⇒ `d_<target>`).
1579    pub output: String,
1580    pub original: Option<IRExpr>,
1581    pub derivatives: Vec<IRExpr>,
1582}
1583
1584#[derive(Debug, Clone, Serialize)]
1585pub struct IRFocusStep {
1586    pub node_type: &'static str,
1587    pub source_line: u32,
1588    pub source_column: u32,
1589    pub expression: String,
1590    /// §Fase 108.d — the data-plane `where:` (D108.9; empty ⇒ no filter).
1591    pub where_expr: String,
1592    /// §Fase 108.d — π: projected columns (empty ⇒ all).
1593    pub select: Vec<String>,
1594    /// §Fase 108.d — result binding (`as:`; empty ⇒ the dataspace name).
1595    pub output: String,
1596}
1597
1598#[derive(Debug, Clone, Serialize)]
1599pub struct IRAssociateStep {
1600    pub node_type: &'static str,
1601    pub source_line: u32,
1602    pub source_column: u32,
1603    pub left: String,
1604    pub right: String,
1605    pub using_field: String,
1606    /// §Fase 108.d — result binding (`as:`; empty ⇒ `<L>_<R>`).
1607    pub output: String,
1608}
1609
1610#[derive(Debug, Clone, Serialize)]
1611pub struct IRAggregateStep {
1612    pub node_type: &'static str,
1613    pub source_line: u32,
1614    pub source_column: u32,
1615    pub target: String,
1616    pub group_by: Vec<String>,
1617    pub alias: String,
1618    /// §Fase 108.d — the closed aggregate catalog entries, raw
1619    /// (`count`, `sum(score)`, …) — canonical spelling, T930-validated.
1620    pub compute: Vec<String>,
1621    /// §Fase 108.d — the data-plane `where:` (D108.9).
1622    pub where_expr: String,
1623}
1624
1625#[derive(Debug, Clone, Serialize)]
1626pub struct IRExploreStep {
1627    pub node_type: &'static str,
1628    pub source_line: u32,
1629    pub source_column: u32,
1630    pub target: String,
1631    pub limit: Option<i64>,
1632    /// §Fase 108.d — result binding (`as:`; empty ⇒ the target).
1633    pub output: String,
1634}
1635
1636#[derive(Debug, Clone, Serialize)]
1637pub struct IRIngestStep {
1638    pub node_type: &'static str,
1639    pub source_line: u32,
1640    pub source_column: u32,
1641    pub source: String,
1642    pub target: String,
1643    /// §Fase 108.c — the declared wire format (`csv` | `json`, validated
1644    /// by axon-T929). Empty only in a pre-108.c artifact — the runtime
1645    /// handler fails CLOSED on it.
1646    pub format: String,
1647    /// §Fase 108.c — bounds enforced on the raw byte stream BEFORE
1648    /// parsing (§100). `None` ⇒ the engine's conservative defaults.
1649    pub max_bytes: Option<u64>,
1650    pub max_rows: Option<u64>,
1651}
1652
1653/// §Fase 114.w — a shield's compiled BREACH POLICY, resolved onto the nodes
1654/// that enforce it (`IRShieldApplyStep` / `IREmit`) at LOWERING — the same
1655/// discipline as `IREmit.shield_ref`: the policy rides the artifact, so every
1656/// dispatch path honors it by construction (no per-ctx shield map a forgotten
1657/// site could miss). Before §114.w the whole `on_breach:` catalog
1658/// (`halt|sanitize_and_retry|escalate|quarantine|deflect`) was documented,
1659/// parsed, type-checked — and the runtime always `halt`ed.
1660#[derive(Debug, Clone, Serialize)]
1661pub struct IRBreachPolicy {
1662    /// The declared policy (validated against `VALID_ON_BREACH_POLICIES`).
1663    pub on_breach: String,
1664    /// The quarantine SINK name (`on_breach: quarantine` requires it, axon-T952).
1665    #[serde(default, skip_serializing_if = "String::is_empty")]
1666    pub quarantine: String,
1667    /// The canned safe reply (`on_breach: deflect` requires it, axon-T952).
1668    #[serde(default, skip_serializing_if = "String::is_empty")]
1669    pub deflect_message: String,
1670    /// Fields masked by `sanitize_and_retry` (requires ≥ 1, axon-T952).
1671    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1672    pub redact: Vec<String>,
1673    /// Re-scan budget for `sanitize_and_retry` (parser default: 3).
1674    pub max_retries: i64,
1675}
1676
1677#[derive(Debug, Clone, Serialize)]
1678pub struct IRShieldApplyStep {
1679    pub node_type: &'static str,
1680    pub source_line: u32,
1681    pub source_column: u32,
1682    pub shield_name: String,
1683    pub target: String,
1684    pub output_type: String,
1685    /// §Fase 114.w — the named shield's breach policy, resolved at lowering.
1686    /// `None` ⇒ the shield declares no `on_breach:` (halt, the fail-closed
1687    /// default) — and every pre-§114.w program serializes byte-identically.
1688    #[serde(default, skip_serializing_if = "Option::is_none")]
1689    pub breach_policy: Option<IRBreachPolicy>,
1690}
1691
1692#[derive(Debug, Clone, Serialize)]
1693pub struct IRStreamBlock {
1694    pub node_type: &'static str,
1695    pub source_line: u32,
1696    pub source_column: u32,
1697    /// §Fase 111.e — the block's lowered body. ADDITIVE: `skip_serializing_if`
1698    /// elides it when empty, so every pre-111 program's IR JSON stays
1699    /// byte-identical and a legacy IR deserialises to an empty body (which then
1700    /// executes as a no-op, exactly as before — no silent behaviour change for
1701    /// an artifact compiled by an older frontend).
1702    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1703    pub body: Vec<IRFlowNode>,
1704    /// §Fase 119.n — the `<T>` in `stream<T>`: the CHUNK type. Elided when the
1705    /// block was written without one.
1706    #[serde(default, skip_serializing_if = "String::is_empty")]
1707    pub chunk_type: String,
1708    /// §Fase 119.n — `on_chunk: { … }`, lowered. Run once per chunk with the
1709    /// chunk bound as `chunk`.
1710    #[serde(default, skip_serializing_if = "Option::is_none")]
1711    pub on_chunk: Option<Box<IRStep>>,
1712    /// §Fase 119.n — `on_complete: { … }`, lowered. Run once, after the source
1713    /// closes, with the accumulated stream bound as `complete`.
1714    #[serde(default, skip_serializing_if = "Option::is_none")]
1715    pub on_complete: Option<Box<IRStep>>,
1716    /// §Fase 119.n.3 — `on_error: { … }`, lowered. Run when the SOURCE fails,
1717    /// with the failure bound as `error`. Never fires for a failure of the
1718    /// author's own handlers, nor for cancellation.
1719    #[serde(default, skip_serializing_if = "Option::is_none")]
1720    pub on_error: Option<Box<IRStep>>,
1721}
1722
1723// ── §Fase 120 — algebraic effects, lowered ──────────────────────────────────
1724//
1725// ⚠️ Names deliberately prefixed `IREffect*`. `axon-rs/src/effects/ir.rs`
1726// already owns `IRPerform` / `IRHandlerFrame` / `IRHandlerClause` / `IRResume`
1727// / `IRAbort` / `IRForward`, and `axon-rs` re-exports THIS module — an
1728// unprefixed name would collide there and, worse, would read as the same type.
1729// It is not: that module's `Instruction` alphabet has an INERT catch-all
1730// (`Passthrough`), so a handler body lowered onto it would silently execute
1731// nothing. D120.1 puts the effect machine in the DISPATCHER, whose alphabet is
1732// `IRFlowNode`; these nodes are that alphabet's five new members.
1733
1734/// `handle E { clauses } in { body }`, lowered — one handler frame.
1735#[derive(Debug, Clone, Serialize)]
1736pub struct IREffectHandle {
1737    pub node_type: &'static str,
1738    pub source_line: u32,
1739    pub source_column: u32,
1740    pub effect_names: Vec<String>,
1741    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1742    pub clauses: Vec<IREffectClause>,
1743    /// The `in { … }` block — ORDINARY flow nodes. This field is D120.1.
1744    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1745    pub body: Vec<IRFlowNode>,
1746    /// Per-flow frame identity, allocated in source order. Distinguishes two
1747    /// frames over the same effect so `forward` can name which one it left.
1748    pub frame_id: u32,
1749}
1750
1751/// One clause of an [`IREffectHandle`]: `Op(params) -> { body }`.
1752#[derive(Debug, Clone, Serialize)]
1753pub struct IREffectClause {
1754    pub operation_name: String,
1755    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1756    pub parameter_names: Vec<String>,
1757    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1758    pub body: Vec<IRFlowNode>,
1759    pub source_line: u32,
1760    pub source_column: u32,
1761}
1762
1763/// `perform E.Op(args)`, lowered.
1764///
1765/// `effect_name` is ALWAYS populated here even when the source wrote the bare
1766/// form — the IR generator resolves it against the declared catalog (D120.2).
1767/// A node that reached this point with an empty `effect_name` is a compiler
1768/// bug, and the dispatcher fails closed on it rather than searching by
1769/// operation name alone (which would let the handler stack pick a frame the
1770/// author never named).
1771#[derive(Debug, Clone, Serialize)]
1772pub struct IREffectPerform {
1773    pub node_type: &'static str,
1774    pub source_line: u32,
1775    pub source_column: u32,
1776    pub effect_name: String,
1777    pub operation_name: String,
1778    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1779    pub arguments: Vec<String>,
1780    /// True when the SOURCE wrote `perform Op(x)` and the catalog supplied the
1781    /// effect. Kept so a diagnostic can quote what the author actually typed.
1782    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1783    pub resolved_from_bare: bool,
1784}
1785
1786/// `resume(v)`, lowered — invoke the captured one-shot continuation (D2).
1787#[derive(Debug, Clone, Serialize)]
1788pub struct IREffectResume {
1789    pub node_type: &'static str,
1790    pub source_line: u32,
1791    pub source_column: u32,
1792    #[serde(default, skip_serializing_if = "String::is_empty")]
1793    pub value_expr: String,
1794}
1795
1796/// `abort(v)`, lowered — leave the enclosing `handle` without resuming.
1797#[derive(Debug, Clone, Serialize)]
1798pub struct IREffectAbort {
1799    pub node_type: &'static str,
1800    pub source_line: u32,
1801    pub source_column: u32,
1802    #[serde(default, skip_serializing_if = "String::is_empty")]
1803    pub value_expr: String,
1804}
1805
1806/// `forward E.Op(args)`, lowered (D12) — propagate to the next OUTER frame.
1807#[derive(Debug, Clone, Serialize)]
1808pub struct IREffectForward {
1809    pub node_type: &'static str,
1810    pub source_line: u32,
1811    pub source_column: u32,
1812    pub effect_name: String,
1813    pub operation_name: String,
1814    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1815    pub arguments: Vec<String>,
1816}
1817
1818#[derive(Debug, Clone, Serialize)]
1819pub struct IRNavigateStep {
1820    /// §Fase 119.f — per-navigation depth override → `NavConfig.d_max`.
1821    #[serde(default, skip_serializing_if = "Option::is_none")]
1822    pub depth: Option<i64>,
1823    pub node_type: &'static str,
1824    pub source_line: u32,
1825    pub source_column: u32,
1826    pub pix_ref: String,
1827    pub corpus_ref: String,
1828    pub query: String,
1829    pub trail_enabled: bool,
1830    pub output_name: String,
1831    /// §Fase 63.B — MDN corpus-graph navigation: the seed document (`from:`).
1832    #[serde(default, skip_serializing_if = "String::is_empty")]
1833    pub seed: String,
1834    /// §Fase 63.B — MDN navigation budget (`budget:` = max documents).
1835    #[serde(default, skip_serializing_if = "Option::is_none")]
1836    pub budget: Option<i64>,
1837    /// §Fase 66 (Q2) — column-scope filter for a `corpus from axonstore`. A raw
1838    /// filter expr threaded to `read_all_store_rows` → `stream_retrieve` for
1839    /// BOTH the documents and edges stores, so the sourced MDN graph is scoped
1840    /// to a sub-tenant column (`where: "tenant_id == '${tenant_id}'"`). The
1841    /// §37.d filter compiler resolves `${name}` → `$N` bind params (injection-
1842    /// safe). Empty = no column filter (axon-tenant RLS scope only).
1843    #[serde(default, skip_serializing_if = "String::is_empty")]
1844    pub where_expr: String,
1845}
1846
1847#[derive(Debug, Clone, Serialize)]
1848pub struct IRDrillStep {
1849    pub node_type: &'static str,
1850    pub source_line: u32,
1851    pub source_column: u32,
1852    pub pix_ref: String,
1853    pub subtree_path: String,
1854    pub query: String,
1855    pub output_name: String,
1856}
1857
1858#[derive(Debug, Clone, Serialize)]
1859pub struct IRTrailStep {
1860    pub node_type: &'static str,
1861    pub source_line: u32,
1862    pub source_column: u32,
1863    pub navigate_ref: String,
1864}
1865
1866#[derive(Debug, Clone, Serialize)]
1867pub struct IRCorroborateStep {
1868    pub node_type: &'static str,
1869    pub source_line: u32,
1870    pub source_column: u32,
1871    pub navigate_ref: String,
1872    pub output_name: String,
1873}
1874
1875#[derive(Debug, Clone, Serialize)]
1876pub struct IROtsApplyStep {
1877    pub node_type: &'static str,
1878    pub source_line: u32,
1879    pub source_column: u32,
1880    pub ots_name: String,
1881    pub target: String,
1882    pub output_type: String,
1883}
1884
1885#[derive(Debug, Clone, Serialize)]
1886pub struct IRMandateApplyStep {
1887    pub node_type: &'static str,
1888    pub source_line: u32,
1889    pub source_column: u32,
1890    pub mandate_name: String,
1891    pub target: String,
1892    pub output_type: String,
1893}
1894
1895#[derive(Debug, Clone, Serialize)]
1896pub struct IRAgentCall {
1897    pub node_type: &'static str,
1898    pub source_line: u32,
1899    pub source_column: u32,
1900    /// The declared `agent` this call invokes. Resolved against
1901    /// `DispatchCtx::agent_specs` at dispatch; an unresolved name fails CLOSED
1902    /// (the §111.f compute doctrine — an empty catalog runs nothing).
1903    pub agent_name: String,
1904    /// Positional §119.f.10 subjects. Resolved against the flow bindings at
1905    /// dispatch, so `TrendAnalyzer(Gather.output)` hands the agent the prior
1906    /// step's VALUE.
1907    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1908    pub arguments: Vec<String>,
1909}
1910
1911#[derive(Debug, Clone, Serialize)]
1912pub struct IRComputeApplyStep {
1913    pub node_type: &'static str,
1914    pub source_line: u32,
1915    pub source_column: u32,
1916    pub compute_name: String,
1917    pub arguments: Vec<String>,
1918    pub output_name: String,
1919}
1920
1921#[derive(Debug, Clone, Serialize)]
1922pub struct IRListenStep {
1923    pub node_type: &'static str,
1924    pub source_line: u32,
1925    pub source_column: u32,
1926    pub channel: String,
1927    /// §λ-L-E Fase 13 D4 — true ⇒ `channel` is a declared
1928    /// `IRChannel` ref; false ⇒ legacy string topic.
1929    pub channel_is_ref: bool,
1930    pub event_alias: String,
1931    /// §Fase 52.a — the handler body's lowered flow-steps, executed per event /
1932    /// scheduled tick by the §52.c runtime. `skip_serializing_if` keeps a
1933    /// bodyless `listen`'s JSON byte-identical to the pre-§52.a shape (D8).
1934    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1935    pub body: Vec<IRFlowNode>,
1936}
1937
1938#[derive(Debug, Clone, Serialize)]
1939pub struct IRDaemonStepNode {
1940    pub node_type: &'static str,
1941    pub source_line: u32,
1942    pub source_column: u32,
1943    pub daemon_ref: String,
1944}
1945
1946#[derive(Debug, Clone, Serialize)]
1947pub struct IRPersistStep {
1948    pub node_type: &'static str,
1949    pub source_line: u32,
1950    pub source_column: u32,
1951    pub store_name: String,
1952    /// §Fase 35.o — declared `{ col: value }` field block (value
1953    /// expressions kept raw; interpolated at runtime). Empty ⇒ the
1954    /// runtime writes the flow's user bindings (v1.30.0 fallback).
1955    pub fields: Vec<(String, String)>,
1956}
1957
1958#[derive(Debug, Clone, Serialize)]
1959pub struct IRRetrieveStep {
1960    pub node_type: &'static str,
1961    pub source_line: u32,
1962    pub source_column: u32,
1963    pub store_name: String,
1964    pub where_expr: String,
1965    pub alias: String,
1966    /// §Fase 67.b — `order_by:` clause (raw `"col [asc|desc], …"`).
1967    /// `skip_serializing_if` empty so a store that doesn't order never
1968    /// perturbs the serialized IR bytes (the §52 brief-#33 /
1969    /// [[feedback-boot-hydrate-self-heal]] no-drift discipline).
1970    #[serde(skip_serializing_if = "String::is_empty", default)]
1971    pub order_by: String,
1972    /// §Fase 67.b — `limit:` clause (raw `"100"` or `"${max}"`).
1973    #[serde(skip_serializing_if = "String::is_empty", default)]
1974    pub limit_expr: String,
1975    /// §Fase 76.d — `aggregate:` clause (raw, closed catalog: `count` /
1976    /// `sum(col)` / `avg(col)` / `min(col)` / `max(col)`).
1977    /// `skip_serializing_if` empty so a non-aggregating retrieve never
1978    /// perturbs the serialized IR bytes (the same §67.b no-drift
1979    /// discipline — zero IR-SHA drift for existing programs).
1980    #[serde(skip_serializing_if = "String::is_empty", default)]
1981    pub aggregate: String,
1982    /// §Fase 76.d — `group_by:` clause (raw `"col, col2"`).
1983    #[serde(skip_serializing_if = "String::is_empty", default)]
1984    pub group_by: String,
1985    /// §Fase 85.b — `cache:` reference (a declared `cache` name). Empty ⇒
1986    /// uncached. Elided when empty (IR-SHA stable for cache-less retrieves).
1987    #[serde(skip_serializing_if = "String::is_empty", default)]
1988    pub cache: String,
1989}
1990
1991#[derive(Debug, Clone, Serialize)]
1992pub struct IRMutateStep {
1993    pub node_type: &'static str,
1994    pub source_line: u32,
1995    pub source_column: u32,
1996    pub store_name: String,
1997    pub where_expr: String,
1998    /// §Fase 35.p — declared `{ col: value }` SET assignments (value
1999    /// expressions kept raw; interpolated at runtime). Empty ⇒ the
2000    /// runtime writes the flow's user bindings (v1.31.0 fallback).
2001    pub fields: Vec<(String, String)>,
2002}
2003
2004#[derive(Debug, Clone, Serialize)]
2005pub struct IRPurgeStep {
2006    pub node_type: &'static str,
2007    pub source_line: u32,
2008    pub source_column: u32,
2009    pub store_name: String,
2010    pub where_expr: String,
2011}
2012
2013#[derive(Debug, Clone, Serialize)]
2014pub struct IRTransactBlock {
2015    pub node_type: &'static str,
2016    pub source_line: u32,
2017    pub source_column: u32,
2018}
2019
2020/// §Fase 88.a — IR for the `warden` adversarial-analysis block. Carries the
2021/// target, the mandatory `scope_ref`, and the recursively-lowered body so the
2022/// enterprise engine (§88.f) can drive the analysis and §88.c can enforce the
2023/// authorization discipline.
2024#[derive(Debug, Clone, Serialize)]
2025pub struct IRWarden {
2026    pub node_type: &'static str,
2027    pub source_line: u32,
2028    pub source_column: u32,
2029    pub target: String,
2030    pub scope_ref: String,
2031    /// Nested flow-body IR (recursively lowered).
2032    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2033    pub body: Vec<IRFlowNode>,
2034}
2035
2036/// §Fase 88.a — IR for a `scope` authorization-policy declaration.
2037#[derive(Debug, Clone, Serialize)]
2038pub struct IRScope {
2039    pub node_type: &'static str,
2040    pub source_line: u32,
2041    pub source_column: u32,
2042    pub name: String,
2043    pub targets: Vec<String>,
2044    pub depth: String,
2045    pub approver: String,
2046}
2047
2048/// §Fase 51.a — IR for the `quant` cognitive block (Hilbert-space projection).
2049/// Mirrors `ast::QuantBlock`. Optional attributes serialize only when present
2050/// (`skip_serializing_if`) so a bare `quant {}` lowers to a minimal node and
2051/// the JSON stays diff-stable. The body lowers recursively, like `par` branches.
2052#[derive(Debug, Clone, Serialize)]
2053pub struct IRQuant {
2054    pub node_type: &'static str,
2055    pub source_line: u32,
2056    pub source_column: u32,
2057    /// Encoding scheme surface spelling (`amplitude` | `angle`); `None` = default.
2058    #[serde(default, skip_serializing_if = "Option::is_none")]
2059    pub encoding: Option<String>,
2060    /// Referenced `Observable` (Pauli-sum) name; `None` if unspecified.
2061    #[serde(default, skip_serializing_if = "Option::is_none")]
2062    pub observable: Option<String>,
2063    /// Register width n; `None` = inferred.
2064    #[serde(default, skip_serializing_if = "Option::is_none")]
2065    pub qubits: Option<i64>,
2066    /// Variational circuit depth L; `None` = backend default.
2067    #[serde(default, skip_serializing_if = "Option::is_none")]
2068    pub depth: Option<i64>,
2069    /// Projected-kernel bandwidth γ (D7); `None` = backend default.
2070    #[serde(default, skip_serializing_if = "Option::is_none")]
2071    pub bandwidth: Option<f64>,
2072    /// §Fase 69.c — data re-uploading layers L (`None`/`1` = no re-uploading).
2073    /// `skip_serializing_if` keeps a non-re-uploading block's IR byte-identical.
2074    #[serde(default, skip_serializing_if = "Option::is_none")]
2075    pub reupload: Option<i64>,
2076    /// Algebraic-effect backend tag (`quant_sim` | `qpu_native`).
2077    pub effect: String,
2078    /// Nested flow-body IR (recursively lowered).
2079    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2080    pub body: Vec<IRFlowNode>,
2081}
2082
2083// ── Tier 2 IR nodes ─────────────────────────────────────────────────────────
2084
2085#[derive(Debug, Clone, Serialize)]
2086pub struct IRAgent {
2087    pub node_type: &'static str,
2088    pub source_line: u32,
2089    pub source_column: u32,
2090    pub name: String,
2091    pub goal: String,
2092    pub tools: Vec<String>,
2093    pub memory_ref: String,
2094    pub strategy: String,
2095    pub on_stuck: String,
2096    pub shield_ref: String,
2097    pub max_iterations: Option<i64>,
2098    pub max_tokens: Option<i64>,
2099    pub max_time: String,
2100    pub max_cost: Option<f64>,
2101}
2102
2103/// §Fase 71.a — the lowered temporal execution-window guard. The runtime
2104/// (§71.b) evaluates `is_in_window(now, tz, allow)`; the daemon binding +
2105/// coalesced defer ledger are §71.c/d.
2106#[derive(Debug, Clone, Serialize)]
2107pub struct IRWindow {
2108    pub node_type: &'static str,
2109    pub source_line: u32,
2110    pub source_column: u32,
2111    pub name: String,
2112    pub timezone: String,
2113    pub allow: Vec<IRWindowSpan>,
2114    /// §Fase 71.e — excluded dates (holidays): ISO `YYYY-MM-DD` literals. A tick
2115    /// whose local date is in this set is OUTSIDE regardless of the hour spans.
2116    /// `skip_serializing_if` keeps a holiday-less window's JSON byte-identical.
2117    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2118    pub exclude: Vec<String>,
2119    pub on_outside: String,
2120}
2121
2122/// §Fase 71.a — one allowed day/hour span.
2123#[derive(Debug, Clone, Serialize)]
2124pub struct IRWindowSpan {
2125    pub day_start: String,
2126    pub day_end: String,
2127    pub hour_start: i64,
2128    pub hour_end: i64,
2129}
2130
2131/// §Fase 72.a — the `budget { … }` linear-effect rate limit lowered to IR. Each
2132/// quota gates a declared tool's dispatch on a renewable token bucket (the §72.b
2133/// `RateLease`); `on_exhausted` is the exhaustion policy.
2134#[derive(Debug, Clone, Serialize)]
2135pub struct IRBudget {
2136    pub node_type: &'static str,
2137    pub source_line: u32,
2138    pub source_column: u32,
2139    /// §Fase 114.a — the name of a **top-level** `budget`. Empty ⇒ the anonymous
2140    /// daemon-attached form. Skip-if-empty ⇒ every pre-§114 program serializes
2141    /// byte-identically (IR-SHA stability).
2142    #[serde(default, skip_serializing_if = "String::is_empty")]
2143    pub name: String,
2144    pub quotas: Vec<IRBudgetQuota>,
2145    /// `block` (fail-closed) | `defer` (reschedule via the §71 defer ledger) |
2146    /// `shed` (skip the call). An omitted policy lowers to `block` (the safe
2147    /// fail-closed default).
2148    pub on_exhausted: String,
2149}
2150
2151/// §Fase 72.a — one quota: `<kind>: <limit> per <period> on Tool(<effect>)`.
2152#[derive(Debug, Clone, Serialize)]
2153pub struct IRBudgetQuota {
2154    /// `rate` (renewable bucket) | `max` (windowed hard cap, no intra-window refill).
2155    pub kind: String,
2156    /// Token allowance per period (> 0, validated by `axon-T831`).
2157    pub limit: i64,
2158    /// `second` | `minute` | `hour` | `day` (closed catalog, `axon-T832`).
2159    pub period: String,
2160    /// The declared tool this quota governs (`on Tool(X)`; resolved by `axon-T830`).
2161    pub effect: String,
2162}
2163
2164#[derive(Debug, Clone, Serialize)]
2165pub struct IRShield {
2166    pub node_type: &'static str,
2167    pub source_line: u32,
2168    pub source_column: u32,
2169    pub name: String,
2170    pub scan: Vec<String>,
2171    pub strategy: String,
2172    pub on_breach: String,
2173    pub severity: String,
2174    pub quarantine: String,
2175    /// §8.2.h.3 — Python emits concrete 0, not null. AST keeps `Option<i64>`
2176    /// so the parser can distinguish "not set"; IR lowering collapses.
2177    pub max_retries: i64,
2178    pub confidence_threshold: f64,
2179    pub allow_tools: Vec<String>,
2180    pub deny_tools: Vec<String>,
2181    pub sandbox: bool,
2182    pub redact: Vec<String>,
2183    pub log: String,
2184    pub deflect_message: String,
2185    // `taint` exists on `ShieldDefinition` (AST) but Python's reference
2186    // IRShield doesn't emit it. Hidden from JSON output for §8.2.h parity.
2187    #[serde(skip)]
2188    pub taint: String,
2189    /// §ESK Fase 6.1 — covered regulatory classes for this shield.
2190    pub compliance: Vec<String>,
2191    /// §Fase 77.a — egress signing algorithm (`hmac_sha256`; empty = the
2192    /// shield does not sign). Elided from JSON when empty so every pre-§77
2193    /// program's IR stays byte-identical (zero IR-SHA drift).
2194    #[serde(skip_serializing_if = "String::is_empty")]
2195    pub sign: String,
2196}
2197
2198#[derive(Debug, Clone, Serialize)]
2199pub struct IRPix {
2200    pub node_type: &'static str,
2201    pub source_line: u32,
2202    pub source_column: u32,
2203    pub name: String,
2204    pub source: String,
2205    pub depth: Option<i64>,
2206    pub branching: Option<i64>,
2207    pub model: String,
2208}
2209
2210/// §Fase 62.0 — the audit-chain (`ledger`) IR node. Same shape as [`IRPix`]
2211/// but a DISTINCT node (`node_type: "ledger"`): a ledger binds a hash-linked
2212/// recorder to an audited surface (`source`), retaining `depth` rows under a
2213/// `branching`-factor Merkle tree, hashed with `model`. Kept separate from
2214/// `IRPix` so the navigator and the audit chain never alias on the wire.
2215#[derive(Debug, Clone, Serialize)]
2216pub struct IRLedger {
2217    pub node_type: &'static str,
2218    pub source_line: u32,
2219    pub source_column: u32,
2220    pub name: String,
2221    pub source: String,
2222    pub depth: Option<i64>,
2223    pub branching: Option<i64>,
2224    pub model: String,
2225}
2226
2227#[derive(Debug, Clone, Serialize)]
2228pub struct IRPsyche {
2229    pub node_type: &'static str,
2230    pub source_line: u32,
2231    pub source_column: u32,
2232    pub name: String,
2233    pub dimensions: Vec<String>,
2234    pub manifold_noise: Option<f64>,
2235    pub manifold_momentum: Option<f64>,
2236    pub safety_constraints: Vec<String>,
2237    pub quantum_enabled: Option<bool>,
2238    pub inference_mode: String,
2239}
2240
2241#[derive(Debug, Clone, Serialize)]
2242pub struct IRCorpus {
2243    pub node_type: &'static str,
2244    pub source_line: u32,
2245    pub source_column: u32,
2246    pub name: String,
2247    pub documents: Vec<String>,
2248    /// §Fase 63.A — typed weighted edges. Non-empty ⇒ this corpus is an MDN
2249    /// graph `C = (D, R, τ, ω, σ)`; the runtime builds an `mdn::Corpus` from it.
2250    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2251    pub relations: Vec<IRCorpusRelation>,
2252    /// §Fase 63.C — `adaptive: true` enables the memory endofunctor on this
2253    /// corpus's navigations.
2254    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2255    pub adaptive: bool,
2256    pub mcp_server: String,
2257    pub mcp_resource_uri: String,
2258    /// §Fase 64.A — when present, this is a DYNAMIC store-sourced MDN graph: the
2259    /// documents and typed edges are rows in two declared `axonstore`s and the
2260    /// runtime builds the `mdn::Corpus` from the live rows at navigate-time
2261    /// (per-tenant, growing). Absent ⇒ the static §63 corpus (byte-identical IR).
2262    #[serde(default, skip_serializing_if = "Option::is_none")]
2263    pub store_source: Option<IRCorpusStoreSource>,
2264}
2265
2266/// §Fase 63.A — a lowered MDN corpus-graph edge `(from, to, τ, ω)`.
2267#[derive(Debug, Clone, Serialize)]
2268pub struct IRCorpusRelation {
2269    pub etype: String,
2270    pub from: String,
2271    pub to: String,
2272    pub weight: f64,
2273}
2274
2275/// §Fase 64.A — the lowered store-mapping of a dynamic, `axonstore`-sourced MDN
2276/// corpus graph. `doc_store(doc_id, doc_title)` maps rows → nodes;
2277/// `edge_store(edge_from, edge_to, edge_type, edge_weight)` maps rows → typed
2278/// weighted edges. The runtime (§64.B) reads these stores tenant-scoped at
2279/// navigate-time to build the `mdn::Corpus`.
2280#[derive(Debug, Clone, Serialize)]
2281pub struct IRCorpusStoreSource {
2282    pub doc_store: String,
2283    pub doc_id: String,
2284    pub doc_title: String,
2285    pub edge_store: String,
2286    pub edge_from: String,
2287    pub edge_to: String,
2288    pub edge_type: String,
2289    pub edge_weight: String,
2290}
2291
2292/// §Fase 108.b — one compiled dataspace column. `column_type` is the
2293/// CANONICAL catalog name (`Text` / `Int` / `Float` / `Bool` /
2294/// `Timestamp` / `Json`) — aliases are resolved at IR generation, so
2295/// every downstream consumer (the engine's deploy hook, the §108.d PCC
2296/// class) reads one spelling.
2297#[derive(Debug, Clone, Serialize)]
2298pub struct IRDataspaceColumn {
2299    pub name: String,
2300    pub column_type: String,
2301}
2302
2303#[derive(Debug, Clone, Serialize)]
2304pub struct IRDataspace {
2305    pub node_type: &'static str,
2306    pub source_line: u32,
2307    pub source_column: u32,
2308    pub name: String,
2309    /// §Fase 108.b — the typed columnar schema (canonical type names).
2310    pub columns: Vec<IRDataspaceColumn>,
2311}
2312
2313#[derive(Debug, Clone, Serialize)]
2314pub struct IROts {
2315    pub node_type: &'static str,
2316    pub source_line: u32,
2317    pub source_column: u32,
2318    pub name: String,
2319    pub teleology: String,
2320    pub homotopy_search: String,
2321    pub loss_function: String,
2322}
2323
2324#[derive(Debug, Clone, Serialize)]
2325pub struct IRMandate {
2326    pub node_type: &'static str,
2327    pub source_line: u32,
2328    pub source_column: u32,
2329    pub name: String,
2330    pub constraint: String,
2331    pub kp: Option<f64>,
2332    pub ki: Option<f64>,
2333    pub kd: Option<f64>,
2334    pub tolerance: Option<f64>,
2335    pub max_steps: Option<i64>,
2336    /// §Fase 119.b — the declared stability hypotheses `(D, L)`, carried to the
2337    /// runtime as PROOF OBLIGATIONS: the static band check was conditional on
2338    /// them, so dispatch must verify them against the measured backend or
2339    /// refuse. `None` means nothing was statically promised — absence is
2340    /// visible, never defaulted.
2341    pub drift_bound: Option<f64>,
2342    pub lipschitz: Option<f64>,
2343    pub on_violation: String,
2344}
2345
2346#[derive(Debug, Clone, Serialize)]
2347pub struct IRCompute {
2348    pub node_type: &'static str,
2349    pub source_line: u32,
2350    pub source_column: u32,
2351    pub name: String,
2352    pub shield_ref: String,
2353    /// §Fase 111.f — typed parameters. ADDITIVE (`skip_serializing_if`), so every
2354    /// pre-111 program's IR JSON stays byte-identical.
2355    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2356    pub parameters: Vec<IRParameter>,
2357    /// §Fase 111.f — the declared result type.
2358    #[serde(default, skip_serializing_if = "String::is_empty")]
2359    pub return_type: String,
2360    /// §Fase 111.f — the lowered §70 expression the runtime evaluates natively.
2361    /// `None` ⇒ the apply is refused (axon-T941): a compute with no body cannot
2362    /// compute, and binding a placeholder string in its place is how the old
2363    /// runtime handed a downstream step the text `"compute:Name(args)"` where it
2364    /// expected a number.
2365    #[serde(default, skip_serializing_if = "Option::is_none")]
2366    pub body: Option<IRExpr>,
2367}
2368
2369#[derive(Debug, Clone, Serialize)]
2370pub struct IRDaemon {
2371    pub node_type: &'static str,
2372    pub source_line: u32,
2373    pub source_column: u32,
2374    pub name: String,
2375    pub goal: String,
2376    pub tools: Vec<String>,
2377    pub memory_ref: String,
2378    pub strategy: String,
2379    pub on_stuck: String,
2380    pub shield_ref: String,
2381    /// §Fase 71.c — the `window:` temporal binding (a `window` primitive name).
2382    /// Empty ⇒ no temporal guard; `skip_serializing_if` keeps a windowless
2383    /// daemon's JSON byte-identical (D8 zero-drift).
2384    #[serde(default, skip_serializing_if = "String::is_empty")]
2385    pub window_ref: String,
2386    /// §Fase 72.a — the `budget { … }` linear-effect rate limit. `None` ⇒ no
2387    /// budget; `skip_serializing_if` keeps a budgetless daemon's JSON
2388    /// byte-identical (D8 zero-drift).
2389    #[serde(default, skip_serializing_if = "Option::is_none")]
2390    pub budget: Option<IRBudget>,
2391    pub max_tokens: Option<i64>,
2392    pub max_time: String,
2393    pub max_cost: Option<f64>,
2394    /// §Fase 52.a — the daemon's `listen` listeners (channel + alias + handler
2395    /// body). Pre-§52.a these were DROPPED at lowering (the IR daemon carried no
2396    /// listeners at all); now they survive so the §52.c runtime can mount + run
2397    /// them and the §52.d enterprise supervisor can extract them per-tenant.
2398    /// `skip_serializing_if` keeps a listenerless daemon's JSON unchanged (D8).
2399    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2400    pub listeners: Vec<IRListenStep>,
2401    /// §Fase 52.d — the capability scope the daemon's runs are confined to
2402    /// (`requires: [cap, …]`). The enterprise supervisor mints a per-run
2403    /// principal scoped to exactly these. `skip_serializing_if` keeps a
2404    /// requires-less daemon's JSON byte-identical (D8).
2405    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2406    pub requires_capabilities: Vec<String>,
2407}
2408
2409// ── §Fase 87 — the long-horizon autonomous research primitive ────────────────
2410
2411/// §Fase 87.a — a compiled `savant` (long-horizon autonomous research
2412/// primitive). A governed orchestrator: the IR carries the declared surface so
2413/// the enterprise active-inference engine (§87.h+) can drive the FEP loop, and
2414/// the §87.c checker can bind `memory` to a declared store, `budget` to a §72
2415/// linear budget, and the body to a §79 interruptible session.
2416#[derive(Debug, Clone, Serialize)]
2417pub struct IRSavant {
2418    pub node_type: &'static str,
2419    pub source_line: u32,
2420    pub source_column: u32,
2421    pub name: String,
2422    pub domain: String,
2423    #[serde(default, skip_serializing_if = "Option::is_none")]
2424    pub cognition: Option<IRSavantCognition>,
2425    #[serde(default, skip_serializing_if = "Option::is_none")]
2426    pub memory: Option<IRSavantMemory>,
2427    #[serde(default, skip_serializing_if = "Option::is_none")]
2428    pub budget: Option<IRSavantBudget>,
2429    pub mandates: Vec<IRSavantMandate>,
2430}
2431
2432/// §Fase 87.a — the compiled `cognition { … }` sub-block (active-inference
2433/// engine parameters).
2434#[derive(Debug, Clone, Serialize)]
2435pub struct IRSavantCognition {
2436    /// `standard | deep | hyper` — HRR dimensionality tier (validated §87.b).
2437    pub depth: String,
2438    /// Expected-Free-Energy convergence bound (`> 0`, §87.b). `None` ⇒ default.
2439    #[serde(default, skip_serializing_if = "Option::is_none")]
2440    pub entropic_threshold: Option<f64>,
2441    /// `low | med | high` — explore/exploit balance (validated §87.b).
2442    pub divergence: String,
2443}
2444
2445/// §Fase 87.a — the compiled `memory { … }` sub-block (retention binding).
2446#[derive(Debug, Clone, Serialize)]
2447pub struct IRSavantMemory {
2448    /// A declared `memory`/`corpus` name (resolved §87.c). Empty ⇒ ephemeral.
2449    pub backend: String,
2450    /// Whether to index the corpus as a simplicial-complex graph (topological
2451    /// β_n reading).
2452    pub corpus_graph: bool,
2453    /// Per-tenant tensor partitioning level (enforced by the enterprise engine).
2454    pub isolation_level: String,
2455}
2456
2457/// §Fase 87.a — the compiled `budget { … }` sub-block (compute ceiling, bound to
2458/// a §72 linear budget in §87.c).
2459#[derive(Debug, Clone, Serialize)]
2460pub struct IRSavantBudget {
2461    /// Hard ceiling on FEP-loop iterations before the savant pauses.
2462    #[serde(default, skip_serializing_if = "Option::is_none")]
2463    pub max_iterations: Option<i64>,
2464    /// Hard ceiling on `synth` (§87.d) tool-creation events per mandate.
2465    #[serde(default, skip_serializing_if = "Option::is_none")]
2466    pub max_tool_synth: Option<i64>,
2467}
2468
2469/// §Fase 87.a — a compiled `mandate <Name> { … }` sub-block (one research goal).
2470#[derive(Debug, Clone, Serialize)]
2471pub struct IRSavantMandate {
2472    pub name: String,
2473    pub objective: String,
2474    pub output_type: String,
2475}
2476
2477// ── §Fase 99.b — Native Document Synthesis IR ─────────────────────────────────
2478
2479/// §Fase 99.b — a compiled document declaration. The runtime `DocumentRenderer`
2480/// tool serialises this to deterministic OOXML bytes (§99.e); the
2481/// `DocumentProvenanceSoundness` PCC class (§99.d) re-derives the barrier from
2482/// it. `blocks` is the closed-catalog body tree.
2483#[derive(Debug, Clone, Serialize)]
2484pub struct IRDocument {
2485    pub node_type: &'static str,
2486    pub source_line: u32,
2487    pub source_column: u32,
2488    pub name: String,
2489    /// `docx | pptx | xlsx`.
2490    pub target: String,
2491    #[serde(default, skip_serializing_if = "String::is_empty")]
2492    pub template: String,
2493    #[serde(default, skip_serializing_if = "String::is_empty")]
2494    pub provenance: String,
2495    /// The propagated effect row (`io`, `storage`, `sensitive:*`, `legal:*`).
2496    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2497    pub effect_row: Vec<String>,
2498    /// §Fase 99.d — the enclosing `epistemic { mode: … }` at compile time
2499    /// (`believe`/`know` vouch the whole document is ≥ believe, satisfying the
2500    /// assertion-laundering barrier without per-field `attribute:`). Empty at
2501    /// top level. Recorded so the `DocumentProvenanceSoundness` PCC class
2502    /// re-derives the barrier identically (no false refutation).
2503    #[serde(default, skip_serializing_if = "String::is_empty")]
2504    pub epistemic_mode: String,
2505    pub blocks: Vec<IRDocBlock>,
2506}
2507
2508/// §Fase 99.b — one compiled document block. `fields` preserves declaration
2509/// order (a `Vec` of `(name, value)`), so the render is deterministic.
2510#[derive(Debug, Clone, Serialize)]
2511pub struct IRDocBlock {
2512    pub kind: String,
2513    pub fields: Vec<IRDocField>,
2514    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2515    pub children: Vec<IRDocBlock>,
2516}
2517
2518/// §Fase 99.b — a compiled `(field, value)` pair. `kind` tags the value shape
2519/// (`text`|`ref`|`list`|`int`|`bool`) so the renderer + the barrier can
2520/// discriminate a literal from a flow-value reference without re-parsing.
2521#[derive(Debug, Clone, Serialize)]
2522pub struct IRDocField {
2523    pub name: String,
2524    /// `text | ref | list | int | bool`.
2525    pub kind: &'static str,
2526    #[serde(default, skip_serializing_if = "String::is_empty")]
2527    pub value: String,
2528    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2529    pub items: Vec<String>,
2530}
2531
2532// ── §Fase 105 — Governed CRM Delivery IR ─────────────────────────────────────
2533
2534/// §Fase 105 — a compiled delivery declaration. The runtime delivery dispatch
2535/// (`axon::delivery`) transduces this to the configured CRM engine; the
2536/// `DeliveryProvenanceSoundness` PCC class (T920) re-derives the barrier from it.
2537/// `ops` is the closed-catalog operation list.
2538/// §Fase 110 — the compiled `notify` declaration. `epistemic_mode`
2539/// records the enclosing vouch (the §99.d/§105 discipline) so T933
2540/// re-derives identically at deploy (PCC `NotificationProvenanceSoundness`).
2541#[derive(Debug, Clone, Serialize)]
2542pub struct IRNotify {
2543    pub node_type: &'static str,
2544    pub source_line: u32,
2545    pub source_column: u32,
2546    pub name: String,
2547    pub channel: String,
2548    /// The §94 secret-class ref (the recipient value NEVER rides the IR).
2549    pub to_secret: String,
2550    pub template: String,
2551    pub window: String,
2552    #[serde(default, skip_serializing_if = "String::is_empty")]
2553    pub provenance: String,
2554    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2555    pub effects: Vec<String>,
2556    #[serde(default, skip_serializing_if = "String::is_empty")]
2557    pub epistemic_mode: String,
2558}
2559
2560#[derive(Debug, Clone, Serialize)]
2561pub struct IRDeliver {
2562    pub node_type: &'static str,
2563    pub source_line: u32,
2564    pub source_column: u32,
2565    pub name: String,
2566    /// `crm`.
2567    pub target: String,
2568    /// `attached | cleared` (empty ⇒ `attached`). How field provenance crosses
2569    /// the boundary (D105.2) — the T920 barrier's subject.
2570    #[serde(default, skip_serializing_if = "String::is_empty")]
2571    pub provenance: String,
2572    /// The per-tenant credential key (§94 custody — resolved at dispatch, never
2573    /// in cognition).
2574    #[serde(default, skip_serializing_if = "String::is_empty")]
2575    pub secret: String,
2576    /// The propagated effect row (`web`, `sensitive:*`, `legal:*`).
2577    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2578    pub effect_row: Vec<String>,
2579    /// The enclosing `epistemic { mode: … }` at compile time (`believe`/`know`
2580    /// vouch the delivered values are ≥ believe, satisfying the T920 barrier for
2581    /// a `provenance: cleared` delivery). Empty at top level. Recorded so the
2582    /// `DeliveryProvenanceSoundness` PCC class re-derives the barrier identically.
2583    #[serde(default, skip_serializing_if = "String::is_empty")]
2584    pub epistemic_mode: String,
2585    pub ops: Vec<IRDeliverOp>,
2586}
2587
2588/// §Fase 105 — one compiled delivery operation. `fields` preserves declaration
2589/// order (a `Vec` of `(name, value)`) so the transduced request is deterministic.
2590/// Reuses [`IRDocField`] — the same `(name, kind, value)` shape a document block
2591/// field carries, so the barrier + transducer discriminate a literal from a
2592/// flow-value `ref` without re-parsing.
2593#[derive(Debug, Clone, Serialize)]
2594pub struct IRDeliverOp {
2595    /// `upsert_contact | create_deal | add_note`.
2596    pub kind: String,
2597    pub fields: Vec<IRDocField>,
2598}
2599
2600/// §Fase 87.d — a compiled `synth` dynamic tool-synthesis policy. The IR carries
2601/// the safety envelope so the enterprise Extism/WASM executor (§87.j) enforces
2602/// it; OSS ships a deny-by-default `SynthBackend` that refuses execution.
2603#[derive(Debug, Clone, Serialize)]
2604pub struct IRSynth {
2605    pub node_type: &'static str,
2606    pub source_line: u32,
2607    pub source_column: u32,
2608    pub name: String,
2609    pub target: String,
2610    pub risk: String,
2611    #[serde(default, skip_serializing_if = "String::is_empty")]
2612    pub language: String,
2613    pub sandbox: String,
2614    /// `required | none`; an omitted policy lowers to `required` (fail-closed).
2615    pub review: String,
2616    #[serde(default, skip_serializing_if = "Option::is_none")]
2617    pub max_lines: Option<i64>,
2618}
2619
2620// ── §Fase 53 — Closed-catalog extension mechanism ────────────────────────────
2621
2622/// §Fase 53 — one compiled member of an `extension`. For `effects`
2623/// the `name` is a provenance base; `default_confidence` is a CEILING
2624/// (§53.d tainted-overriding). Metadata is elided from JSON when absent
2625/// so the serialised shape stays minimal once the `extensions` field is
2626/// un-skipped alongside the Python IR mirror.
2627#[derive(Debug, Clone, Serialize)]
2628pub struct IRExtensionMember {
2629    pub name: String,
2630    #[serde(default, skip_serializing_if = "Option::is_none")]
2631    pub semantics: Option<String>,
2632    #[serde(default, skip_serializing_if = "Option::is_none")]
2633    pub default_confidence: Option<f64>,
2634}
2635
2636/// §Fase 53 — a compiled `extension` declaration. Rides in the IR (and,
2637/// once un-skipped, the proof bundle) so an independent PCC verifier
2638/// re-derives `is_known_base` against the artifact's own extensions
2639/// (soundness invariant #1). `category` ∈ {`effects`, `scan`} — the
2640/// type-checker (§53.c) enforces the closed category + no-shadowing +
2641/// provenance-class invariants before this IR is trusted.
2642#[derive(Debug, Clone, Serialize)]
2643pub struct IRExtension {
2644    pub node_type: &'static str,
2645    pub source_line: u32,
2646    pub source_column: u32,
2647    pub name: String,
2648    pub category: String,
2649    pub members: Vec<IRExtensionMember>,
2650}
2651
2652#[derive(Debug, Clone, Serialize)]
2653pub struct IRAxonStore {
2654    pub node_type: &'static str,
2655    pub source_line: u32,
2656    pub source_column: u32,
2657    pub name: String,
2658    pub backend: String,
2659    /// The DSN. **This is the field that actually runs.**
2660    ///
2661    /// `connection:` → `resolve_dsn` → a real sqlx `PgPool`. It is the sole DSN
2662    /// source for every store op in every deployed flow; there is no
2663    /// global-pool fallback. §113's census established this, and it is why
2664    /// §113 is delicate: moving authority to `resource` moves it *away* from
2665    /// the one field that governs anything, *toward* the half that governs
2666    /// nothing. A `resource:` that merely renames this string would be the
2667    /// nominal link — wired and hollow.
2668    ///
2669    /// §113: still parsed, but **deprecated in favour of [`Self::resource_ref`]**,
2670    /// and a store declared this way is INELIGIBLE for `lease` / `observe` /
2671    /// `reconcile`. *You cannot govern what you did not declare.*
2672    pub connection: String,
2673    /// §Fase 113 — the `resource` this store runs on (`axonstore U { resource: Db }`).
2674    ///
2675    /// When present, the store DERIVES its DSN (`resource.endpoint`), its pool
2676    /// size (`resource.capacity` — a knob that did not exist before §113; the
2677    /// pool was hardcoded at 10), and its sharing discipline
2678    /// (`resource.lifetime`, `axon-T941`) from the resource. **That derivation
2679    /// — not the reference — is what makes this real.**
2680    ///
2681    /// Empty ⇒ the legacy un-resourced form. Skip-if-empty ⇒ every pre-§113
2682    /// store serializes byte-identically (the §94.a `class` precedent).
2683    #[serde(default, skip_serializing_if = "String::is_empty")]
2684    pub resource_ref: String,
2685    pub confidence_floor: Option<f64>,
2686    pub isolation: String,
2687    pub on_breach: String,
2688    /// §Fase 35.j (D11) — Pillar IV: the capability slug required to
2689    /// access this store (empty = no gate).
2690    pub capability: String,
2691    /// §Fase 94.a — the secret-class prefix of a `backend: secrets`
2692    /// metadata store (`rotation_without_revelation`). Non-empty ⇔
2693    /// `backend == "secrets"` (both directions enforced by `axon-T900`
2694    /// before the IR ships). Elided from the wire when empty — every
2695    /// pre-§94 store serializes byte-identically (IR-SHA stability).
2696    #[serde(default, skip_serializing_if = "String::is_empty")]
2697    pub class: String,
2698    /// §Fase 38.b (D1) — the OPTIONAL column-schema declaration. Three
2699    /// closed forms (inline / manifest-ref / env-var). `None` means the
2700    /// 37.x runtime+deploy path applies verbatim (D5 absolute). The
2701    /// §38.d / §38.e type-checker proves every store reference against
2702    /// this when present. §Fase 94.a: for a `backend: secrets` store the
2703    /// generator synthesizes the FIXED metadata schema here (the artifact
2704    /// is self-describing; PCC and the deploy gate re-derive against it).
2705    #[serde(default, skip_serializing_if = "Option::is_none")]
2706    pub column_schema: Option<IRStoreColumnSchema>,
2707}
2708
2709/// §Fase 38.b (D1) — IR mirror of [`crate::store_schema::StoreColumnSchema`].
2710/// Serializes as a tagged union: `{"form": "inline" | "manifest_ref" |
2711/// "env_var", …}`.
2712#[derive(Debug, Clone, Serialize)]
2713#[serde(tag = "form", rename_all = "snake_case")]
2714pub enum IRStoreColumnSchema {
2715    Inline { columns: Vec<IRStoreColumn> },
2716    ManifestRef { qualified_name: String },
2717    EnvVar { var_name: String },
2718}
2719
2720/// §Fase 38.b (D1) — IR mirror of [`crate::store_schema::StoreColumn`].
2721/// The serialized `col_type` is the canonical PascalCase name (e.g.
2722/// `"Uuid"`, `"Int"`, `"Timestamptz"`).
2723#[derive(Debug, Clone, Serialize)]
2724pub struct IRStoreColumn {
2725    pub name: String,
2726    pub col_type: String,
2727    #[serde(default, skip_serializing_if = "is_false")]
2728    pub primary_key: bool,
2729    #[serde(default, skip_serializing_if = "is_false")]
2730    pub auto_increment: bool,
2731    #[serde(default, skip_serializing_if = "is_false")]
2732    pub not_null: bool,
2733    #[serde(default, skip_serializing_if = "is_false")]
2734    pub unique: bool,
2735    #[serde(default, skip_serializing_if = "String::is_empty")]
2736    pub default_value: String,
2737    /// §Fase 38.x.c (D2, D5) — `true` iff the column is declared with
2738    /// `GENERATED ALWAYS AS IDENTITY` or `GENERATED BY DEFAULT AS
2739    /// IDENTITY`. Distinct from `auto_increment` (legacy SERIAL via
2740    /// `nextval(...)` default). `skip_serializing_if` keeps IR JSON
2741    /// byte-identical to v1.38.2 for any column where `identity = false`.
2742    #[serde(default, skip_serializing_if = "is_false")]
2743    pub identity: bool,
2744    /// §Fase 73.f (D1) — `true` iff the column carries the `index`
2745    /// declaration. Surfaced into the IR so the deployment layer (the
2746    /// enterprise deploy gate) SEES the index as a declared capability and
2747    /// can materialize it (a GIN path index for a `Json`/`Jsonb` column, a
2748    /// b-tree otherwise) — never a silent out-of-band DBA action.
2749    /// `skip_serializing_if` keeps IR JSON byte-identical for any column
2750    /// where `indexed = false`.
2751    #[serde(default, skip_serializing_if = "is_false")]
2752    pub indexed: bool,
2753    /// §Fase 73.g (D1) — the OPTIONAL `Json<T>` shape-lens struct name on a
2754    /// `Json`/`Jsonb` column (`payload: Json<UserEvent>` → `Some("UserEvent")`).
2755    /// Surfaced into the IR so the PCC `JsonShapeSoundness` proof can
2756    /// RE-DERIVE, from the artifact alone, that every lens shape resolves
2757    /// to a declared struct `type` — the §73.a/§73.e lens well-formedness
2758    /// made an independently-verifiable proof object. `skip_serializing_if`
2759    /// keeps IR JSON byte-identical for any column with no shape lens.
2760    #[serde(default, skip_serializing_if = "Option::is_none")]
2761    pub json_shape: Option<String>,
2762}
2763
2764#[inline]
2765fn is_false(b: &bool) -> bool {
2766    !*b
2767}
2768
2769#[derive(Debug, Clone, Serialize)]
2770pub struct IRAxonEndpoint {
2771    pub node_type: &'static str,
2772    pub source_line: u32,
2773    pub source_column: u32,
2774    pub name: String,
2775    pub method: String,
2776    pub path: String,
2777    pub body_type: String,
2778    pub execute_flow: String,
2779    pub output_type: String,
2780    pub shield_ref: String,
2781    /// §8.2.h.3 — Python emits concrete `0`; AST stays `Option<i64>`.
2782    pub retries: i64,
2783    pub timeout: String,
2784    /// §ESK Fase 6.1 — κ regulatory class on the boundary.
2785    pub compliance: Vec<String>,
2786    /// §Fase 37.y (D1) — Path parameter names extracted from the
2787    /// `path:` string. Mirrors `AxonEndpointDefinition.path_params`.
2788    /// **`skip_serializing_if = Vec::is_empty`** so a pre-v1.38.5 IR
2789    /// JSON snapshot (without the field) is byte-identical to a
2790    /// v1.38.5 IR JSON for the same endpoint — D5 backwards-compat
2791    /// absolute. The runtime + adopter tools that consume the IR
2792    /// JSON parse `path_params` as an absent key → empty Vec via
2793    /// serde's `default` semantics.
2794    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2795    pub path_params: Vec<String>,
2796    /// §Fase 37.y (D2) — Query parameters from the inline
2797    /// `query: { … }` block. Mirrors `AxonEndpointDefinition.query_params`
2798    /// using `IRTypeField` (shared with body type fields → uniform
2799    /// downstream tooling). **`skip_serializing_if = Vec::is_empty`**
2800    /// — same D5 IR-JSON byte-identity guarantee as `path_params`.
2801    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2802    pub query_params: Vec<IRTypeField>,
2803    /// §Fase 51.x — capability scopes the request bearer must hold
2804    /// (the `requires: [scope.dotted]` declaration, §Fase 32.g). Mirror
2805    /// of `AxonEndpointDefinition.requires_capabilities`, lowered into
2806    /// the IR so the PCC CapabilityContainment property can prove that
2807    /// the stores this endpoint's flow reaches are all covered by the
2808    /// declared requires. **`skip_serializing_if = Vec::is_empty`** so a
2809    /// pre-§51.x IR-JSON snapshot (no `requires:`) stays byte-identical
2810    /// (D5 backwards-compat — empty key parses back to empty Vec).
2811    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2812    pub requires_capabilities: Vec<String>,
2813    /// §Fase 83.a — the `cors: <Name>` reference, or `""` when absent
2814    /// (D83.5: no CORS headers, ever). NEW field on an EXISTING struct —
2815    /// `skip_serializing_if` (not `shield_ref`'s bare/always-emitted
2816    /// historical shape) so a cors-less endpoint's IR stays byte-identical
2817    /// to pre-§83 (zero IR-SHA drift — the standing §76.d discipline).
2818    #[serde(default, skip_serializing_if = "String::is_empty")]
2819    pub cors_ref: String,
2820    /// §Fase 89.a — the explicit authorization-coverage opt-out lowered into
2821    /// the IR so the enterprise runtime (§89.d) and the PCC
2822    /// `AuthorizationCoverage` witness (§89.c) can read it. `false` (the
2823    /// default + the common case) elides from JSON via `is_false` so a
2824    /// pre-§89 IR-JSON snapshot stays byte-identical (zero IR-SHA drift —
2825    /// the standing §76.d discipline).
2826    #[serde(default, skip_serializing_if = "is_false")]
2827    pub public: bool,
2828}
2829
2830// ── §λ-L-E Fase 13 — Mobile Typed Channels IR ───────────────────────────────
2831
2832/// Compiled `channel Name { … }` declaration.
2833///
2834/// Direct port of `axon.compiler.ir_nodes.IRChannel`.  Lives in
2835/// `IRProgram.channels`; emit/publish/discover reductions embed in
2836/// their containing flow/listener (paper §3 + §4 — π-calc prefix
2837/// discipline preserved structurally, not lifted to top-level ops).
2838#[derive(Debug, Clone, Serialize)]
2839pub struct IRChannel {
2840    pub node_type: &'static str,
2841    pub source_line: u32,
2842    pub source_column: u32,
2843    pub name: String,
2844    pub message: String, // surface spelling — Order | Channel<Order> | …
2845    pub qos: String,
2846    pub lifetime: String,
2847    pub persistence: String,
2848    pub shield_ref: String,
2849    /// §Fase 77.b — non-empty ⇒ some `publish <this> within <Shield>` site
2850    /// referenced a SIGNING shield: the channel is an EGRESS channel and
2851    /// its durable events are signed-deliverable to registered external
2852    /// subscribers under this algorithm (first publish site wins;
2853    /// deterministic — the catalog has one algorithm in v1). Elided from
2854    /// JSON when empty (zero IR-SHA drift for pre-§77 programs).
2855    #[serde(skip_serializing_if = "String::is_empty")]
2856    pub egress_sign: String,
2857}
2858
2859/// §Fase 41.b — compiled typed WebSocket transport. `protocol` names the
2860/// `session` it carries; `backpressure_credit` is the typed-resource window
2861/// (`null` if unspecified). axon-rs realises the endpoint over a `tokio` WS,
2862/// crediting/decrementing the window per §4.2 of the paper.
2863#[derive(Debug, Clone, Serialize)]
2864pub struct IRSocket {
2865    pub node_type: &'static str,
2866    pub source_line: u32,
2867    pub source_column: u32,
2868    pub name: String,
2869    pub protocol: String,
2870    pub backpressure_credit: Option<i64>,
2871    pub reconnect: bool,
2872    pub legal_basis: Option<String>,
2873}
2874
2875/// §Fase 80.b — compiled outbound vendor connection (the client dual of
2876/// [`IRSocket`]). `protocol`/`role` bind the axon-facing session interface;
2877/// `resolve`/`secret` are per-tenant config keys (never literals — T850);
2878/// `map` is the compile-time-total wire↔session projection (T849). Optional
2879/// fields elide when absent so the IR shape is purely additive.
2880#[derive(Debug, Clone, Serialize)]
2881pub struct IRUpstream {
2882    pub node_type: &'static str,
2883    pub source_line: u32,
2884    pub source_column: u32,
2885    pub name: String,
2886    pub transport: String,
2887    pub protocol: String,
2888    pub role: String,
2889    pub resolve: String,
2890    /// §Fase 114.u — the `resource` this upstream's channel rides. When set,
2891    /// `resolve` above was DERIVED from the resource's `endpoint` at LOWERING
2892    /// (the §114 shield-egress discipline: derivation stamped into the
2893    /// artifact reaches every dial path by construction — no per-site wiring
2894    /// to forget) and `capacity` below carries the resource's bound. Elided
2895    /// when empty — every pre-§114.u upstream serializes byte-identically.
2896    #[serde(default, skip_serializing_if = "String::is_empty")]
2897    pub resource_ref: String,
2898    /// §Fase 114.u — **max concurrent connection INSTANCES** of this upstream
2899    /// (from `resource.capacity`). Frames are already flow-controlled by
2900    /// `backpressure_credit`; this bounds CONNECTIONS. The runtime holds a
2901    /// per-process semaphore permit for the life of each dialed handle —
2902    /// the same in-memory/per-process bound §114.e documented for tools.
2903    #[serde(default, skip_serializing_if = "Option::is_none")]
2904    pub capacity: Option<i64>,
2905    pub secret: String,
2906    pub auth_kind: String,
2907    #[serde(default, skip_serializing_if = "Option::is_none")]
2908    pub auth_name: Option<String>,
2909    #[serde(default, skip_serializing_if = "Option::is_none")]
2910    pub auth_prefix: Option<String>,
2911    pub map: Vec<IRUpstreamMapRule>,
2912    #[serde(default, skip_serializing_if = "Option::is_none")]
2913    pub reconnect: Option<IRUpstreamReconnect>,
2914    #[serde(default, skip_serializing_if = "Option::is_none")]
2915    pub overflow: Option<String>,
2916    #[serde(default, skip_serializing_if = "Option::is_none")]
2917    pub backpressure_credit: Option<i64>,
2918    /// §80.f — the `Preset@vN` reference this declaration was expanded from
2919    /// (provenance for the compliance reviewer); absent for hand-written ones.
2920    #[serde(default, skip_serializing_if = "Option::is_none")]
2921    pub preset: Option<String>,
2922}
2923
2924/// §Fase 80.b — one compiled `map:` projection rule.
2925#[derive(Debug, Clone, Serialize)]
2926pub struct IRUpstreamMapRule {
2927    pub node_type: &'static str,
2928    pub direction: String,
2929    pub message: String,
2930    pub framing: String,
2931    #[serde(default, skip_serializing_if = "Option::is_none")]
2932    pub tag: Option<String>,
2933    #[serde(default, skip_serializing_if = "Option::is_none")]
2934    pub when_field: Option<String>,
2935    #[serde(default, skip_serializing_if = "Option::is_none")]
2936    pub when_value: Option<String>,
2937}
2938
2939/// §Fase 80.b — compiled reconnect policy (all three fields required by the
2940/// parser — a reconnection policy with a hole is not a policy).
2941#[derive(Debug, Clone, Serialize)]
2942pub struct IRUpstreamReconnect {
2943    pub backoff_ms: i64,
2944    pub max_attempts: i64,
2945    pub on_exhausted: String,
2946}
2947
2948/// §Fase 83.a — a named, referenced browser-origin policy. Mirrors
2949/// `IRShield`'s field-for-field shape; consumed by `IRAxonEndpoint.cors_ref`.
2950/// Wildcard+credentials (T853), origin-glob shape (T854), and closed-method
2951/// (T855) violations are all rejected before this node is ever lowered — the
2952/// checker re-derives the same closed catalogs at deploy time (§83.c,
2953/// `CorsPolicyConsistency`), so an IR that reaches the runtime is already
2954/// proven consistent.
2955#[derive(Debug, Clone, Serialize)]
2956pub struct IRCors {
2957    pub node_type: &'static str,
2958    pub source_line: u32,
2959    pub source_column: u32,
2960    pub name: String,
2961    pub allow_origins: Vec<String>,
2962    pub allow_methods: Vec<String>,
2963    pub allow_headers: Vec<String>,
2964    pub allow_credentials: bool,
2965    /// Duration literal (`"3600s"`) — same string-carries-the-unit
2966    /// convention as `axonendpoint.timeout`; the consumer (enterprise's
2967    /// dynamic CORS middleware) parses it into seconds at request time.
2968    #[serde(default, skip_serializing_if = "Option::is_none")]
2969    pub max_age: Option<String>,
2970    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2971    pub expose_headers: Vec<String>,
2972}
2973
2974/// §Fase 85.b — compiled `cache` policy. The checker (§85.c) re-derives the
2975/// same laws at the deploy gate (`CacheSoundness`), so an IR that reaches the
2976/// runtime is already proven sound (one default max, non-pure ⇒ finite ttl,
2977/// references resolve). Every optional field is `skip_serializing_if` so a
2978/// bundle using `cache` only pays IR bytes for what it declares, and a bundle
2979/// with no `cache` never emits a `caches` key (IR-SHA stable, §76.d).
2980#[derive(Debug, Clone, Serialize)]
2981pub struct IRCache {
2982    pub node_type: &'static str,
2983    pub source_line: u32,
2984    pub source_column: u32,
2985    pub name: String,
2986    /// `"redis"` | `"in_process"`; empty ⇒ runtime default (`in_process`).
2987    #[serde(default, skip_serializing_if = "String::is_empty")]
2988    pub backend: String,
2989    /// Duration literal (`"10s"`) — same string-carries-the-unit convention as
2990    /// `cors.max_age`. `None` ⇒ cache-forever (sound only for a `pure` cache).
2991    #[serde(default, skip_serializing_if = "Option::is_none")]
2992    pub ttl: Option<String>,
2993    /// The parameter-name subset forming the key; empty ⇒ all bound params.
2994    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2995    pub key_params: Vec<String>,
2996    /// `true` ⇒ auto-covers every eligible tool (at most one per module).
2997    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2998    pub default_policy: bool,
2999    /// Effect classes this cache memoises; empty ⇒ `["pure"]`.
3000    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3001    pub apply_to_effects: Vec<String>,
3002    /// Channel names whose `emit` flushes this cache's namespace.
3003    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3004    pub invalidate_on: Vec<String>,
3005}
3006
3007/// Compiled emit step — `c⟨v⟩.P` (Chan-Output / Chan-Mobility).
3008///
3009/// `value_is_channel = true` ⇒ resolved at lowering time as a channel
3010/// handle (second-order mobility, paper §3.2); the runtime dispatches
3011/// on this flag without re-resolving symbols.
3012#[derive(Debug, Clone, Serialize)]
3013pub struct IREmit {
3014    pub node_type: &'static str,
3015    pub source_line: u32,
3016    pub source_column: u32,
3017    pub channel_ref: String,
3018    pub value_ref: String,
3019    pub value_is_channel: bool,
3020    /// §Fase 114 (owed) — the σ-shield the target `channel` declares
3021    /// (`channel C { … shield: S }`), RESOLVED here at lowering (Phase 0
3022    /// pre-pass, like `IRPublish.sign`) so the runtime `run_emit` scans the
3023    /// emitted value through S on EVERY dispatch path without re-deriving the
3024    /// channel↔shield map. Empty ⇒ an unshielded channel (byte-identical to a
3025    /// pre-§114 emit: `skip_serializing_if` elides it → zero IR-SHA drift for
3026    /// programs whose channels declare no shield).
3027    #[serde(default, skip_serializing_if = "String::is_empty")]
3028    pub shield_ref: String,
3029    /// §Fase 114.w — the σ-shield's breach policy, resolved at lowering beside
3030    /// `shield_ref` (same Phase 0 pre-pass). `None` ⇒ no `on_breach:` declared
3031    /// (halt, the fail-closed default); elided → zero IR-SHA drift.
3032    #[serde(default, skip_serializing_if = "Option::is_none")]
3033    pub breach_policy: Option<IRBreachPolicy>,
3034}
3035
3036/// §Fase 92.a — compiled `credential` contract. The TTL is carried as
3037/// SECONDS (converted at lowering from the duration literal) so every
3038/// consumer — the OSS minter port, the enterprise PASETO minter, the
3039/// deploy gate — shares one arithmetic-ready representation.
3040#[derive(Debug, Clone, Serialize)]
3041pub struct IRCredential {
3042    pub node_type: &'static str,
3043    pub source_line: u32,
3044    pub source_column: u32,
3045    pub name: String,
3046    /// The bearer lifetime in seconds (from the `ttl:` duration literal;
3047    /// `0` = unparseable, rejected by `axon-T894` before the IR ships).
3048    pub ttl_secs: u64,
3049    /// The capability slugs the minted bearer carries (validated dotted
3050    /// slugs; non-empty per `axon-T893`).
3051    pub grants: Vec<String>,
3052}
3053
3054/// §Fase 92.b — compiled `mint <Credential> as <binding>` step. The runtime
3055/// resolves the contract, enforces the attenuation law
3056/// (`grants ⊆ capabilities(minter)`, fail-closed), mints via the
3057/// `CredentialMinter` port, and binds the raw bearer under `binding`.
3058#[derive(Debug, Clone, Serialize)]
3059pub struct IRMintStep {
3060    pub node_type: &'static str,
3061    pub source_line: u32,
3062    pub source_column: u32,
3063    pub credential_ref: String,
3064    pub binding: String,
3065}
3066
3067/// §Fase 94.b — compiled `rotate <SecretsStore> [where "…"] with <Tool>
3068/// as <binding>` step. The runtime enumerates the custody entries of the
3069/// store's class matching `where_expr` (whole class when empty), performs
3070/// ONE mediated exchange per key through the named tool (reveal → tool
3071/// renews → CAS commit at version+1), and binds the METADATA-ONLY
3072/// summary. Fail-closed without a custody port; each per-key failure
3073/// degrades with a witness, never destructively.
3074#[derive(Debug, Clone, Serialize)]
3075pub struct IRRotateStep {
3076    pub node_type: &'static str,
3077    pub source_line: u32,
3078    pub source_column: u32,
3079    pub store_ref: String,
3080    /// §67 metadata filter; empty = the whole class (elided from the wire).
3081    #[serde(default, skip_serializing_if = "String::is_empty")]
3082    pub where_expr: String,
3083    pub tool_ref: String,
3084    pub binding: String,
3085}
3086
3087/// Compiled publish step — capability extrusion (Publish-Ext, paper §4.3).
3088#[derive(Debug, Clone, Serialize)]
3089pub struct IRPublish {
3090    pub node_type: &'static str,
3091    pub source_line: u32,
3092    pub source_column: u32,
3093    pub channel_ref: String,
3094    pub shield_ref: String,
3095    /// §Fase 77.b — the referenced shield's `sign:` algorithm, RESOLVED at
3096    /// lowering (order-independent pre-pass over every declared shield).
3097    /// Non-empty ⇒ this publish is an EGRESS declaration: the channel's
3098    /// events are signed-deliverable to registered external subscribers.
3099    /// Elided from JSON when empty (zero IR-SHA drift for pre-§77 programs).
3100    #[serde(skip_serializing_if = "String::is_empty")]
3101    pub sign: String,
3102}
3103
3104/// Compiled discover step — dual of publish.
3105#[derive(Debug, Clone, Serialize)]
3106pub struct IRDiscover {
3107    pub node_type: &'static str,
3108    pub source_line: u32,
3109    pub source_column: u32,
3110    pub capability_ref: String,
3111    pub alias: String,
3112}