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/// v2.67.0 — `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    /// v2.27.0 — temporal execution-window guards.
32    pub windows: Vec<IRWindow>,
33    /// v2.69.0 — 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    /// v2.12.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    /// v2.5.0 — 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    /// v2.63.0 un-skipped it).
57    /// The in-memory field feeds the v2.5.0 type-checker + v2.5.0 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. v2.5.0 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 (v2.5.0 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    /// v2.63.0 — 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    /// (section 8.2.h.1) — which meant the runtime literally could not see a
73    /// declared dataspace (the v2.63.0 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    /// v1.1.0 — 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    /// v1.1.0 (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    /// v1.1.0 — Control cognitivo primitives (compiled).
90    pub reconciles: Vec<IRReconcile>,
91    pub leases: Vec<IRLease>,
92    pub ensembles: Vec<IREnsemble>,
93    /// v1.1.0 — Topology + Session (compiled).
94    pub sessions: Vec<IRSession>,
95    pub topologies: Vec<IRTopology>,
96    /// v1.1.0 — Immune system (compiled).
97    pub immunes: Vec<IRImmune>,
98    pub reflexes: Vec<IRReflex>,
99    pub heals: Vec<IRHeal>,
100    /// v1.3.1 — UI cognitiva declarativa (compiled).
101    pub components: Vec<IRComponent>,
102    pub views: Vec<IRView>,
103    /// v1.6.0 — Mobile typed channels (compiled).
104    pub channels: Vec<IRChannel>,
105    /// v2.3.0 — 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    /// v2.4.0 — 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    /// v2.4.0 checker + the v2.4.0 runtime. The checker resolves
115    /// `quant(observable: …)` against the AST symbol table, not this field.
116    #[serde(skip)]
117    pub observables: Vec<IRObservable>,
118    /// v2.23.0 — Advantage-Witness declarations. `skip_serializing_if = empty`
119    /// keeps a witness-less program's IR JSON byte-identical (zero IR-SHA drift,
120    /// the v2.4.0/v2.21.0 pattern); when present it rides the IR to the enterprise
121    /// deploy/runtime evaluator (v2.23.0+).
122    #[serde(default, skip_serializing_if = "Vec::is_empty")]
123    pub witnesses: Vec<IRWitness>,
124    /// v2.37.0 — 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 v2.33.0 discipline).
131    #[serde(default, skip_serializing_if = "Vec::is_empty")]
132    pub upstreams: Vec<IRUpstream>,
133    /// v2.38.0 — 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 v2.33.0 discipline).
136    #[serde(default, skip_serializing_if = "Vec::is_empty")]
137    pub cors_policies: Vec<IRCors>,
138    /// v2.40.0 — 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    /// v2.46.0 — 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    /// v2.42.0 — long-horizon autonomous research primitives (compiled). Each
149    /// carries its domain, cognition params, memory binding, compute budget and
150    /// mandates so the enterprise engine (v2.42.0+) 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    /// v2.53.0 — 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    /// v2.60.0 — 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    /// v2.66.0 — governed human notifications (the third egress dual).
168    pub notifications: Vec<IRNotify>,
169    /// v2.42.0 — 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    /// v2.43.0 — 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    /// v1.17.0 — 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    /// **v2.87.0 — 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    /// v2.87.0 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 the design decision resolves a bare
196    /// `perform Emit(x)` over.
197    ///
198    /// ⚠️ Reusing this field rather than adding one was a deliberate correction
199    /// mid-cycle: a parallel `effect_specs` would have made TWO compiled
200    /// catalogs of one concept in one artifact — the v2.83.0 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    /// v2.76.0 — 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 v2.33.0 discipline).
212    #[serde(default, skip_serializing_if = "Vec::is_empty")]
213    pub modules: Vec<IRModuleProvenance>,
214}
215
216/// v2.76.0 — 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/// v2.4.0 — 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/// v2.4.0 — 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/// v2.4.0 — 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/// v2.23.0 — 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// ── v1.17.0 / v2.87.0 — Algebraic effect declarations ────────────────────
350//
351// v1.17.0 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: []`. v2.87.0 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// ── v1.1.0 — 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    /// v2.67.0 — the pool size. Until v2.67.0 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    /// v2.67.0 — **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 v2.67.0, 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    /// v2.67.0 — 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-v2.67.0 program
454    /// serializes byte-identically (IR-SHA stability, the v2.48.0 `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// ── v1.1.0 — 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// ── v1.1.0 — 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// ── v1.1.0 — IRObserve ───────────────────────────────────────────────
511
512// ── v1.1.0 — 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 (v1.1.0) 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/// v1.1.0; 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// ── v1.1.0 — 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// ── v1.1.0 — IRSession / IRTopology ──────────────────────────────────
597
598/// One operation in a compiled session protocol
599/// (send / receive / loop / end / select / branch — v2.3.0 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    /// v2.3.0 — labelled branches (only for `op == "select" | "branch"`;
608    /// v2.36.0 reuses them for `op == "interrupt"`: `body` + `handler` arms).
609    #[serde(skip_serializing_if = "Vec::is_empty", default)]
610    pub branches: Vec<IRSessionBranch>,
611    /// v2.36.0 — `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 v2.33.0/v2.34.0 additive-only discipline).
614    #[serde(skip_serializing_if = "String::is_empty", default)]
615    pub binder: String,
616    /// v2.36.0 — `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/// v2.3.0 — 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// ── v1.1.0 — 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// ── v1.3.1 — 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    /// v2.76.0 — `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-v2.76.0 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    /// v2.76.0 — 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    /// v2.46.0 — the frame's declared cognitive timezone (IANA name).
804    /// Elided when absent → pre-v2.46.0 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/// v2.8.0 — 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 (v2.8.0 D1, single source of
833/// truth). Lossless round-trip is gated in v2.8.0.
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/// v2.8.0 — 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 (v2.8.0) 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    /// v2.10.0 — `"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    /// v2.69.0 — the `resource` this tool's channel runs on. Empty ⇒ legacy
869    /// form. Skip-if-empty ⇒ every pre-v2.69.0 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    /// v2.8.0 — the tool's typed INPUT SCHEMA (D1). Distinct from the v1.23.0
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 (v2.8.0) and the runtime binds
879    /// structured args against (v2.8.0). Empty for a schema-less tool (D5).
880    pub parameters: Vec<IRToolParam>,
881    /// v2.8.0 — 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    /// v2.77.0 — the authorization scopes this tool's operation
886    /// requires: flat capability atoms, the `credential.grants` (v2.46.0) /
887    /// `requires_capabilities` (v2.4.0) vocabulary. `axon-T956` enforces subset
888    /// coverage at compile; the PCC `ScopeCoverage` witness rides the linked
889    /// IR. Elided when empty — every pre-v2.77.0 tool serializes byte-identically
890    /// (IR-SHA stability), and stored FlowIr hydrates via `default` (v2.4.0
891    /// mirror discipline).
892    #[serde(default, skip_serializing_if = "Vec::is_empty")]
893    pub requires: Vec<String>,
894    /// v2.48.0 — 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-v2.48.0 tool serializes byte-identically
898    /// (IR-SHA stability).
899    #[serde(default, skip_serializing_if = "String::is_empty")]
900    pub secret: String,
901    /// v2.49.0 — 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 v2.48.0 and
904    /// pre-v2.48.0 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    /// v2.39.0 — Remote Hands. All three fields are `skip_serializing_if`
911    /// so a program using none of them serialises **byte-identically** to the
912    /// pre-v2.39.0 IR (the v2.33.0 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    /// v2.40.0 — 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    /// v2.52.0 — the closed-catalog web-acquisition config. `None` for
926    /// every non-scrape tool, and `skip_serializing_if` so the entire
927    /// pre-v2.52.0 corpus serialises byte-identically (the v2.33.0 IR-SHA /
928    /// additive-only gate). Present ⇒ this tool acquires open-web content
929    /// (born Untrusted, the design decision) and its `effect_row` carries `web`.
930    #[serde(skip_serializing_if = "Option::is_none")]
931    pub scrape: Option<IRScrapeSpec>,
932}
933
934/// v2.52.0 — 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 — κ 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    /// v2.22.0 — the step's model-capability requirement (context window in
1053    /// tokens). `skip_serializing_if = Option::is_none` keeps every pre-v2.22.0 step's
1054    /// IR JSON byte-identical (no IR-SHA drift, the design decision); a legacy IR deserialises
1055    /// to `None` → the v2.22.0 resolver picks the backend default exactly as today.
1056    #[serde(default, skip_serializing_if = "Option::is_none")]
1057    pub requires_context: Option<u32>,
1058    /// v2.46.0 — 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-v2.46.0 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    /// v2.83.0 — governance applications scoped to this step.
1065    /// Elided when empty so every pre-v2.83.0 program's IR JSON stays
1066    /// byte-identical (no IR-SHA drift — the v2.22.0/v2.46.0 discipline).
1067    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1068    pub guards: Vec<IRStepGuard>,
1069    /// v2.83.0 — 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-v2.83.0 program's IR JSON is byte-identical.
1074    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1075    pub pix_ops: Vec<IRFlowNode>,
1076    /// v2.83.0 — 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-v2.83.0 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    /// v2.87.0 — 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    /// `the design plan` section 3.1's `perform Emit(response.token)` performs the step's own
1087    /// result. Elided when empty, so every pre-v2.87.0 program's IR JSON stays
1088    /// byte-identical (the v2.22.0/v2.46.0/v2.83.0 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/// v2.83.0 — one step-scoped governance application. For a
1095/// `mandate` guard, dispatch must run the v2.83.0 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    /// v1.14.0 — 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    /// v1.14.0 — 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    /// v2.65.0 — 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    /// v2.87.0 — `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    /// v2.87.0 — `perform E.Op(args)`.
1218    Perform(IREffectPerform),
1219    /// v2.87.0 — `resume(v)`, the one-shot continuation invocation (D2).
1220    Resume(IREffectResume),
1221    /// v2.87.0 — `abort(v)`.
1222    Abort(IREffectAbort),
1223    /// v2.87.0 — `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    /// v2.83.0 — `<Agent>(arg, …)`. The 46th variant, and the one that
1233    /// makes v2.83.0's bounded control loop reachable from source.
1234    AgentCall(IRAgentCall),
1235    Listen(IRListenStep),
1236    DaemonStep(IRDaemonStepNode),
1237    /// v1.6.0 — π-calc output prefix (Chan-Output / Chan-Mobility).
1238    Emit(IREmit),
1239    /// v2.46.0 — ephemeral-credential minting (attenuated, TTL-bounded).
1240    Mint(IRMintStep),
1241    /// v2.48.0 — mediated secret renewal (`rotation_without_revelation`).
1242    Rotate(IRRotateStep),
1243    /// v1.6.0 — capability extrusion (Publish-Ext).
1244    Publish(IRPublish),
1245    /// v1.6.0 — 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    /// v2.43.0 — the `warden` adversarial security-analysis block.
1253    Warden(IRWarden),
1254    /// v2.4.0 — the `quant` cognitive block (Hilbert-space projection).
1255    Quant(IRQuant),
1256    /// v2.4.0 — the `yield` measurement point inside a `quant` block.
1257    Yield(IRYield),
1258    /// v2.4.0 — `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    /// v2.83.0 — the evidence the deliberation reasons OVER, resolved
1279    /// against the flow bindings at dispatch. Elided when empty so every
1280    /// pre-v2.83.0 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    /// v2.83.0 — the deliberation's question. The README writes it in
1284    /// every one of its sixteen `reason { … }` blocks and, before this cycle, it
1285    /// reached the model in none of them.
1286    #[serde(default, skip_serializing_if = "String::is_empty")]
1287    pub ask: String,
1288    /// v2.83.0 — 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    /// v2.88.0 — the `type` that `against: <Schema>` names, RESOLVED at
1302    /// lowering and stamped into the artifact.
1303    ///
1304    /// # Why resolved here rather than threaded to the dispatcher
1305    ///
1306    /// The alternative was an `Arc<Vec<IRType>>` on `DispatchCtx`, the shape
1307    /// `mandate_specs` / `compute_specs` / `agent_specs` use. v2.83.0 records
1308    /// what that costs: threading `agent_specs` meant touching EIGHT production
1309    /// sites, and missing one is a silent hole. `IRRun` already shows the other
1310    /// pattern — `resolved_flow`, `resolved_persona`, `resolved_anchors` — and
1311    /// it is the better one here: the artifact carries its own derivation, so
1312    /// dispatch cannot be reached with the resolution missing, and the schema a
1313    /// validation scored against is auditable after the fact.
1314    ///
1315    /// `None` when `against:` was absent. It is NOT `None` for an *unresolved*
1316    /// name — the type-checker refuses that program, so a `None` here never
1317    /// means "the author named a schema I could not find".
1318    ///
1319    /// Elided when absent, so every pre-v2.88.0 program's IR JSON is
1320    /// byte-identical (the v2.22.0/v2.46.0/v2.83.0/v2.87.0 discipline).
1321    #[serde(default, skip_serializing_if = "Option::is_none")]
1322    pub resolved_schema: Option<Box<IRType>>,
1323    /// v2.88.0 — `if confidence < θ -> refine(max_attempts: N)`, attached to
1324    /// THIS validation (see `ast::ValidateStep::guard` for why attachment, not
1325    /// siblinghood). Elided when absent.
1326    #[serde(default, skip_serializing_if = "Option::is_none")]
1327    pub guard: Option<IRConfidenceGuard>,
1328}
1329
1330/// v2.88.0 — the lowered confidence guard: a floor over the validation's CSR
1331/// plus a bounded recovery. Termination is by construction — attempts strictly
1332/// increase toward `max_attempts`, a `u32` from a literal.
1333#[derive(Debug, Clone, Serialize)]
1334pub struct IRConfidenceGuard {
1335    pub node_type: &'static str,
1336    pub source_line: u32,
1337    pub source_column: u32,
1338    pub threshold: f64,
1339    pub max_attempts: u32,
1340}
1341
1342#[derive(Debug, Clone, Serialize)]
1343pub struct IRRefineStep {
1344    pub node_type: &'static str,
1345    pub source_line: u32,
1346    pub source_column: u32,
1347    pub target: String,
1348    pub strategy: String,
1349}
1350
1351#[derive(Debug, Clone, Serialize)]
1352pub struct IRWeaveStep {
1353    pub node_type: &'static str,
1354    pub source_line: u32,
1355    pub source_column: u32,
1356    pub sources: Vec<String>,
1357    pub target: String,
1358    pub format_type: String,
1359    pub priority: Vec<String>,
1360    pub style: String,
1361    /// v2.83.0 — the parts the synthesis must contain
1362    /// (`include: [summary, risks, recommendations]`). Elided when empty so
1363    /// every pre-v2.83.0 program's IR JSON is byte-identical (no IR-SHA drift).
1364    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1365    pub include: Vec<String>,
1366}
1367
1368#[derive(Debug, Clone, Serialize)]
1369pub struct IRUseToolStep {
1370    pub node_type: &'static str,
1371    pub source_line: u32,
1372    pub source_column: u32,
1373    pub tool_name: String,
1374    pub argument: String,
1375    /// v2.8.0 — the bound keyword args of `use Tool(k = v, …)` (W1: the
1376    /// structured args survive to the IR, no longer collapsed to one opaque
1377    /// string). Empty for the legacy single-`on <arg>` form (`argument`
1378    /// carries that, D5).
1379    pub named_args: Vec<IRNamedArg>,
1380}
1381
1382#[derive(Debug, Clone, Serialize)]
1383pub struct IRRememberStep {
1384    pub node_type: &'static str,
1385    pub source_line: u32,
1386    pub source_column: u32,
1387    pub expression: String,
1388    pub memory_target: String,
1389}
1390
1391#[derive(Debug, Clone, Serialize)]
1392pub struct IRRecallStep {
1393    pub node_type: &'static str,
1394    pub source_line: u32,
1395    pub source_column: u32,
1396    pub query: String,
1397    pub memory_source: String,
1398}
1399
1400/// v2.26.0 — the lowered form of a pure expression (`Expr`). Carried in the
1401/// IR for conditions the legacy `(condition, op, value)` triple cannot express.
1402/// Operators are canonical lowercase strings so the JSON is stable + readable;
1403/// the runtime evaluator (v2.26.0) matches on them. Externally-tagged by `kind`.
1404#[derive(Debug, Clone, Serialize)]
1405#[serde(tag = "kind", rename_all = "snake_case")]
1406pub enum IRExpr {
1407    /// A typed literal.
1408    Lit { lit: IRExprLit },
1409    /// A reference to a binding / dotted path.
1410    Ref { path: String },
1411    /// Unary op — `op ∈ {neg, not}`.
1412    Unary { op: String, operand: Box<IRExpr> },
1413    /// Binary op — `op ∈ {add,sub,mul,div,mod,eq,ne,lt,le,gt,ge,and,or}`.
1414    Binary {
1415        op: String,
1416        lhs: Box<IRExpr>,
1417        rhs: Box<IRExpr>,
1418    },
1419    /// v2.26.0 — a closed-catalog builtin call. `args[0]` is the receiver.
1420    /// `builtin ∈ {length,count,is_empty,is_null,contains,starts_with,ends_with}`.
1421    Call {
1422        builtin: String,
1423        args: Vec<IRExpr>,
1424    },
1425    /// v2.26.0 — field access on a non-reference base (the JSONB seam).
1426    Field {
1427        base: Box<IRExpr>,
1428        field: String,
1429    },
1430    /// v2.26.0 — index access `base[index]`.
1431    Index {
1432        base: Box<IRExpr>,
1433        index: Box<IRExpr>,
1434    },
1435    /// v2.83.0 — `let <name> = <value>` scoped over `<body>`. The lowered
1436    /// form of a `logic { let … return … }` chain: one `Let` per binding,
1437    /// nested, so each bound term is evaluated exactly once and shadowing falls
1438    /// out of the nesting.
1439    Let {
1440        name: String,
1441        value: Box<IRExpr>,
1442        body: Box<IRExpr>,
1443    },
1444}
1445
1446/// v2.26.0 — a literal inside an [`IRExpr`].
1447#[derive(Debug, Clone, Serialize)]
1448#[serde(tag = "ty", rename_all = "snake_case")]
1449pub enum IRExprLit {
1450    Int { value: i64 },
1451    Float { value: f64 },
1452    Bool { value: bool },
1453    Str { value: String },
1454}
1455
1456#[derive(Debug, Clone, Serialize)]
1457pub struct IRConditional {
1458    pub node_type: &'static str,
1459    pub source_line: u32,
1460    pub source_column: u32,
1461    pub condition: String,
1462    pub comparison_op: String,
1463    pub comparison_value: String,
1464    pub then_body: Vec<IRFlowNode>,
1465    pub else_body: Vec<IRFlowNode>,
1466    pub conditions: Vec<(String, String, String)>,
1467    pub conjunctor: String,
1468    /// v2.26.0 — the lowered expression form, present only for conditions
1469    /// the legacy triple cannot express. `skip_serializing_if` keeps the IR
1470    /// JSON (and its SHA) byte-identical for every pre-v2.26.0 program.
1471    #[serde(skip_serializing_if = "Option::is_none")]
1472    pub cond: Option<IRExpr>,
1473}
1474
1475#[derive(Debug, Clone, Serialize)]
1476pub struct IRForIn {
1477    pub node_type: &'static str,
1478    pub source_line: u32,
1479    pub source_column: u32,
1480    pub variable: String,
1481    pub iterable: String,
1482    pub body: Vec<IRFlowNode>,
1483}
1484
1485#[derive(Debug, Clone, Serialize)]
1486pub struct IRLetBinding {
1487    pub node_type: &'static str,
1488    pub source_line: u32,
1489    pub source_column: u32,
1490    pub target: String,
1491    pub value: String,
1492    /// v1.12.0 — preserves parser tokenization intent.
1493    /// One of "literal" | "reference" | "expression".
1494    pub value_kind: String,
1495    /// v2.26.0 — the lowered expression form of the value, present only for
1496    /// `value_kind == "expression"`. The runtime evaluates it instead of
1497    /// treating the value string as an opaque literal. `skip_serializing_if`
1498    /// keeps the IR byte-identical for every literal / reference let.
1499    #[serde(skip_serializing_if = "Option::is_none")]
1500    pub value_ast: Option<IRExpr>,
1501}
1502
1503#[derive(Debug, Clone, Serialize)]
1504pub struct IRReturnStep {
1505    pub node_type: &'static str,
1506    pub source_line: u32,
1507    pub source_column: u32,
1508    pub value_expr: String,
1509}
1510
1511/// v1.14.0 — `break` keyword IR node. Payload-free (the runner
1512/// raises a sentinel; no value is carried). Mirrors Python's
1513/// ``IRBreak`` (axon/compiler/ir_nodes.py).
1514#[derive(Debug, Clone, Serialize)]
1515pub struct IRBreakStep {
1516    pub node_type: &'static str,
1517    pub source_line: u32,
1518    pub source_column: u32,
1519}
1520
1521/// v1.14.0 — `continue` keyword IR node. Same shape as
1522/// ``IRBreakStep``; the runner uses a different sentinel type to
1523/// distinguish loop-exit (break) from iteration-skip (continue).
1524#[derive(Debug, Clone, Serialize)]
1525pub struct IRContinueStep {
1526    pub node_type: &'static str,
1527    pub source_line: u32,
1528    pub source_column: u32,
1529}
1530
1531#[derive(Debug, Clone, Serialize)]
1532pub struct IRParallelBlock {
1533    pub node_type: &'static str,
1534    pub source_line: u32,
1535    pub source_column: u32,
1536    /// v2.15.0 — the concurrent branches lowered from the AST `par { … }`.
1537    /// Each branch is a flow-IR body run concurrently by the dispatcher's
1538    /// `run_branches_concurrently`. `skip_serializing_if = "Vec::is_empty"` so a
1539    /// payload-free / empty `par` serializes byte-identically to the pre-v2.15.0
1540    /// shape (D5 back-compat); a `par` with real branches carries them.
1541    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1542    pub branches: Vec<Vec<IRFlowNode>>,
1543}
1544
1545#[derive(Debug, Clone, Serialize)]
1546pub struct IRHibernateStep {
1547    pub node_type: &'static str,
1548    pub source_line: u32,
1549    pub source_column: u32,
1550    pub event_name: String,
1551    pub timeout: String,
1552}
1553
1554#[derive(Debug, Clone, Serialize)]
1555pub struct IRDeliberateBlock {
1556    pub node_type: &'static str,
1557    pub source_line: u32,
1558    pub source_column: u32,
1559}
1560
1561#[derive(Debug, Clone, Serialize)]
1562pub struct IRConsensusBlock {
1563    pub node_type: &'static str,
1564    pub source_line: u32,
1565    pub source_column: u32,
1566}
1567
1568/// v2.41.0 — the compiled Directed Creative Synthesis block. This IS the
1569/// "structured IR metadata that the runtime executes as an orchestrated
1570/// pipeline" the README always claimed — pre-v2.41.0 it carried only a source
1571/// location. New fields are `skip_serializing_if`-elided so a program with no
1572/// `forge` stays IR-SHA stable.
1573#[derive(Debug, Clone, Serialize, Default)]
1574pub struct IRForgeBlock {
1575    pub node_type: &'static str,
1576    pub source_line: u32,
1577    pub source_column: u32,
1578    #[serde(default, skip_serializing_if = "String::is_empty")]
1579    pub name: String,
1580    #[serde(default, skip_serializing_if = "String::is_empty")]
1581    pub seed: String,
1582    #[serde(default, skip_serializing_if = "String::is_empty")]
1583    pub output_type: String,
1584    #[serde(default, skip_serializing_if = "String::is_empty")]
1585    pub mode: String,
1586    #[serde(default, skip_serializing_if = "is_default_novelty")]
1587    pub novelty: f64,
1588    #[serde(default, skip_serializing_if = "is_one_i64")]
1589    pub depth: i64,
1590    #[serde(default, skip_serializing_if = "is_one_i64")]
1591    pub branches: i64,
1592    #[serde(default, skip_serializing_if = "String::is_empty")]
1593    pub constraints_ref: String,
1594}
1595
1596fn is_default_novelty(v: &f64) -> bool {
1597    (*v - 0.5).abs() < f64::EPSILON
1598}
1599fn is_one_i64(v: &i64) -> bool {
1600    *v == 1
1601}
1602
1603/// v2.65.0 — the proof-carrying derivative. `original` is the
1604/// differentiated `let`'s expression; `derivatives[i]` = ∂original/∂wrt[i],
1605/// SIMPLIFIED — computed at compile time by the symbolic
1606/// differentiator and re-derived at deploy by PCC `GradientSoundness`.
1607/// `original: None` / empty `derivatives` only in a stale artifact — the
1608/// runtime fails CLOSED on it and the PCC refutes it.
1609#[derive(Debug, Clone, Serialize)]
1610pub struct IRGradStep {
1611    pub node_type: &'static str,
1612    pub source_line: u32,
1613    pub source_column: u32,
1614    /// The prior rich `let` differentiated.
1615    pub target: String,
1616    pub wrt: Vec<String>,
1617    /// Result binding (empty ⇒ `d_<target>`).
1618    pub output: String,
1619    pub original: Option<IRExpr>,
1620    pub derivatives: Vec<IRExpr>,
1621}
1622
1623#[derive(Debug, Clone, Serialize)]
1624pub struct IRFocusStep {
1625    pub node_type: &'static str,
1626    pub source_line: u32,
1627    pub source_column: u32,
1628    pub expression: String,
1629    /// v2.63.0 — the data-plane `where:` (the design decision; empty ⇒ no filter).
1630    pub where_expr: String,
1631    /// v2.63.0 — π: projected columns (empty ⇒ all).
1632    pub select: Vec<String>,
1633    /// v2.63.0 — result binding (`as:`; empty ⇒ the dataspace name).
1634    pub output: String,
1635}
1636
1637#[derive(Debug, Clone, Serialize)]
1638pub struct IRAssociateStep {
1639    pub node_type: &'static str,
1640    pub source_line: u32,
1641    pub source_column: u32,
1642    pub left: String,
1643    pub right: String,
1644    pub using_field: String,
1645    /// v2.63.0 — result binding (`as:`; empty ⇒ `<L>_<R>`).
1646    pub output: String,
1647}
1648
1649#[derive(Debug, Clone, Serialize)]
1650pub struct IRAggregateStep {
1651    pub node_type: &'static str,
1652    pub source_line: u32,
1653    pub source_column: u32,
1654    pub target: String,
1655    pub group_by: Vec<String>,
1656    pub alias: String,
1657    /// v2.63.0 — the closed aggregate catalog entries, raw
1658    /// (`count`, `sum(score)`, …) — canonical spelling, T930-validated.
1659    pub compute: Vec<String>,
1660    /// v2.63.0 — the data-plane `where:`.
1661    pub where_expr: String,
1662}
1663
1664#[derive(Debug, Clone, Serialize)]
1665pub struct IRExploreStep {
1666    pub node_type: &'static str,
1667    pub source_line: u32,
1668    pub source_column: u32,
1669    pub target: String,
1670    pub limit: Option<i64>,
1671    /// v2.63.0 — result binding (`as:`; empty ⇒ the target).
1672    pub output: String,
1673}
1674
1675#[derive(Debug, Clone, Serialize)]
1676pub struct IRIngestStep {
1677    pub node_type: &'static str,
1678    pub source_line: u32,
1679    pub source_column: u32,
1680    pub source: String,
1681    pub target: String,
1682    /// v2.63.0 — the declared wire format (`csv` | `json`, validated
1683    /// by axon-T929). Empty only in a pre-108.c artifact — the runtime
1684    /// handler fails CLOSED on it.
1685    pub format: String,
1686    /// v2.63.0 — bounds enforced on the raw byte stream BEFORE
1687    /// parsing (v2.54.0). `None` ⇒ the engine's conservative defaults.
1688    pub max_bytes: Option<u64>,
1689    pub max_rows: Option<u64>,
1690}
1691
1692/// v2.69.0 — a shield's compiled BREACH POLICY, resolved onto the nodes
1693/// that enforce it (`IRShieldApplyStep` / `IREmit`) at LOWERING — the same
1694/// discipline as `IREmit.shield_ref`: the policy rides the artifact, so every
1695/// dispatch path honors it by construction (no per-ctx shield map a forgotten
1696/// site could miss). Before v2.69.0 the whole `on_breach:` catalog
1697/// (`halt|sanitize_and_retry|escalate|quarantine|deflect`) was documented,
1698/// parsed, type-checked — and the runtime always `halt`ed.
1699#[derive(Debug, Clone, Serialize)]
1700pub struct IRBreachPolicy {
1701    /// The declared policy (validated against `VALID_ON_BREACH_POLICIES`).
1702    pub on_breach: String,
1703    /// The quarantine SINK name (`on_breach: quarantine` requires it, axon-T952).
1704    #[serde(default, skip_serializing_if = "String::is_empty")]
1705    pub quarantine: String,
1706    /// The canned safe reply (`on_breach: deflect` requires it, axon-T952).
1707    #[serde(default, skip_serializing_if = "String::is_empty")]
1708    pub deflect_message: String,
1709    /// Fields masked by `sanitize_and_retry` (requires ≥ 1, axon-T952).
1710    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1711    pub redact: Vec<String>,
1712    /// Re-scan budget for `sanitize_and_retry` (parser default: 3).
1713    pub max_retries: i64,
1714}
1715
1716#[derive(Debug, Clone, Serialize)]
1717pub struct IRShieldApplyStep {
1718    pub node_type: &'static str,
1719    pub source_line: u32,
1720    pub source_column: u32,
1721    pub shield_name: String,
1722    pub target: String,
1723    pub output_type: String,
1724    /// v2.69.0 — the named shield's breach policy, resolved at lowering.
1725    /// `None` ⇒ the shield declares no `on_breach:` (halt, the fail-closed
1726    /// default) — and every pre-v2.69.0 program serializes byte-identically.
1727    #[serde(default, skip_serializing_if = "Option::is_none")]
1728    pub breach_policy: Option<IRBreachPolicy>,
1729    /// v2.89.0 — the named shield's declared `scan:` list, resolved at
1730    /// lowering beside `breach_policy` (same Phase 0 pre-pass).
1731    ///
1732    /// It is the SUBJECT of the v2.89.0 refusal. A shield that declares
1733    /// `scan: [prompt_injection]` is not asking to filter — it is ASSERTING
1734    /// that nothing past this point carries an injection. With no scanner
1735    /// registered under the name, OSS used to pass the value through
1736    /// untouched and bind it under a name that claims the property. That is
1737    /// the `warden` shape (v2.67.0 F12): a clean-looking result for something
1738    /// never examined. The runtime now refuses instead.
1739    ///
1740    /// Empty ⇒ a shield with no declared scan, where v2.83.0's argument still
1741    /// holds (a filter's absence honestly leaves data untouched) and the
1742    /// identity passthrough is kept. `skip_serializing_if` elides it → zero
1743    /// IR-SHA drift for every program whose shields declare no `scan:`.
1744    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1745    pub scan: Vec<String>,
1746}
1747
1748#[derive(Debug, Clone, Serialize)]
1749pub struct IRStreamBlock {
1750    pub node_type: &'static str,
1751    pub source_line: u32,
1752    pub source_column: u32,
1753    /// v2.67.0 — the block's lowered body. ADDITIVE: `skip_serializing_if`
1754    /// elides it when empty, so every pre-111 program's IR JSON stays
1755    /// byte-identical and a legacy IR deserialises to an empty body (which then
1756    /// executes as a no-op, exactly as before — no silent behaviour change for
1757    /// an artifact compiled by an older frontend).
1758    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1759    pub body: Vec<IRFlowNode>,
1760    /// v2.83.0 — the `<T>` in `stream<T>`: the CHUNK type. Elided when the
1761    /// block was written without one.
1762    #[serde(default, skip_serializing_if = "String::is_empty")]
1763    pub chunk_type: String,
1764    /// v2.83.0 — `on_chunk: { … }`, lowered. Run once per chunk with the
1765    /// chunk bound as `chunk`.
1766    #[serde(default, skip_serializing_if = "Option::is_none")]
1767    pub on_chunk: Option<Box<IRStep>>,
1768    /// v2.83.0 — `on_complete: { … }`, lowered. Run once, after the source
1769    /// closes, with the accumulated stream bound as `complete`.
1770    #[serde(default, skip_serializing_if = "Option::is_none")]
1771    pub on_complete: Option<Box<IRStep>>,
1772    /// v2.83.0 — `on_error: { … }`, lowered. Run when the SOURCE fails,
1773    /// with the failure bound as `error`. Never fires for a failure of the
1774    /// author's own handlers, nor for cancellation.
1775    #[serde(default, skip_serializing_if = "Option::is_none")]
1776    pub on_error: Option<Box<IRStep>>,
1777}
1778
1779// ── v2.87.0 — algebraic effects, lowered ──────────────────────────────────
1780//
1781// ⚠️ Names deliberately prefixed `IREffect*`. `axon-rs/src/effects/ir.rs`
1782// already owns `IRPerform` / `IRHandlerFrame` / `IRHandlerClause` / `IRResume`
1783// / `IRAbort` / `IRForward`, and `axon-rs` re-exports THIS module — an
1784// unprefixed name would collide there and, worse, would read as the same type.
1785// It is not: that module's `Instruction` alphabet has an INERT catch-all
1786// (`Passthrough`), so a handler body lowered onto it would silently execute
1787// nothing. the design decision puts the effect machine in the DISPATCHER, whose alphabet is
1788// `IRFlowNode`; these nodes are that alphabet's five new members.
1789
1790/// `handle E { clauses } in { body }`, lowered — one handler frame.
1791#[derive(Debug, Clone, Serialize)]
1792pub struct IREffectHandle {
1793    pub node_type: &'static str,
1794    pub source_line: u32,
1795    pub source_column: u32,
1796    pub effect_names: Vec<String>,
1797    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1798    pub clauses: Vec<IREffectClause>,
1799    /// The `in { … }` block — ORDINARY flow nodes. This field is the design decision.
1800    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1801    pub body: Vec<IRFlowNode>,
1802    /// Per-flow frame identity, allocated in source order. Distinguishes two
1803    /// frames over the same effect so `forward` can name which one it left.
1804    pub frame_id: u32,
1805}
1806
1807/// One clause of an [`IREffectHandle`]: `Op(params) -> { body }`.
1808#[derive(Debug, Clone, Serialize)]
1809pub struct IREffectClause {
1810    pub operation_name: String,
1811    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1812    pub parameter_names: Vec<String>,
1813    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1814    pub body: Vec<IRFlowNode>,
1815    pub source_line: u32,
1816    pub source_column: u32,
1817}
1818
1819/// `perform E.Op(args)`, lowered.
1820///
1821/// `effect_name` is ALWAYS populated here even when the source wrote the bare
1822/// form — the IR generator resolves it against the declared catalog.
1823/// A node that reached this point with an empty `effect_name` is a compiler
1824/// bug, and the dispatcher fails closed on it rather than searching by
1825/// operation name alone (which would let the handler stack pick a frame the
1826/// author never named).
1827#[derive(Debug, Clone, Serialize)]
1828pub struct IREffectPerform {
1829    pub node_type: &'static str,
1830    pub source_line: u32,
1831    pub source_column: u32,
1832    pub effect_name: String,
1833    pub operation_name: String,
1834    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1835    pub arguments: Vec<String>,
1836    /// True when the SOURCE wrote `perform Op(x)` and the catalog supplied the
1837    /// effect. Kept so a diagnostic can quote what the author actually typed.
1838    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1839    pub resolved_from_bare: bool,
1840}
1841
1842/// `resume(v)`, lowered — invoke the captured one-shot continuation (D2).
1843#[derive(Debug, Clone, Serialize)]
1844pub struct IREffectResume {
1845    pub node_type: &'static str,
1846    pub source_line: u32,
1847    pub source_column: u32,
1848    #[serde(default, skip_serializing_if = "String::is_empty")]
1849    pub value_expr: String,
1850}
1851
1852/// `abort(v)`, lowered — leave the enclosing `handle` without resuming.
1853#[derive(Debug, Clone, Serialize)]
1854pub struct IREffectAbort {
1855    pub node_type: &'static str,
1856    pub source_line: u32,
1857    pub source_column: u32,
1858    #[serde(default, skip_serializing_if = "String::is_empty")]
1859    pub value_expr: String,
1860}
1861
1862/// `forward E.Op(args)`, lowered (D12) — propagate to the next OUTER frame.
1863#[derive(Debug, Clone, Serialize)]
1864pub struct IREffectForward {
1865    pub node_type: &'static str,
1866    pub source_line: u32,
1867    pub source_column: u32,
1868    pub effect_name: String,
1869    pub operation_name: String,
1870    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1871    pub arguments: Vec<String>,
1872}
1873
1874#[derive(Debug, Clone, Serialize)]
1875pub struct IRNavigateStep {
1876    /// v2.83.0 — per-navigation depth override → `NavConfig.d_max`.
1877    #[serde(default, skip_serializing_if = "Option::is_none")]
1878    pub depth: Option<i64>,
1879    pub node_type: &'static str,
1880    pub source_line: u32,
1881    pub source_column: u32,
1882    pub pix_ref: String,
1883    pub corpus_ref: String,
1884    pub query: String,
1885    pub trail_enabled: bool,
1886    pub output_name: String,
1887    /// v2.13.0 — MDN corpus-graph navigation: the seed document (`from:`).
1888    #[serde(default, skip_serializing_if = "String::is_empty")]
1889    pub seed: String,
1890    /// v2.13.0 — MDN navigation budget (`budget:` = max documents).
1891    #[serde(default, skip_serializing_if = "Option::is_none")]
1892    pub budget: Option<i64>,
1893    /// v2.17.0 (Q2) — column-scope filter for a `corpus from axonstore`. A raw
1894    /// filter expr threaded to `read_all_store_rows` → `stream_retrieve` for
1895    /// BOTH the documents and edges stores, so the sourced MDN graph is scoped
1896    /// to a sub-tenant column (`where: "tenant_id == '${tenant_id}'"`). The
1897    /// v1.32.0 filter compiler resolves `${name}` → `$N` bind params (injection-
1898    /// safe). Empty = no column filter (axon-tenant RLS scope only).
1899    #[serde(default, skip_serializing_if = "String::is_empty")]
1900    pub where_expr: String,
1901}
1902
1903#[derive(Debug, Clone, Serialize)]
1904pub struct IRDrillStep {
1905    pub node_type: &'static str,
1906    pub source_line: u32,
1907    pub source_column: u32,
1908    pub pix_ref: String,
1909    pub subtree_path: String,
1910    pub query: String,
1911    pub output_name: String,
1912}
1913
1914#[derive(Debug, Clone, Serialize)]
1915pub struct IRTrailStep {
1916    pub node_type: &'static str,
1917    pub source_line: u32,
1918    pub source_column: u32,
1919    pub navigate_ref: String,
1920}
1921
1922#[derive(Debug, Clone, Serialize)]
1923pub struct IRCorroborateStep {
1924    pub node_type: &'static str,
1925    pub source_line: u32,
1926    pub source_column: u32,
1927    pub navigate_ref: String,
1928    pub output_name: String,
1929}
1930
1931#[derive(Debug, Clone, Serialize)]
1932pub struct IROtsApplyStep {
1933    pub node_type: &'static str,
1934    pub source_line: u32,
1935    pub source_column: u32,
1936    pub ots_name: String,
1937    pub target: String,
1938    pub output_type: String,
1939}
1940
1941#[derive(Debug, Clone, Serialize)]
1942pub struct IRMandateApplyStep {
1943    pub node_type: &'static str,
1944    pub source_line: u32,
1945    pub source_column: u32,
1946    pub mandate_name: String,
1947    pub target: String,
1948    pub output_type: String,
1949}
1950
1951#[derive(Debug, Clone, Serialize)]
1952pub struct IRAgentCall {
1953    pub node_type: &'static str,
1954    pub source_line: u32,
1955    pub source_column: u32,
1956    /// The declared `agent` this call invokes. Resolved against
1957    /// `DispatchCtx::agent_specs` at dispatch; an unresolved name fails CLOSED
1958    /// (the v2.67.0 compute doctrine — an empty catalog runs nothing).
1959    pub agent_name: String,
1960    /// Positional v2.83.0 subjects. Resolved against the flow bindings at
1961    /// dispatch, so `TrendAnalyzer(Gather.output)` hands the agent the prior
1962    /// step's VALUE.
1963    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1964    pub arguments: Vec<String>,
1965}
1966
1967#[derive(Debug, Clone, Serialize)]
1968pub struct IRComputeApplyStep {
1969    pub node_type: &'static str,
1970    pub source_line: u32,
1971    pub source_column: u32,
1972    pub compute_name: String,
1973    pub arguments: Vec<String>,
1974    pub output_name: String,
1975}
1976
1977#[derive(Debug, Clone, Serialize)]
1978pub struct IRListenStep {
1979    pub node_type: &'static str,
1980    pub source_line: u32,
1981    pub source_column: u32,
1982    pub channel: String,
1983    /// v1.6.0 D4 — true ⇒ `channel` is a declared
1984    /// `IRChannel` ref; false ⇒ legacy string topic.
1985    pub channel_is_ref: bool,
1986    pub event_alias: String,
1987    /// v2.4.0 — the handler body's lowered flow-steps, executed per event /
1988    /// scheduled tick by the v2.4.0 runtime. `skip_serializing_if` keeps a
1989    /// bodyless `listen`'s JSON byte-identical to the pre-v2.4.0 shape (D8).
1990    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1991    pub body: Vec<IRFlowNode>,
1992}
1993
1994#[derive(Debug, Clone, Serialize)]
1995pub struct IRDaemonStepNode {
1996    pub node_type: &'static str,
1997    pub source_line: u32,
1998    pub source_column: u32,
1999    pub daemon_ref: String,
2000}
2001
2002#[derive(Debug, Clone, Serialize)]
2003pub struct IRPersistStep {
2004    pub node_type: &'static str,
2005    pub source_line: u32,
2006    pub source_column: u32,
2007    pub store_name: String,
2008    /// v1.30.0 — declared `{ col: value }` field block (value
2009    /// expressions kept raw; interpolated at runtime). Empty ⇒ the
2010    /// runtime writes the flow's user bindings (v1.30.0 fallback).
2011    pub fields: Vec<(String, String)>,
2012}
2013
2014#[derive(Debug, Clone, Serialize)]
2015pub struct IRRetrieveStep {
2016    pub node_type: &'static str,
2017    pub source_line: u32,
2018    pub source_column: u32,
2019    pub store_name: String,
2020    pub where_expr: String,
2021    pub alias: String,
2022    /// v2.21.0 — `order_by:` clause (raw `"col [asc|desc], …"`).
2023    /// `skip_serializing_if` empty so a store that doesn't order never
2024    /// perturbs the serialized IR bytes (the v2.4.0 brief-#33 /
2025    /// [[feedback-boot-hydrate-self-heal]] no-drift discipline).
2026    #[serde(skip_serializing_if = "String::is_empty", default)]
2027    pub order_by: String,
2028    /// v2.21.0 — `limit:` clause (raw `"100"` or `"${max}"`).
2029    #[serde(skip_serializing_if = "String::is_empty", default)]
2030    pub limit_expr: String,
2031    /// v2.33.0 — `aggregate:` clause (raw, closed catalog: `count` /
2032    /// `sum(col)` / `avg(col)` / `min(col)` / `max(col)`).
2033    /// `skip_serializing_if` empty so a non-aggregating retrieve never
2034    /// perturbs the serialized IR bytes (the same v2.21.0 no-drift
2035    /// discipline — zero IR-SHA drift for existing programs).
2036    #[serde(skip_serializing_if = "String::is_empty", default)]
2037    pub aggregate: String,
2038    /// v2.33.0 — `group_by:` clause (raw `"col, col2"`).
2039    #[serde(skip_serializing_if = "String::is_empty", default)]
2040    pub group_by: String,
2041    /// v2.40.0 — `cache:` reference (a declared `cache` name). Empty ⇒
2042    /// uncached. Elided when empty (IR-SHA stable for cache-less retrieves).
2043    #[serde(skip_serializing_if = "String::is_empty", default)]
2044    pub cache: String,
2045}
2046
2047#[derive(Debug, Clone, Serialize)]
2048pub struct IRMutateStep {
2049    pub node_type: &'static str,
2050    pub source_line: u32,
2051    pub source_column: u32,
2052    pub store_name: String,
2053    pub where_expr: String,
2054    /// v1.30.0 — declared `{ col: value }` SET assignments (value
2055    /// expressions kept raw; interpolated at runtime). Empty ⇒ the
2056    /// runtime writes the flow's user bindings (v1.31.0 fallback).
2057    pub fields: Vec<(String, String)>,
2058}
2059
2060#[derive(Debug, Clone, Serialize)]
2061pub struct IRPurgeStep {
2062    pub node_type: &'static str,
2063    pub source_line: u32,
2064    pub source_column: u32,
2065    pub store_name: String,
2066    pub where_expr: String,
2067}
2068
2069#[derive(Debug, Clone, Serialize)]
2070pub struct IRTransactBlock {
2071    pub node_type: &'static str,
2072    pub source_line: u32,
2073    pub source_column: u32,
2074}
2075
2076/// v2.43.0 — IR for the `warden` adversarial-analysis block. Carries the
2077/// target, the mandatory `scope_ref`, and the recursively-lowered body so the
2078/// enterprise engine (v2.43.0) can drive the analysis and v2.43.0 can enforce the
2079/// authorization discipline.
2080#[derive(Debug, Clone, Serialize)]
2081pub struct IRWarden {
2082    pub node_type: &'static str,
2083    pub source_line: u32,
2084    pub source_column: u32,
2085    pub target: String,
2086    pub scope_ref: String,
2087    /// Nested flow-body IR (recursively lowered).
2088    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2089    pub body: Vec<IRFlowNode>,
2090}
2091
2092/// v2.43.0 — IR for a `scope` authorization-policy declaration.
2093#[derive(Debug, Clone, Serialize)]
2094pub struct IRScope {
2095    pub node_type: &'static str,
2096    pub source_line: u32,
2097    pub source_column: u32,
2098    pub name: String,
2099    pub targets: Vec<String>,
2100    pub depth: String,
2101    pub approver: String,
2102}
2103
2104/// v2.4.0 — IR for the `quant` cognitive block (Hilbert-space projection).
2105/// Mirrors `ast::QuantBlock`. Optional attributes serialize only when present
2106/// (`skip_serializing_if`) so a bare `quant {}` lowers to a minimal node and
2107/// the JSON stays diff-stable. The body lowers recursively, like `par` branches.
2108#[derive(Debug, Clone, Serialize)]
2109pub struct IRQuant {
2110    pub node_type: &'static str,
2111    pub source_line: u32,
2112    pub source_column: u32,
2113    /// Encoding scheme surface spelling (`amplitude` | `angle`); `None` = default.
2114    #[serde(default, skip_serializing_if = "Option::is_none")]
2115    pub encoding: Option<String>,
2116    /// Referenced `Observable` (Pauli-sum) name; `None` if unspecified.
2117    #[serde(default, skip_serializing_if = "Option::is_none")]
2118    pub observable: Option<String>,
2119    /// Register width n; `None` = inferred.
2120    #[serde(default, skip_serializing_if = "Option::is_none")]
2121    pub qubits: Option<i64>,
2122    /// Variational circuit depth L; `None` = backend default.
2123    #[serde(default, skip_serializing_if = "Option::is_none")]
2124    pub depth: Option<i64>,
2125    /// Projected-kernel bandwidth γ (D7); `None` = backend default.
2126    #[serde(default, skip_serializing_if = "Option::is_none")]
2127    pub bandwidth: Option<f64>,
2128    /// v2.23.0 — data re-uploading layers L (`None`/`1` = no re-uploading).
2129    /// `skip_serializing_if` keeps a non-re-uploading block's IR byte-identical.
2130    #[serde(default, skip_serializing_if = "Option::is_none")]
2131    pub reupload: Option<i64>,
2132    /// Algebraic-effect backend tag (`quant_sim` | `qpu_native`).
2133    pub effect: String,
2134    /// Nested flow-body IR (recursively lowered).
2135    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2136    pub body: Vec<IRFlowNode>,
2137}
2138
2139// ── Tier 2 IR nodes ─────────────────────────────────────────────────────────
2140
2141#[derive(Debug, Clone, Serialize)]
2142pub struct IRAgent {
2143    pub node_type: &'static str,
2144    pub source_line: u32,
2145    pub source_column: u32,
2146    pub name: String,
2147    pub goal: String,
2148    pub tools: Vec<String>,
2149    pub memory_ref: String,
2150    pub strategy: String,
2151    pub on_stuck: String,
2152    pub shield_ref: String,
2153    pub max_iterations: Option<i64>,
2154    pub max_tokens: Option<i64>,
2155    pub max_time: String,
2156    pub max_cost: Option<f64>,
2157    /// `return: T` — the declared result type. The loop's final answer is
2158    /// validated against it when `T` is a declared struct type (4.1.0). Skipped
2159    /// when empty so every pre-4.1.0 IR JSON stays byte-identical.
2160    #[serde(default, skip_serializing_if = "String::is_empty")]
2161    pub return_type: String,
2162    /// The declared fields of `return_type` when it names a declared struct
2163    /// `type` — resolved at lowering so the loop validates the final answer
2164    /// against them with no type table in hand. Empty for builtin / undeclared
2165    /// / generic return types (the answer is then free text, as before).
2166    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2167    pub return_schema: Vec<IRTypeField>,
2168    /// The `custom` policy's step sequence, lowered exactly as a flow body is.
2169    /// Empty for every other strategy (the checker refuses a body there).
2170    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2171    pub body: Vec<IRFlowNode>,
2172}
2173
2174/// v2.27.0 — the lowered temporal execution-window guard. The runtime
2175/// (v2.27.0) evaluates `is_in_window(now, tz, allow)`; the daemon binding +
2176/// coalesced defer ledger are v2.27.0.
2177#[derive(Debug, Clone, Serialize)]
2178pub struct IRWindow {
2179    pub node_type: &'static str,
2180    pub source_line: u32,
2181    pub source_column: u32,
2182    pub name: String,
2183    pub timezone: String,
2184    pub allow: Vec<IRWindowSpan>,
2185    /// v2.27.0 — excluded dates (holidays): ISO `YYYY-MM-DD` literals. A tick
2186    /// whose local date is in this set is OUTSIDE regardless of the hour spans.
2187    /// `skip_serializing_if` keeps a holiday-less window's JSON byte-identical.
2188    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2189    pub exclude: Vec<String>,
2190    pub on_outside: String,
2191}
2192
2193/// v2.27.0 — one allowed day/hour span.
2194#[derive(Debug, Clone, Serialize)]
2195pub struct IRWindowSpan {
2196    pub day_start: String,
2197    pub day_end: String,
2198    pub hour_start: i64,
2199    pub hour_end: i64,
2200}
2201
2202/// v2.28.0 — the `budget { … }` linear-effect rate limit lowered to IR. Each
2203/// quota gates a declared tool's dispatch on a renewable token bucket (the v2.28.0
2204/// `RateLease`); `on_exhausted` is the exhaustion policy.
2205#[derive(Debug, Clone, Serialize)]
2206pub struct IRBudget {
2207    pub node_type: &'static str,
2208    pub source_line: u32,
2209    pub source_column: u32,
2210    /// v2.69.0 — the name of a **top-level** `budget`. Empty ⇒ the anonymous
2211    /// daemon-attached form. Skip-if-empty ⇒ every pre-v2.69.0 program serializes
2212    /// byte-identically (IR-SHA stability).
2213    #[serde(default, skip_serializing_if = "String::is_empty")]
2214    pub name: String,
2215    pub quotas: Vec<IRBudgetQuota>,
2216    /// `block` (fail-closed) | `defer` (reschedule via the v2.27.0 defer ledger) |
2217    /// `shed` (skip the call). An omitted policy lowers to `block` (the safe
2218    /// fail-closed default).
2219    pub on_exhausted: String,
2220}
2221
2222/// v2.28.0 — one quota: `<kind>: <limit> per <period> on Tool(<effect>)`.
2223#[derive(Debug, Clone, Serialize)]
2224pub struct IRBudgetQuota {
2225    /// `rate` (renewable bucket) | `max` (windowed hard cap, no intra-window refill).
2226    pub kind: String,
2227    /// Token allowance per period (> 0, validated by `axon-T831`).
2228    pub limit: i64,
2229    /// `second` | `minute` | `hour` | `day` (closed catalog, `axon-T832`).
2230    pub period: String,
2231    /// The declared tool this quota governs (`on Tool(X)`; resolved by `axon-T830`).
2232    pub effect: String,
2233}
2234
2235#[derive(Debug, Clone, Serialize)]
2236pub struct IRShield {
2237    pub node_type: &'static str,
2238    pub source_line: u32,
2239    pub source_column: u32,
2240    pub name: String,
2241    pub scan: Vec<String>,
2242    pub strategy: String,
2243    pub on_breach: String,
2244    pub severity: String,
2245    pub quarantine: String,
2246    /// section 8.2.h.3 — Python emits concrete 0, not null. AST keeps `Option<i64>`
2247    /// so the parser can distinguish "not set"; IR lowering collapses.
2248    pub max_retries: i64,
2249    pub confidence_threshold: f64,
2250    pub allow_tools: Vec<String>,
2251    pub deny_tools: Vec<String>,
2252    pub sandbox: bool,
2253    pub redact: Vec<String>,
2254    pub log: String,
2255    pub deflect_message: String,
2256    // `taint` exists on `ShieldDefinition` (AST) but Python's reference
2257    // IRShield doesn't emit it. Hidden from JSON output for section 8.2.h parity.
2258    #[serde(skip)]
2259    pub taint: String,
2260    /// ESK — covered regulatory classes for this shield.
2261    pub compliance: Vec<String>,
2262    /// v2.34.0 — egress signing algorithm (`hmac_sha256`; empty = the
2263    /// shield does not sign). Elided from JSON when empty so every pre-v2.34.0
2264    /// program's IR stays byte-identical (zero IR-SHA drift).
2265    #[serde(skip_serializing_if = "String::is_empty")]
2266    pub sign: String,
2267}
2268
2269#[derive(Debug, Clone, Serialize)]
2270pub struct IRPix {
2271    pub node_type: &'static str,
2272    pub source_line: u32,
2273    pub source_column: u32,
2274    pub name: String,
2275    pub source: String,
2276    pub depth: Option<i64>,
2277    pub branching: Option<i64>,
2278    pub model: String,
2279}
2280
2281/// v2.12.0 — the audit-chain (`ledger`) IR node. Same shape as [`IRPix`]
2282/// but a DISTINCT node (`node_type: "ledger"`): a ledger binds a hash-linked
2283/// recorder to an audited surface (`source`), retaining `depth` rows under a
2284/// `branching`-factor Merkle tree, hashed with `model`. Kept separate from
2285/// `IRPix` so the navigator and the audit chain never alias on the wire.
2286#[derive(Debug, Clone, Serialize)]
2287pub struct IRLedger {
2288    pub node_type: &'static str,
2289    pub source_line: u32,
2290    pub source_column: u32,
2291    pub name: String,
2292    pub source: String,
2293    pub depth: Option<i64>,
2294    pub branching: Option<i64>,
2295    pub model: String,
2296}
2297
2298#[derive(Debug, Clone, Serialize)]
2299pub struct IRPsyche {
2300    pub node_type: &'static str,
2301    pub source_line: u32,
2302    pub source_column: u32,
2303    pub name: String,
2304    pub dimensions: Vec<String>,
2305    pub manifold_noise: Option<f64>,
2306    pub manifold_momentum: Option<f64>,
2307    pub safety_constraints: Vec<String>,
2308    pub quantum_enabled: Option<bool>,
2309    pub inference_mode: String,
2310}
2311
2312#[derive(Debug, Clone, Serialize)]
2313pub struct IRCorpus {
2314    pub node_type: &'static str,
2315    pub source_line: u32,
2316    pub source_column: u32,
2317    pub name: String,
2318    pub documents: Vec<String>,
2319    /// v2.13.0 — typed weighted edges. Non-empty ⇒ this corpus is an MDN
2320    /// graph `C = (D, R, τ, ω, σ)`; the runtime builds an `mdn::Corpus` from it.
2321    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2322    pub relations: Vec<IRCorpusRelation>,
2323    /// v2.13.0 — `adaptive: true` enables the memory endofunctor on this
2324    /// corpus's navigations.
2325    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2326    pub adaptive: bool,
2327    pub mcp_server: String,
2328    pub mcp_resource_uri: String,
2329    /// v2.14.0 — when present, this is a DYNAMIC store-sourced MDN graph: the
2330    /// documents and typed edges are rows in two declared `axonstore`s and the
2331    /// runtime builds the `mdn::Corpus` from the live rows at navigate-time
2332    /// (per-tenant, growing). Absent ⇒ the static v2.13.0 corpus (byte-identical IR).
2333    #[serde(default, skip_serializing_if = "Option::is_none")]
2334    pub store_source: Option<IRCorpusStoreSource>,
2335}
2336
2337/// v2.13.0 — a lowered MDN corpus-graph edge `(from, to, τ, ω)`.
2338#[derive(Debug, Clone, Serialize)]
2339pub struct IRCorpusRelation {
2340    pub etype: String,
2341    pub from: String,
2342    pub to: String,
2343    pub weight: f64,
2344}
2345
2346/// v2.14.0 — the lowered store-mapping of a dynamic, `axonstore`-sourced MDN
2347/// corpus graph. `doc_store(doc_id, doc_title)` maps rows → nodes;
2348/// `edge_store(edge_from, edge_to, edge_type, edge_weight)` maps rows → typed
2349/// weighted edges. The runtime (v2.14.0) reads these stores tenant-scoped at
2350/// navigate-time to build the `mdn::Corpus`.
2351#[derive(Debug, Clone, Serialize)]
2352pub struct IRCorpusStoreSource {
2353    pub doc_store: String,
2354    pub doc_id: String,
2355    pub doc_title: String,
2356    pub edge_store: String,
2357    pub edge_from: String,
2358    pub edge_to: String,
2359    pub edge_type: String,
2360    pub edge_weight: String,
2361}
2362
2363/// v2.63.0 — one compiled dataspace column. `column_type` is the
2364/// CANONICAL catalog name (`Text` / `Int` / `Float` / `Bool` /
2365/// `Timestamp` / `Json`) — aliases are resolved at IR generation, so
2366/// every downstream consumer (the engine's deploy hook, the v2.63.0 PCC
2367/// class) reads one spelling.
2368#[derive(Debug, Clone, Serialize)]
2369pub struct IRDataspaceColumn {
2370    pub name: String,
2371    pub column_type: String,
2372}
2373
2374#[derive(Debug, Clone, Serialize)]
2375pub struct IRDataspace {
2376    pub node_type: &'static str,
2377    pub source_line: u32,
2378    pub source_column: u32,
2379    pub name: String,
2380    /// v2.63.0 — the typed columnar schema (canonical type names).
2381    pub columns: Vec<IRDataspaceColumn>,
2382}
2383
2384#[derive(Debug, Clone, Serialize)]
2385pub struct IROts {
2386    pub node_type: &'static str,
2387    pub source_line: u32,
2388    pub source_column: u32,
2389    pub name: String,
2390    pub teleology: String,
2391    pub homotopy_search: String,
2392    pub loss_function: String,
2393}
2394
2395#[derive(Debug, Clone, Serialize)]
2396pub struct IRMandate {
2397    pub node_type: &'static str,
2398    pub source_line: u32,
2399    pub source_column: u32,
2400    pub name: String,
2401    pub constraint: String,
2402    pub kp: Option<f64>,
2403    pub ki: Option<f64>,
2404    pub kd: Option<f64>,
2405    pub tolerance: Option<f64>,
2406    pub max_steps: Option<i64>,
2407    /// v2.83.0 — the declared stability hypotheses `(D, L)`, carried to the
2408    /// runtime as PROOF OBLIGATIONS: the static band check was conditional on
2409    /// them, so dispatch must verify them against the measured backend or
2410    /// refuse. `None` means nothing was statically promised — absence is
2411    /// visible, never defaulted.
2412    pub drift_bound: Option<f64>,
2413    pub lipschitz: Option<f64>,
2414    pub on_violation: String,
2415}
2416
2417#[derive(Debug, Clone, Serialize)]
2418pub struct IRCompute {
2419    pub node_type: &'static str,
2420    pub source_line: u32,
2421    pub source_column: u32,
2422    pub name: String,
2423    pub shield_ref: String,
2424    /// v2.67.0 — typed parameters. ADDITIVE (`skip_serializing_if`), so every
2425    /// pre-111 program's IR JSON stays byte-identical.
2426    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2427    pub parameters: Vec<IRParameter>,
2428    /// v2.67.0 — the declared result type.
2429    #[serde(default, skip_serializing_if = "String::is_empty")]
2430    pub return_type: String,
2431    /// v2.67.0 — the lowered v2.26.0 expression the runtime evaluates natively.
2432    /// `None` ⇒ the apply is refused (axon-T941): a compute with no body cannot
2433    /// compute, and binding a placeholder string in its place is how the old
2434    /// runtime handed a downstream step the text `"compute:Name(args)"` where it
2435    /// expected a number.
2436    #[serde(default, skip_serializing_if = "Option::is_none")]
2437    pub body: Option<IRExpr>,
2438}
2439
2440#[derive(Debug, Clone, Serialize)]
2441pub struct IRDaemon {
2442    pub node_type: &'static str,
2443    pub source_line: u32,
2444    pub source_column: u32,
2445    pub name: String,
2446    pub goal: String,
2447    pub tools: Vec<String>,
2448    pub memory_ref: String,
2449    pub strategy: String,
2450    pub on_stuck: String,
2451    pub shield_ref: String,
2452    /// v2.27.0 — the `window:` temporal binding (a `window` primitive name).
2453    /// Empty ⇒ no temporal guard; `skip_serializing_if` keeps a windowless
2454    /// daemon's JSON byte-identical (D8 zero-drift).
2455    #[serde(default, skip_serializing_if = "String::is_empty")]
2456    pub window_ref: String,
2457    /// v2.28.0 — the `budget { … }` linear-effect rate limit. `None` ⇒ no
2458    /// budget; `skip_serializing_if` keeps a budgetless daemon's JSON
2459    /// byte-identical (D8 zero-drift).
2460    #[serde(default, skip_serializing_if = "Option::is_none")]
2461    pub budget: Option<IRBudget>,
2462    pub max_tokens: Option<i64>,
2463    pub max_time: String,
2464    pub max_cost: Option<f64>,
2465    /// v2.4.0 — the daemon's `listen` listeners (channel + alias + handler
2466    /// body). Pre-v2.4.0 these were DROPPED at lowering (the IR daemon carried no
2467    /// listeners at all); now they survive so the v2.4.0 runtime can mount + run
2468    /// them and the v2.4.0 enterprise supervisor can extract them per-tenant.
2469    /// `skip_serializing_if` keeps a listenerless daemon's JSON unchanged (D8).
2470    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2471    pub listeners: Vec<IRListenStep>,
2472    /// v2.4.0 — the capability scope the daemon's runs are confined to
2473    /// (`requires: [cap, …]`). The enterprise supervisor mints a per-run
2474    /// principal scoped to exactly these. `skip_serializing_if` keeps a
2475    /// requires-less daemon's JSON byte-identical (D8).
2476    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2477    pub requires_capabilities: Vec<String>,
2478}
2479
2480// ── v2.42.0 — the long-horizon autonomous research primitive ────────────────
2481
2482/// v2.42.0 — a compiled `savant` (long-horizon autonomous research
2483/// primitive). A governed orchestrator: the IR carries the declared surface so
2484/// the enterprise active-inference engine (v2.42.0+) can drive the FEP loop, and
2485/// the v2.42.0 checker can bind `memory` to a declared store, `budget` to a v2.28.0
2486/// linear budget, and the body to a v2.36.0 interruptible session.
2487#[derive(Debug, Clone, Serialize)]
2488pub struct IRSavant {
2489    pub node_type: &'static str,
2490    pub source_line: u32,
2491    pub source_column: u32,
2492    pub name: String,
2493    pub domain: String,
2494    #[serde(default, skip_serializing_if = "Option::is_none")]
2495    pub cognition: Option<IRSavantCognition>,
2496    #[serde(default, skip_serializing_if = "Option::is_none")]
2497    pub memory: Option<IRSavantMemory>,
2498    #[serde(default, skip_serializing_if = "Option::is_none")]
2499    pub budget: Option<IRSavantBudget>,
2500    pub mandates: Vec<IRSavantMandate>,
2501}
2502
2503/// v2.42.0 — the compiled `cognition { … }` sub-block (active-inference
2504/// engine parameters).
2505#[derive(Debug, Clone, Serialize)]
2506pub struct IRSavantCognition {
2507    /// `standard | deep | hyper` — HRR dimensionality tier (validated v2.42.0).
2508    pub depth: String,
2509    /// Expected-Free-Energy convergence bound (`> 0`, v2.42.0). `None` ⇒ default.
2510    #[serde(default, skip_serializing_if = "Option::is_none")]
2511    pub entropic_threshold: Option<f64>,
2512    /// `low | med | high` — explore/exploit balance (validated v2.42.0).
2513    pub divergence: String,
2514}
2515
2516/// v2.42.0 — the compiled `memory { … }` sub-block (retention binding).
2517#[derive(Debug, Clone, Serialize)]
2518pub struct IRSavantMemory {
2519    /// A declared `memory`/`corpus` name (resolved v2.42.0). Empty ⇒ ephemeral.
2520    pub backend: String,
2521    /// Whether to index the corpus as a simplicial-complex graph (topological
2522    /// β_n reading).
2523    pub corpus_graph: bool,
2524    /// Per-tenant tensor partitioning level (enforced by the enterprise engine).
2525    pub isolation_level: String,
2526}
2527
2528/// v2.42.0 — the compiled `budget { … }` sub-block (compute ceiling, bound to
2529/// a v2.28.0 linear budget in v2.42.0).
2530#[derive(Debug, Clone, Serialize)]
2531pub struct IRSavantBudget {
2532    /// Hard ceiling on FEP-loop iterations before the savant pauses.
2533    #[serde(default, skip_serializing_if = "Option::is_none")]
2534    pub max_iterations: Option<i64>,
2535    /// Hard ceiling on `synth` (v2.42.0) tool-creation events per mandate.
2536    #[serde(default, skip_serializing_if = "Option::is_none")]
2537    pub max_tool_synth: Option<i64>,
2538}
2539
2540/// v2.42.0 — a compiled `mandate <Name> { … }` sub-block (one research goal).
2541#[derive(Debug, Clone, Serialize)]
2542pub struct IRSavantMandate {
2543    pub name: String,
2544    pub objective: String,
2545    pub output_type: String,
2546}
2547
2548// ── v2.53.0 — Native Document Synthesis IR ─────────────────────────────────
2549
2550/// v2.53.0 — a compiled document declaration. The runtime `DocumentRenderer`
2551/// tool serialises this to deterministic OOXML bytes (v2.53.0); the
2552/// `DocumentProvenanceSoundness` PCC class (v2.53.0) re-derives the barrier from
2553/// it. `blocks` is the closed-catalog body tree.
2554#[derive(Debug, Clone, Serialize)]
2555pub struct IRDocument {
2556    pub node_type: &'static str,
2557    pub source_line: u32,
2558    pub source_column: u32,
2559    pub name: String,
2560    /// `docx | pptx | xlsx`.
2561    pub target: String,
2562    #[serde(default, skip_serializing_if = "String::is_empty")]
2563    pub template: String,
2564    #[serde(default, skip_serializing_if = "String::is_empty")]
2565    pub provenance: String,
2566    /// The propagated effect row (`io`, `storage`, `sensitive:*`, `legal:*`).
2567    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2568    pub effect_row: Vec<String>,
2569    /// v2.53.0 — the enclosing `epistemic { mode: … }` at compile time
2570    /// (`believe`/`know` vouch the whole document is ≥ believe, satisfying the
2571    /// assertion-laundering barrier without per-field `attribute:`). Empty at
2572    /// top level. Recorded so the `DocumentProvenanceSoundness` PCC class
2573    /// re-derives the barrier identically (no false refutation).
2574    #[serde(default, skip_serializing_if = "String::is_empty")]
2575    pub epistemic_mode: String,
2576    pub blocks: Vec<IRDocBlock>,
2577}
2578
2579/// v2.53.0 — one compiled document block. `fields` preserves declaration
2580/// order (a `Vec` of `(name, value)`), so the render is deterministic.
2581#[derive(Debug, Clone, Serialize)]
2582pub struct IRDocBlock {
2583    pub kind: String,
2584    pub fields: Vec<IRDocField>,
2585    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2586    pub children: Vec<IRDocBlock>,
2587}
2588
2589/// v2.53.0 — a compiled `(field, value)` pair. `kind` tags the value shape
2590/// (`text`|`ref`|`list`|`int`|`bool`) so the renderer + the barrier can
2591/// discriminate a literal from a flow-value reference without re-parsing.
2592#[derive(Debug, Clone, Serialize)]
2593pub struct IRDocField {
2594    pub name: String,
2595    /// `text | ref | list | int | bool`.
2596    pub kind: &'static str,
2597    #[serde(default, skip_serializing_if = "String::is_empty")]
2598    pub value: String,
2599    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2600    pub items: Vec<String>,
2601}
2602
2603// ── v2.60.0 — Governed CRM Delivery IR ─────────────────────────────────────
2604
2605/// v2.60.0 — a compiled delivery declaration. The runtime delivery dispatch
2606/// (`axon::delivery`) transduces this to the configured CRM engine; the
2607/// `DeliveryProvenanceSoundness` PCC class (T920) re-derives the barrier from it.
2608/// `ops` is the closed-catalog operation list.
2609/// v2.66.0 — the compiled `notify` declaration. `epistemic_mode`
2610/// records the enclosing vouch (the v2.53.0/v2.60.0 discipline) so T933
2611/// re-derives identically at deploy (PCC `NotificationProvenanceSoundness`).
2612#[derive(Debug, Clone, Serialize)]
2613pub struct IRNotify {
2614    pub node_type: &'static str,
2615    pub source_line: u32,
2616    pub source_column: u32,
2617    pub name: String,
2618    pub channel: String,
2619    /// The v2.48.0 secret-class ref (the recipient value NEVER rides the IR).
2620    pub to_secret: String,
2621    pub template: String,
2622    pub window: String,
2623    #[serde(default, skip_serializing_if = "String::is_empty")]
2624    pub provenance: String,
2625    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2626    pub effects: Vec<String>,
2627    #[serde(default, skip_serializing_if = "String::is_empty")]
2628    pub epistemic_mode: String,
2629}
2630
2631#[derive(Debug, Clone, Serialize)]
2632pub struct IRDeliver {
2633    pub node_type: &'static str,
2634    pub source_line: u32,
2635    pub source_column: u32,
2636    pub name: String,
2637    /// `crm`.
2638    pub target: String,
2639    /// `attached | cleared` (empty ⇒ `attached`). How field provenance crosses
2640    /// the boundary — the T920 barrier's subject.
2641    #[serde(default, skip_serializing_if = "String::is_empty")]
2642    pub provenance: String,
2643    /// The per-tenant credential key (v2.48.0 custody — resolved at dispatch, never
2644    /// in cognition).
2645    #[serde(default, skip_serializing_if = "String::is_empty")]
2646    pub secret: String,
2647    /// The propagated effect row (`web`, `sensitive:*`, `legal:*`).
2648    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2649    pub effect_row: Vec<String>,
2650    /// The enclosing `epistemic { mode: … }` at compile time (`believe`/`know`
2651    /// vouch the delivered values are ≥ believe, satisfying the T920 barrier for
2652    /// a `provenance: cleared` delivery). Empty at top level. Recorded so the
2653    /// `DeliveryProvenanceSoundness` PCC class re-derives the barrier identically.
2654    #[serde(default, skip_serializing_if = "String::is_empty")]
2655    pub epistemic_mode: String,
2656    pub ops: Vec<IRDeliverOp>,
2657}
2658
2659/// v2.60.0 — one compiled delivery operation. `fields` preserves declaration
2660/// order (a `Vec` of `(name, value)`) so the transduced request is deterministic.
2661/// Reuses [`IRDocField`] — the same `(name, kind, value)` shape a document block
2662/// field carries, so the barrier + transducer discriminate a literal from a
2663/// flow-value `ref` without re-parsing.
2664#[derive(Debug, Clone, Serialize)]
2665pub struct IRDeliverOp {
2666    /// `upsert_contact | create_deal | add_note`.
2667    pub kind: String,
2668    pub fields: Vec<IRDocField>,
2669}
2670
2671/// v2.42.0 — a compiled `synth` dynamic tool-synthesis policy. The IR carries
2672/// the safety envelope so the enterprise Extism/WASM executor (v2.42.0) enforces
2673/// it; OSS ships a deny-by-default `SynthBackend` that refuses execution.
2674#[derive(Debug, Clone, Serialize)]
2675pub struct IRSynth {
2676    pub node_type: &'static str,
2677    pub source_line: u32,
2678    pub source_column: u32,
2679    pub name: String,
2680    pub target: String,
2681    pub risk: String,
2682    #[serde(default, skip_serializing_if = "String::is_empty")]
2683    pub language: String,
2684    pub sandbox: String,
2685    /// `required | none`; an omitted policy lowers to `required` (fail-closed).
2686    pub review: String,
2687    #[serde(default, skip_serializing_if = "Option::is_none")]
2688    pub max_lines: Option<i64>,
2689}
2690
2691// ── v2.5.0 — Closed-catalog extension mechanism ────────────────────────────
2692
2693/// v2.5.0 — one compiled member of an `extension`. For `effects`
2694/// the `name` is a provenance base; `default_confidence` is a CEILING
2695/// (v2.5.0 tainted-overriding). Metadata is elided from JSON when absent
2696/// so the serialised shape stays minimal once the `extensions` field is
2697/// un-skipped alongside the Python IR mirror.
2698#[derive(Debug, Clone, Serialize)]
2699pub struct IRExtensionMember {
2700    pub name: String,
2701    #[serde(default, skip_serializing_if = "Option::is_none")]
2702    pub semantics: Option<String>,
2703    #[serde(default, skip_serializing_if = "Option::is_none")]
2704    pub default_confidence: Option<f64>,
2705}
2706
2707/// v2.5.0 — a compiled `extension` declaration. Rides in the IR (and,
2708/// once un-skipped, the proof bundle) so an independent PCC verifier
2709/// re-derives `is_known_base` against the artifact's own extensions
2710/// (soundness invariant #1). `category` ∈ {`effects`, `scan`} — the
2711/// type-checker (v2.5.0) enforces the closed category + no-shadowing +
2712/// provenance-class invariants before this IR is trusted.
2713#[derive(Debug, Clone, Serialize)]
2714pub struct IRExtension {
2715    pub node_type: &'static str,
2716    pub source_line: u32,
2717    pub source_column: u32,
2718    pub name: String,
2719    pub category: String,
2720    pub members: Vec<IRExtensionMember>,
2721}
2722
2723#[derive(Debug, Clone, Serialize)]
2724pub struct IRAxonStore {
2725    pub node_type: &'static str,
2726    pub source_line: u32,
2727    pub source_column: u32,
2728    pub name: String,
2729    pub backend: String,
2730    /// The DSN. **This is the field that actually runs.**
2731    ///
2732    /// `connection:` → `resolve_dsn` → a real sqlx `PgPool`. It is the sole DSN
2733    /// source for every store op in every deployed flow; there is no
2734    /// global-pool fallback. v2.67.0's census established this, and it is why
2735    /// v2.67.0 is delicate: moving authority to `resource` moves it *away* from
2736    /// the one field that governs anything, *toward* the half that governs
2737    /// nothing. A `resource:` that merely renames this string would be the
2738    /// nominal link — wired and hollow.
2739    ///
2740    /// v2.67.0: still parsed, but **deprecated in favour of [`Self::resource_ref`]**,
2741    /// and a store declared this way is INELIGIBLE for `lease` / `observe` /
2742    /// `reconcile`. *You cannot govern what you did not declare.*
2743    pub connection: String,
2744    /// v2.67.0 — the `resource` this store runs on (`axonstore U { resource: Db }`).
2745    ///
2746    /// When present, the store DERIVES its DSN (`resource.endpoint`), its pool
2747    /// size (`resource.capacity` — a knob that did not exist before v2.67.0; the
2748    /// pool was hardcoded at 10), and its sharing discipline
2749    /// (`resource.lifetime`, `axon-T941`) from the resource. **That derivation
2750    /// — not the reference — is what makes this real.**
2751    ///
2752    /// Empty ⇒ the legacy un-resourced form. Skip-if-empty ⇒ every pre-v2.67.0
2753    /// store serializes byte-identically (the v2.48.0 `class` precedent).
2754    #[serde(default, skip_serializing_if = "String::is_empty")]
2755    pub resource_ref: String,
2756    pub confidence_floor: Option<f64>,
2757    pub isolation: String,
2758    pub on_breach: String,
2759    /// v1.30.0 (D11) — Pillar IV: the capability slug required to
2760    /// access this store (empty = no gate).
2761    pub capability: String,
2762    /// v2.48.0 — the secret-class prefix of a `backend: secrets`
2763    /// metadata store (`rotation_without_revelation`). Non-empty ⇔
2764    /// `backend == "secrets"` (both directions enforced by `axon-T900`
2765    /// before the IR ships). Elided from the wire when empty — every
2766    /// pre-v2.48.0 store serializes byte-identically (IR-SHA stability).
2767    #[serde(default, skip_serializing_if = "String::is_empty")]
2768    pub class: String,
2769    /// v1.31.0 (D1) — the OPTIONAL column-schema declaration. Three
2770    /// closed forms (inline / manifest-ref / env-var). `None` means the
2771    /// 37.x runtime+deploy path applies verbatim (D5 absolute). The
2772    /// v1.31.0 / v1.31.0 type-checker proves every store reference against
2773    /// this when present. v2.48.0: for a `backend: secrets` store the
2774    /// generator synthesizes the FIXED metadata schema here (the artifact
2775    /// is self-describing; PCC and the deploy gate re-derive against it).
2776    #[serde(default, skip_serializing_if = "Option::is_none")]
2777    pub column_schema: Option<IRStoreColumnSchema>,
2778}
2779
2780/// v1.31.0 (D1) — IR mirror of [`crate::store_schema::StoreColumnSchema`].
2781/// Serializes as a tagged union: `{"form": "inline" | "manifest_ref" |
2782/// "env_var", …}`.
2783#[derive(Debug, Clone, Serialize)]
2784#[serde(tag = "form", rename_all = "snake_case")]
2785pub enum IRStoreColumnSchema {
2786    Inline { columns: Vec<IRStoreColumn> },
2787    ManifestRef { qualified_name: String },
2788    EnvVar { var_name: String },
2789}
2790
2791/// v1.31.0 (D1) — IR mirror of [`crate::store_schema::StoreColumn`].
2792/// The serialized `col_type` is the canonical PascalCase name (e.g.
2793/// `"Uuid"`, `"Int"`, `"Timestamptz"`).
2794#[derive(Debug, Clone, Serialize)]
2795pub struct IRStoreColumn {
2796    pub name: String,
2797    pub col_type: String,
2798    #[serde(default, skip_serializing_if = "is_false")]
2799    pub primary_key: bool,
2800    #[serde(default, skip_serializing_if = "is_false")]
2801    pub auto_increment: bool,
2802    #[serde(default, skip_serializing_if = "is_false")]
2803    pub not_null: bool,
2804    #[serde(default, skip_serializing_if = "is_false")]
2805    pub unique: bool,
2806    #[serde(default, skip_serializing_if = "String::is_empty")]
2807    pub default_value: String,
2808    /// v1.31.0 (D2, D5) — `true` iff the column is declared with
2809    /// `GENERATED ALWAYS AS IDENTITY` or `GENERATED BY DEFAULT AS
2810    /// IDENTITY`. Distinct from `auto_increment` (legacy SERIAL via
2811    /// `nextval(...)` default). `skip_serializing_if` keeps IR JSON
2812    /// byte-identical to v1.38.2 for any column where `identity = false`.
2813    #[serde(default, skip_serializing_if = "is_false")]
2814    pub identity: bool,
2815    /// v2.26.0 (D1) — `true` iff the column carries the `index`
2816    /// declaration. Surfaced into the IR so the deployment layer (the
2817    /// enterprise deploy gate) SEES the index as a declared capability and
2818    /// can materialize it (a GIN path index for a `Json`/`Jsonb` column, a
2819    /// b-tree otherwise) — never a silent out-of-band DBA action.
2820    /// `skip_serializing_if` keeps IR JSON byte-identical for any column
2821    /// where `indexed = false`.
2822    #[serde(default, skip_serializing_if = "is_false")]
2823    pub indexed: bool,
2824    /// v2.26.0 (D1) — the OPTIONAL `Json<T>` shape-lens struct name on a
2825    /// `Json`/`Jsonb` column (`payload: Json<UserEvent>` → `Some("UserEvent")`).
2826    /// Surfaced into the IR so the PCC `JsonShapeSoundness` proof can
2827    /// RE-DERIVE, from the artifact alone, that every lens shape resolves
2828    /// to a declared struct `type` — the v2.26.0/v2.26.0 lens well-formedness
2829    /// made an independently-verifiable proof object. `skip_serializing_if`
2830    /// keeps IR JSON byte-identical for any column with no shape lens.
2831    #[serde(default, skip_serializing_if = "Option::is_none")]
2832    pub json_shape: Option<String>,
2833}
2834
2835#[inline]
2836fn is_false(b: &bool) -> bool {
2837    !*b
2838}
2839
2840#[derive(Debug, Clone, Serialize)]
2841pub struct IRAxonEndpoint {
2842    pub node_type: &'static str,
2843    pub source_line: u32,
2844    pub source_column: u32,
2845    pub name: String,
2846    pub method: String,
2847    pub path: String,
2848    pub body_type: String,
2849    pub execute_flow: String,
2850    pub output_type: String,
2851    pub shield_ref: String,
2852    /// section 8.2.h.3 — Python emits concrete `0`; AST stays `Option<i64>`.
2853    pub retries: i64,
2854    pub timeout: String,
2855    /// ESK — κ regulatory class on the boundary.
2856    pub compliance: Vec<String>,
2857    /// v1.32.0 (D1) — Path parameter names extracted from the
2858    /// `path:` string. Mirrors `AxonEndpointDefinition.path_params`.
2859    /// **`skip_serializing_if = Vec::is_empty`** so a pre-v1.38.5 IR
2860    /// JSON snapshot (without the field) is byte-identical to a
2861    /// v1.38.5 IR JSON for the same endpoint — D5 backwards-compat
2862    /// absolute. The runtime + adopter tools that consume the IR
2863    /// JSON parse `path_params` as an absent key → empty Vec via
2864    /// serde's `default` semantics.
2865    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2866    pub path_params: Vec<String>,
2867    /// v1.32.0 (D2) — Query parameters from the inline
2868    /// `query: { … }` block. Mirrors `AxonEndpointDefinition.query_params`
2869    /// using `IRTypeField` (shared with body type fields → uniform
2870    /// downstream tooling). **`skip_serializing_if = Vec::is_empty`**
2871    /// — same D5 IR-JSON byte-identity guarantee as `path_params`.
2872    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2873    pub query_params: Vec<IRTypeField>,
2874    /// v2.4.0 — capability scopes the request bearer must hold
2875    /// (the `requires: [scope.dotted]` declaration, v1.23.0). Mirror
2876    /// of `AxonEndpointDefinition.requires_capabilities`, lowered into
2877    /// the IR so the PCC CapabilityContainment property can prove that
2878    /// the stores this endpoint's flow reaches are all covered by the
2879    /// declared requires. **`skip_serializing_if = Vec::is_empty`** so a
2880    /// pre-v2.4.0 IR-JSON snapshot (no `requires:`) stays byte-identical
2881    /// (D5 backwards-compat — empty key parses back to empty Vec).
2882    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2883    pub requires_capabilities: Vec<String>,
2884    /// v2.38.0 — the `cors: <Name>` reference, or `""` when absent
2885    /// (the design decision: no CORS headers, ever). NEW field on an EXISTING struct —
2886    /// `skip_serializing_if` (not `shield_ref`'s bare/always-emitted
2887    /// historical shape) so a cors-less endpoint's IR stays byte-identical
2888    /// to pre-v2.38.0 (zero IR-SHA drift — the standing v2.33.0 discipline).
2889    #[serde(default, skip_serializing_if = "String::is_empty")]
2890    pub cors_ref: String,
2891    /// v2.44.0 — the explicit authorization-coverage opt-out lowered into
2892    /// the IR so the enterprise runtime (v2.44.0) and the PCC
2893    /// `AuthorizationCoverage` witness (v2.44.0) can read it. `false` (the
2894    /// default + the common case) elides from JSON via `is_false` so a
2895    /// pre-v2.44.0 IR-JSON snapshot stays byte-identical (zero IR-SHA drift —
2896    /// the standing v2.33.0 discipline).
2897    #[serde(default, skip_serializing_if = "is_false")]
2898    pub public: bool,
2899}
2900
2901// ── v1.6.0 — Mobile Typed Channels IR ───────────────────────────────
2902
2903/// Compiled `channel Name { … }` declaration.
2904///
2905/// Direct port of `axon.compiler.ir_nodes.IRChannel`.  Lives in
2906/// `IRProgram.channels`; emit/publish/discover reductions embed in
2907/// their containing flow/listener (paper section 3 + section 4 — π-calc prefix
2908/// discipline preserved structurally, not lifted to top-level ops).
2909#[derive(Debug, Clone, Serialize)]
2910pub struct IRChannel {
2911    pub node_type: &'static str,
2912    pub source_line: u32,
2913    pub source_column: u32,
2914    pub name: String,
2915    pub message: String, // surface spelling — Order | Channel<Order> | …
2916    pub qos: String,
2917    pub lifetime: String,
2918    pub persistence: String,
2919    pub shield_ref: String,
2920    /// v2.34.0 — non-empty ⇒ some `publish <this> within <Shield>` site
2921    /// referenced a SIGNING shield: the channel is an EGRESS channel and
2922    /// its durable events are signed-deliverable to registered external
2923    /// subscribers under this algorithm (first publish site wins;
2924    /// deterministic — the catalog has one algorithm in v1). Elided from
2925    /// JSON when empty (zero IR-SHA drift for pre-v2.34.0 programs).
2926    #[serde(skip_serializing_if = "String::is_empty")]
2927    pub egress_sign: String,
2928}
2929
2930/// v2.3.0 — compiled typed WebSocket transport. `protocol` names the
2931/// `session` it carries; `backpressure_credit` is the typed-resource window
2932/// (`null` if unspecified). axon-rs realises the endpoint over a `tokio` WS,
2933/// crediting/decrementing the window per section 4.2 of the paper.
2934#[derive(Debug, Clone, Serialize)]
2935pub struct IRSocket {
2936    pub node_type: &'static str,
2937    pub source_line: u32,
2938    pub source_column: u32,
2939    pub name: String,
2940    pub protocol: String,
2941    pub backpressure_credit: Option<i64>,
2942    pub reconnect: bool,
2943    pub legal_basis: Option<String>,
2944}
2945
2946/// v2.37.0 — compiled outbound vendor connection (the client dual of
2947/// [`IRSocket`]). `protocol`/`role` bind the axon-facing session interface;
2948/// `resolve`/`secret` are per-tenant config keys (never literals — T850);
2949/// `map` is the compile-time-total wire↔session projection (T849). Optional
2950/// fields elide when absent so the IR shape is purely additive.
2951#[derive(Debug, Clone, Serialize)]
2952pub struct IRUpstream {
2953    pub node_type: &'static str,
2954    pub source_line: u32,
2955    pub source_column: u32,
2956    pub name: String,
2957    pub transport: String,
2958    pub protocol: String,
2959    pub role: String,
2960    pub resolve: String,
2961    /// v2.69.0 — the `resource` this upstream's channel rides. When set,
2962    /// `resolve` above was DERIVED from the resource's `endpoint` at LOWERING
2963    /// (the v2.69.0 shield-egress discipline: derivation stamped into the
2964    /// artifact reaches every dial path by construction — no per-site wiring
2965    /// to forget) and `capacity` below carries the resource's bound. Elided
2966    /// when empty — every pre-v2.69.0 upstream serializes byte-identically.
2967    #[serde(default, skip_serializing_if = "String::is_empty")]
2968    pub resource_ref: String,
2969    /// v2.69.0 — **max concurrent connection INSTANCES** of this upstream
2970    /// (from `resource.capacity`). Frames are already flow-controlled by
2971    /// `backpressure_credit`; this bounds CONNECTIONS. The runtime holds a
2972    /// per-process semaphore permit for the life of each dialed handle —
2973    /// the same in-memory/per-process bound v2.69.0 documented for tools.
2974    #[serde(default, skip_serializing_if = "Option::is_none")]
2975    pub capacity: Option<i64>,
2976    pub secret: String,
2977    pub auth_kind: String,
2978    #[serde(default, skip_serializing_if = "Option::is_none")]
2979    pub auth_name: Option<String>,
2980    #[serde(default, skip_serializing_if = "Option::is_none")]
2981    pub auth_prefix: Option<String>,
2982    pub map: Vec<IRUpstreamMapRule>,
2983    #[serde(default, skip_serializing_if = "Option::is_none")]
2984    pub reconnect: Option<IRUpstreamReconnect>,
2985    #[serde(default, skip_serializing_if = "Option::is_none")]
2986    pub overflow: Option<String>,
2987    #[serde(default, skip_serializing_if = "Option::is_none")]
2988    pub backpressure_credit: Option<i64>,
2989    /// v2.37.0 — the `Preset@vN` reference this declaration was expanded from
2990    /// (provenance for the compliance reviewer); absent for hand-written ones.
2991    #[serde(default, skip_serializing_if = "Option::is_none")]
2992    pub preset: Option<String>,
2993}
2994
2995/// v2.37.0 — one compiled `map:` projection rule.
2996#[derive(Debug, Clone, Serialize)]
2997pub struct IRUpstreamMapRule {
2998    pub node_type: &'static str,
2999    pub direction: String,
3000    pub message: String,
3001    pub framing: String,
3002    #[serde(default, skip_serializing_if = "Option::is_none")]
3003    pub tag: Option<String>,
3004    #[serde(default, skip_serializing_if = "Option::is_none")]
3005    pub when_field: Option<String>,
3006    #[serde(default, skip_serializing_if = "Option::is_none")]
3007    pub when_value: Option<String>,
3008}
3009
3010/// v2.37.0 — compiled reconnect policy (all three fields required by the
3011/// parser — a reconnection policy with a hole is not a policy).
3012#[derive(Debug, Clone, Serialize)]
3013pub struct IRUpstreamReconnect {
3014    pub backoff_ms: i64,
3015    pub max_attempts: i64,
3016    pub on_exhausted: String,
3017}
3018
3019/// v2.38.0 — a named, referenced browser-origin policy. Mirrors
3020/// `IRShield`'s field-for-field shape; consumed by `IRAxonEndpoint.cors_ref`.
3021/// Wildcard+credentials (T853), origin-glob shape (T854), and closed-method
3022/// (T855) violations are all rejected before this node is ever lowered — the
3023/// checker re-derives the same closed catalogs at deploy time (v2.38.0,
3024/// `CorsPolicyConsistency`), so an IR that reaches the runtime is already
3025/// proven consistent.
3026#[derive(Debug, Clone, Serialize)]
3027pub struct IRCors {
3028    pub node_type: &'static str,
3029    pub source_line: u32,
3030    pub source_column: u32,
3031    pub name: String,
3032    pub allow_origins: Vec<String>,
3033    pub allow_methods: Vec<String>,
3034    pub allow_headers: Vec<String>,
3035    pub allow_credentials: bool,
3036    /// Duration literal (`"3600s"`) — same string-carries-the-unit
3037    /// convention as `axonendpoint.timeout`; the consumer (enterprise's
3038    /// dynamic CORS middleware) parses it into seconds at request time.
3039    #[serde(default, skip_serializing_if = "Option::is_none")]
3040    pub max_age: Option<String>,
3041    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3042    pub expose_headers: Vec<String>,
3043}
3044
3045/// v2.40.0 — compiled `cache` policy. The checker (v2.40.0) re-derives the
3046/// same laws at the deploy gate (`CacheSoundness`), so an IR that reaches the
3047/// runtime is already proven sound (one default max, non-pure ⇒ finite ttl,
3048/// references resolve). Every optional field is `skip_serializing_if` so a
3049/// bundle using `cache` only pays IR bytes for what it declares, and a bundle
3050/// with no `cache` never emits a `caches` key (IR-SHA stable, v2.33.0).
3051#[derive(Debug, Clone, Serialize)]
3052pub struct IRCache {
3053    pub node_type: &'static str,
3054    pub source_line: u32,
3055    pub source_column: u32,
3056    pub name: String,
3057    /// `"redis"` | `"in_process"`; empty ⇒ runtime default (`in_process`).
3058    #[serde(default, skip_serializing_if = "String::is_empty")]
3059    pub backend: String,
3060    /// Duration literal (`"10s"`) — same string-carries-the-unit convention as
3061    /// `cors.max_age`. `None` ⇒ cache-forever (sound only for a `pure` cache).
3062    #[serde(default, skip_serializing_if = "Option::is_none")]
3063    pub ttl: Option<String>,
3064    /// The parameter-name subset forming the key; empty ⇒ all bound params.
3065    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3066    pub key_params: Vec<String>,
3067    /// `true` ⇒ auto-covers every eligible tool (at most one per module).
3068    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
3069    pub default_policy: bool,
3070    /// Effect classes this cache memoises; empty ⇒ `["pure"]`.
3071    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3072    pub apply_to_effects: Vec<String>,
3073    /// Channel names whose `emit` flushes this cache's namespace.
3074    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3075    pub invalidate_on: Vec<String>,
3076}
3077
3078/// Compiled emit step — `c⟨v⟩.P` (Chan-Output / Chan-Mobility).
3079///
3080/// `value_is_channel = true` ⇒ resolved at lowering time as a channel
3081/// handle (second-order mobility, paper section 3.2); the runtime dispatches
3082/// on this flag without re-resolving symbols.
3083#[derive(Debug, Clone, Serialize)]
3084pub struct IREmit {
3085    pub node_type: &'static str,
3086    pub source_line: u32,
3087    pub source_column: u32,
3088    pub channel_ref: String,
3089    pub value_ref: String,
3090    pub value_is_channel: bool,
3091    /// v2.69.0 (owed) — the σ-shield the target `channel` declares
3092    /// (`channel C { … shield: S }`), RESOLVED here at lowering (Phase 0
3093    /// pre-pass, like `IRPublish.sign`) so the runtime `run_emit` scans the
3094    /// emitted value through S on EVERY dispatch path without re-deriving the
3095    /// channel↔shield map. Empty ⇒ an unshielded channel (byte-identical to a
3096    /// pre-v2.69.0 emit: `skip_serializing_if` elides it → zero IR-SHA drift for
3097    /// programs whose channels declare no shield).
3098    #[serde(default, skip_serializing_if = "String::is_empty")]
3099    pub shield_ref: String,
3100    /// v2.69.0 — the σ-shield's breach policy, resolved at lowering beside
3101    /// `shield_ref` (same Phase 0 pre-pass). `None` ⇒ no `on_breach:` declared
3102    /// (halt, the fail-closed default); elided → zero IR-SHA drift.
3103    #[serde(default, skip_serializing_if = "Option::is_none")]
3104    pub breach_policy: Option<IRBreachPolicy>,
3105    /// v2.89.0 — the σ-shield's declared `scan:` list, resolved at lowering
3106    /// beside `shield_ref` (same Phase 0 pre-pass). See
3107    /// [`IRShieldApplyStep::scan`] for the argument; the egress site carries it
3108    /// for the same reason the apply site does, because an `emit` through a
3109    /// scanning channel makes the same assertion about the value that leaves.
3110    /// Empty ⇒ unshielded, or shielded by a shield declaring no `scan:`.
3111    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3112    pub scan: Vec<String>,
3113}
3114
3115/// v2.46.0 — compiled `credential` contract. The TTL is carried as
3116/// SECONDS (converted at lowering from the duration literal) so every
3117/// consumer — the OSS minter port, the enterprise PASETO minter, the
3118/// deploy gate — shares one arithmetic-ready representation.
3119#[derive(Debug, Clone, Serialize)]
3120pub struct IRCredential {
3121    pub node_type: &'static str,
3122    pub source_line: u32,
3123    pub source_column: u32,
3124    pub name: String,
3125    /// The bearer lifetime in seconds (from the `ttl:` duration literal;
3126    /// `0` = unparseable, rejected by `axon-T894` before the IR ships).
3127    pub ttl_secs: u64,
3128    /// The capability slugs the minted bearer carries (validated dotted
3129    /// slugs; non-empty per `axon-T893`).
3130    pub grants: Vec<String>,
3131}
3132
3133/// v2.46.0 — compiled `mint <Credential> as <binding>` step. The runtime
3134/// resolves the contract, enforces the attenuation law
3135/// (`grants ⊆ capabilities(minter)`, fail-closed), mints via the
3136/// `CredentialMinter` port, and binds the raw bearer under `binding`.
3137#[derive(Debug, Clone, Serialize)]
3138pub struct IRMintStep {
3139    pub node_type: &'static str,
3140    pub source_line: u32,
3141    pub source_column: u32,
3142    pub credential_ref: String,
3143    pub binding: String,
3144}
3145
3146/// v2.48.0 — compiled `rotate <SecretsStore> [where "…"] with <Tool>
3147/// as <binding>` step. The runtime enumerates the custody entries of the
3148/// store's class matching `where_expr` (whole class when empty), performs
3149/// ONE mediated exchange per key through the named tool (reveal → tool
3150/// renews → CAS commit at version+1), and binds the METADATA-ONLY
3151/// summary. Fail-closed without a custody port; each per-key failure
3152/// degrades with a witness, never destructively.
3153#[derive(Debug, Clone, Serialize)]
3154pub struct IRRotateStep {
3155    pub node_type: &'static str,
3156    pub source_line: u32,
3157    pub source_column: u32,
3158    pub store_ref: String,
3159    /// v2.21.0 metadata filter; empty = the whole class (elided from the wire).
3160    #[serde(default, skip_serializing_if = "String::is_empty")]
3161    pub where_expr: String,
3162    pub tool_ref: String,
3163    pub binding: String,
3164}
3165
3166/// Compiled publish step — capability extrusion (Publish-Ext, paper section 4.3).
3167#[derive(Debug, Clone, Serialize)]
3168pub struct IRPublish {
3169    pub node_type: &'static str,
3170    pub source_line: u32,
3171    pub source_column: u32,
3172    pub channel_ref: String,
3173    pub shield_ref: String,
3174    /// v2.34.0 — the referenced shield's `sign:` algorithm, RESOLVED at
3175    /// lowering (order-independent pre-pass over every declared shield).
3176    /// Non-empty ⇒ this publish is an EGRESS declaration: the channel's
3177    /// events are signed-deliverable to registered external subscribers.
3178    /// Elided from JSON when empty (zero IR-SHA drift for pre-v2.34.0 programs).
3179    #[serde(skip_serializing_if = "String::is_empty")]
3180    pub sign: String,
3181}
3182
3183/// Compiled discover step — dual of publish.
3184#[derive(Debug, Clone, Serialize)]
3185pub struct IRDiscover {
3186    pub node_type: &'static str,
3187    pub source_line: u32,
3188    pub source_column: u32,
3189    pub capability_ref: String,
3190    pub alias: String,
3191}