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