Skip to main content

axon/
wire_envelope.rs

1//! v2.0.0 — Pure Silicon Cognition: the canonical wire payload type
2//! for axonendpoint responses on `transport: json`.
3//!
4//! Isomorphic to the ψ-vector `ψ = ⟨T, V, E⟩` (paper section 5):
5//!
6//! - **T** — the ontological type the value claims to inhabit
7//!   (`FlowEnvelope.ontological_type`)
8//! - **V** — the typed payload, member of T
9//!   (`FlowEnvelope.result`)
10//! - **E** — the epistemic envelope: certainty (Theorem 5.1) +
11//!   provenance + audit-chain + blame attribution
12//!   (the remaining fields)
13//!
14//! Defined ONCE in Rust; no Python mirror; no drift gate (per D3 +
15//! [[feedback_zero_py_files_north_star]]). This module is the
16//! Rust-canonical source of truth for the v2.0.0 wire shape.
17//!
18//! ## Construction
19//!
20//! The canonical builder is
21//! [`FlowEnvelope::from_execution_result`] — converts the v1.x
22//! [`crate::execution_result::ServerExecutionResult`] into a v2.0.0
23//! envelope. The conversion is total: every field of the legacy
24//! struct maps to a pillar-organized slot of the envelope, with no
25//! information loss. Epistemic fields (`certainty`,
26//! `provenance_chain`, `blame_attribution`) receive cycle-39.b safe
27//! defaults; their full producer logic lands in v2.0.0.
28//!
29//! ## Sealing
30//!
31//! [`FlowEnvelope::seal`] is the single egress point before HTTP
32//! serialization. In v2.0.0 it runs the Rust-side fallback for
33//! Theorem 5.1 enforcement (clamp `certainty ≤ 0.99` if derived) +
34//! computes the `audit_chain_hash` over the canonical provenance
35//! representation. In v2.0.0 this method delegates to the C23
36//! kernel `axon-csys::effects::envelope::validate_epistemic_degradation`,
37//! making the bound structurally unbypassable from any Rust caller.
38//!
39//! ## Pillars
40//!
41//! - **Pillar I (Epistemic)** — `ontological_type`, `result`,
42//!   `certainty` (Theorem 5.1 bounded)
43//! - **Pillar II (Audit-chained)** — `provenance_chain`,
44//!   `step_audit`, `audit_chain_hash`
45//! - **Pillar III (Streaming)** — N/A (SSE has its own event family
46//!   per D9; this envelope is JSON-transport-only)
47//! - **Pillar IV (Capability)** — `blame_attribution` (carries
48//!   `BlameKind` of failure when present)
49//!
50//! See plan vivo `the design plan` section 4 for
51//! the full wire-shape contract.
52
53use serde::{Deserialize, Serialize};
54use sha2::{Digest, Sha256};
55
56// ════════════════════════════════════════════════════════════════════
57// FlowEnvelope — the canonical wire payload for `transport: json`
58// ════════════════════════════════════════════════════════════════════
59
60/// v2.0.0 (D1, D2, D5) — the wire payload of every `transport: json`
61/// axonendpoint response (HTTP 2xx) and every legacy
62/// `POST /v1/execute` invocation.
63///
64/// Fields are organized by Pillar (see module docs). At wire
65/// emission, `result` carries a `serde_json::Value` (monomorphic at
66/// runtime); D5 validation (v1.23.0) — once simplified in 39.d —
67/// will type-check this slot against the declared inner T of the
68/// adopter's `output: FlowEnvelope<T>` declaration.
69#[derive(Serialize, Deserialize, Debug, Clone)]
70pub struct FlowEnvelope {
71    // ── Pillar I (Epistemic) — the ψ-vector slots ────────────────
72    /// The ontological type declared at the endpoint surface (the
73    /// inner T of `output: FlowEnvelope<T>`). Slug form:
74    /// `TenantRecord`, `List<PatientRecord>`, `Stream<Token>`.
75    /// For legacy `/v1/execute` invocations (no endpoint
76    /// declaration), this is the runtime-inferred type slug.
77    pub ontological_type: String,
78
79    /// The typed payload — member of `ontological_type`.
80    /// `serde_json::Value` at the wire layer because the runtime
81    /// is monomorphic; D5 (when simplified in 39.d) validates the
82    /// inner shape against the declared T.
83    pub result: serde_json::Value,
84
85    /// Certainty `c ∈ [0.0, 1.0]`, bounded by Theorem 5.1:
86    /// `c ≤ 0.99` whenever `derived_status = true`. In v2.0.0
87    /// the bound is enforced by [`FlowEnvelope::seal`]'s Rust
88    /// fallback; in v2.0.0 the bound moves to the C23 kernel
89    /// `axon-csys::effects::envelope::validate_epistemic_degradation`,
90    /// making it structurally unbypassable.
91    pub certainty: f64,
92
93    /// v2.7.0 — the Theorem 5.1 `(base, scope, confidence)` triple of
94    /// every flow-level `use <Tool>` dispatch whose tool declares an
95    /// `epistemic:<level>` effect. Surfaces the epistemic degradation on
96    /// the wire — the v2.4.0 parity gate, promoted (the competitive
97    /// differential: an adopter sees a query routed through an
98    /// `epistemic:speculate` tool decay to `confidence ≤ 0.80`). Empty —
99    /// and elided from the JSON — for flows with no epistemic tool (D5
100    /// backward-compat: byte-identical wire for every pre-55.b flow).
101    #[serde(default, skip_serializing_if = "Vec::is_empty")]
102    pub epistemic_envelopes: Vec<crate::epistemic_capture::EpistemicEnvelope>,
103
104    // ── Pillar II (Audit-chained) — provenance + step trail ──────
105    /// Ordered list of `kind:identifier` tuples capturing the
106    /// lineage of `result`. Examples:
107    ///   - `["flow:FetchTenants", "retrieve:tenants", "backend:stub"]`
108    ///   - `["step:Triage", "shield:Hipaa", "backend:anthropic"]`
109    /// Empty for endpoints with no derived state (singular literal
110    /// returns); populated by [`FlowEnvelope::from_execution_result`].
111    pub provenance_chain: Vec<String>,
112
113    /// Per-step audit trail. Survives from v1.x as the canonical
114    /// observability surface; here it is structured (not just
115    /// `Vec<String>`). Step results are TYPED `Value` post-39.b
116    /// (pre-v2.0.0 they were stringified — the typed form is a D5
117    /// simplification dividend).
118    pub step_audit: StepAuditTrail,
119
120    /// HMAC-SHA256 hex of the canonical form of `provenance_chain
121    /// || step_audit`. Computed by [`FlowEnvelope::seal`]; in
122    /// v2.0.0 the hash moves to the C23 kernel for byte-
123    /// deterministic cross-deployment verification.
124    pub audit_chain_hash: String,
125
126    // ── Pillar IV (Capability) — blame attribution ───────────────
127    /// Populated only when the flow's success path produced a
128    /// degraded posture (anchor breach, shield rejection, backend
129    /// soft-fail, store breach, type-mismatch on recoverable path).
130    /// `None` on the clean happy path.
131    pub blame_attribution: Option<BlameContext>,
132
133    // ── Cross-cutting — observability + correlation ──────────────
134    /// Execution metrics — latency, tokens, backend identity.
135    /// Always populated.
136    pub execution_metrics: ExecutionMetrics,
137
138    /// Correlation anchor (matches `X-Axon-Trace-Id` header).
139    /// String form for cross-stack compat (the v1.x `trace_id: u64`
140    /// is reborn here as Uuid v4 hex string).
141    pub trace_id: String,
142
143    /// v2.15.0 — the HONEST hard-failure detail when the flow aborted on
144    /// a node's `DispatchError` (a failing `persist`/`mutate`/`purge` store
145    /// write, a backend error, etc.). `Some("flow 'F' failed at persist into
146    /// 'S': <cause>")` names the failing node + the underlying cause. This is
147    /// distinct from `blame_attribution`, which is reserved for SOFT
148    /// degradation reported ON the success path — a hard fail needs its own
149    /// slot. Mirrors the streaming dispatcher's `FlowError.error` so a
150    /// non-streaming endpoint surfaces store-write failures with the SAME
151    /// honesty the SSE path always had (closing the v2.15.0 silent-abort
152    /// regression). `None` — and elided from the JSON — on the clean path, so
153    /// every pre-v2.15.0 happy-path wire stays byte-identical.
154    #[serde(default, skip_serializing_if = "Option::is_none")]
155    pub error: Option<String>,
156
157    /// v2.46.0 — the run's temporal record when any step rendered a
158    /// declared `now:` (`time_is_an_explicit_input` applied to cognition):
159    /// the single captured instant (RFC 3339 UTC), the tz-database release
160    /// it resolved against, and the declared zones actually rendered — the
161    /// triple that makes the exact prompt the model saw reconstructible
162    /// byte-for-byte. `None` — and elided from the JSON — for every
163    /// `now:`-less flow, so every pre-v2.46.0 wire stays byte-identical.
164    #[serde(default, skip_serializing_if = "Option::is_none")]
165    pub temporal_context: Option<crate::temporal_context::TemporalRecord>,
166}
167
168/// v2.0.0 (D5) — per-step audit surface. Structured replacement
169/// for the v1.x `Vec<String>` step results.
170#[derive(Serialize, Deserialize, Debug, Clone, Default)]
171pub struct StepAuditTrail {
172    pub step_names: Vec<String>,
173    /// v2.0.0 — TYPED. The v1.x stringified results are parsed
174    /// as JSON values when constructible; opaque strings fall back
175    /// to `Value::String(...)`. The D5 simplification in 39.d
176    /// leverages this typed form.
177    pub step_results: Vec<serde_json::Value>,
178    pub anchor_checks: usize,
179    pub anchor_breaches: usize,
180    pub errors: usize,
181    pub steps_executed: usize,
182    /// v1.24.0 carry-over — per-step EnforcementSummary entries
183    /// (from `StreamPolicyEnforcer` runs). Empty in the legacy sync
184    /// path; populated by `server_execute_streaming` per the D2
185    /// contract.
186    #[serde(default, skip_serializing_if = "Vec::is_empty")]
187    pub enforcement_summaries:
188        Vec<(String, crate::execution_result::EnforcementSummaryWire)>,
189    /// v1.24.0 carry-over — per-step `<stream:<policy>>` slugs
190    /// declared in source. Empty when no step declares one.
191    #[serde(default, skip_serializing_if = "Vec::is_empty")]
192    pub effect_policies: Vec<(String, String)>,
193    /// v1.24.0 carry-over — closed-catalog runtime warnings
194    /// (only populated on legacy-path fallback under axon-W002 —
195    /// structurally unreachable post-33.z but the slot survives
196    /// for forward-compat with future warnings).
197    #[serde(default, skip_serializing_if = "Vec::is_empty")]
198    pub runtime_warnings: Vec<crate::runtime_warnings::RuntimeWarning>,
199}
200
201/// v2.0.0 (D5) — execution metrics + provenance identity. Always
202/// populated.
203#[derive(Serialize, Deserialize, Debug, Clone, Default)]
204pub struct ExecutionMetrics {
205    pub latency_ms: u64,
206    pub tokens_input: u64,
207    pub tokens_output: u64,
208    pub backend: String,
209    pub flow_name: String,
210    pub source_file: String,
211}
212
213// ════════════════════════════════════════════════════════════════════
214// BlameContext — Pillar IV attribution surface
215// ════════════════════════════════════════════════════════════════════
216
217/// v2.0.0 (D11) — closed-catalog blame attribution. Surfaces
218/// WHICH layer produced the degraded posture on a 2xx response.
219/// Hard-fails (4xx/5xx) are handled by the existing error envelopes
220/// (not this struct) — `BlameContext` is for SOFT degradation
221/// reported on the success path.
222#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
223pub struct BlameContext {
224    pub kind: BlameKind,
225    /// v2.83.0 — WHO is responsible, orthogonal to [`BlameKind`]'s WHAT
226    /// degraded. `None` when this degradation does not determine a party.
227    ///
228    /// Elided from the wire when absent (`skip_serializing_if`), so every
229    /// pre-v2.83.0 envelope serialises byte-identically and v2.0.0's D11 wire
230    /// contract is untouched.
231    #[serde(default, skip_serializing_if = "Option::is_none")]
232    pub party: Option<BlameParty>,
233    /// `file:line:col` (compile-time origin) OR `step:name`
234    /// (runtime origin). Empty string when the origin cannot be
235    /// pinpointed.
236    pub location: String,
237    /// Human-readable diagnostic. Forms the audit_log entry's
238    /// primary message.
239    pub message: String,
240    /// Optional anchor back to a plan-vivo D-letter (e.g. "39.c",
241    /// "33.x.d") for forward correlation when the blame ties to a
242    /// specific architectural commitment.
243    pub d_letter: Option<String>,
244}
245
246/// v2.83.0 — the RESPONSIBILITY axis of the Findler-Felleisen blame
247/// calculus: **who** is answerable for a degradation, as opposed to
248/// [`BlameKind`]'s **what** degraded.
249///
250/// # Why these names and not the paper's
251///
252/// `paper_agent.md` (Eje 2) writes this axis as
253/// `{Orchestrator, SubAgent, Environment}`. The runtime already had it, under
254/// different names: [`crate::emcp::Blame`] `{Caller, Server, Network}`, whose own
255/// doc cites *"the contract-based blame calculus from ℰMCP spec (CT-2/CT-3)"* —
256/// the same Findler-Felleisen calculus, specified twice.
257///
258/// The code's vocabulary wins because it is **already on a wire an adopter
259/// reads**: a failed ℰMCP call surfaces literally as `"… [blame=server]"`, and
260/// `Blame` derives `Serialize`. Renaming it to match a paper would break a live
261/// contract to buy nothing. `Caller`/`Server` are also the more general pair —
262/// many blame sites are tools or stores, and calling those a "SubAgent"
263/// presupposes an agent that is not there.
264///
265/// `Network` generalises to `Environment` because the paper's environmental
266/// blame covers timeouts, FFI boundary breaks and memory corruption, not only
267/// the network. `emcp::Blame::Network` maps onto it and keeps its own
268/// `as_str()` — see `crate::emcp::Blame::party`.
269///
270/// # Why there is no `None` variant
271///
272/// `emcp::Blame::None` means *"the call succeeded"*. A [`BlameContext`] only
273/// exists because something degraded, so that state is unrepresentable here by
274/// construction; "no party could be determined" is `Option::None` on the field,
275/// which is a different fact and must not share a spelling with it.
276#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
277#[serde(rename_all = "snake_case")]
278pub enum BlameParty {
279    /// Positive blame — the CALLER violated a precondition before invoking:
280    /// arguments outside their domain, an insufficient cognitive budget.
281    Caller,
282    /// Negative blame — the INVOKED party (sub-agent, tool, backend) violated a
283    /// postcondition: a non-conforming return type, a confidence above the
284    /// Theorem 5.1 ceiling on derived knowledge, or an anchor breach.
285    Server,
286    /// Environmental blame — not attributable to the logic of any software
287    /// component: timeouts, FFI boundary breaks, memory corruption, transport
288    /// collapse.
289    Environment,
290}
291
292/// v2.0.0 (D11) — closed catalog of blame kinds. Adding a variant
293/// is a non-breaking surface change (consumers MUST handle
294/// `#[non_exhaustive]`-style fall-through).
295#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
296#[serde(rename_all = "snake_case")]
297pub enum BlameKind {
298    /// Pillar IV — an anchor's `require:` predicate failed; flow
299    /// chose to proceed (degraded path).
300    AnchorBreach,
301    /// Pillar I — a shield scanner flagged content; flow chose to
302    /// proceed.
303    ShieldRejection,
304    /// Backend returned a degraded response (truncated, partial,
305    /// soft-rate-limited).
306    BackendSoftFail,
307    /// Pillar II — store mutation chain verification failed; flow
308    /// proceeded with the prior-state read.
309    StoreBreach,
310    /// D5 detected partial typing inconsistency that is recoverable
311    /// (e.g. missing optional field with a sane default).
312    TypeMismatch,
313}
314
315// ════════════════════════════════════════════════════════════════════
316// FlowEnvelope::from_execution_result — v1.x → v2.0.0 converter
317// ════════════════════════════════════════════════════════════════════
318
319impl FlowEnvelope {
320    /// v2.0.0 — convert a v1.x [`crate::execution_result::ServerExecutionResult`]
321    /// into a v2.0.0 envelope. Total: every legacy field maps to a
322    /// pillar-organized slot; no information loss.
323    ///
324    /// Epistemic field defaults applied here (refined in 39.c):
325    /// - `certainty = 1.0` when `anchor_breaches == 0` and
326    ///   `errors == 0` (clean happy path; no derived posture).
327    /// - `certainty = 0.99` when `anchor_breaches > 0 ||
328    ///   errors > 0` (Theorem 5.1: derived states bounded ≤ 0.99).
329    /// - `provenance_chain` built from
330    ///   `flow_name + step_names + backend`.
331    /// - `blame_attribution = None` always at this layer (the soft-
332    ///   degradation surface is populated by the runtime when it
333    ///   detects anchor/shield/store/backend events — 39.c lands
334    ///   that wiring).
335    ///
336    /// The `result` slot is populated from the LAST step's typed
337    /// output (`step_results.last()` parsed as `Value`). For flows
338    /// with no steps (degenerate) the result is `Value::Null`.
339    ///
340    /// `trace_id` is converted from the legacy `u64` to a Uuid v4
341    /// hex string. When the legacy id is 0 (pre-record), a fresh
342    /// Uuid is minted.
343    pub fn from_execution_result(
344        exec_result: crate::execution_result::ServerExecutionResult,
345        ontological_type: String,
346    ) -> Self {
347        // ── Pillar II — provenance chain ──
348        // v2.0.0 — interleave semantic provenance events
349        // (`retrieve:*`, `shield:*`, etc.) with the canonical
350        // step/backend entries. Order: `flow:F`, then taxonomy
351        // events from execution_units walk, then `step:S` entries
352        // for each canonical step, then `backend:B` last. This
353        // gives auditors a complete lineage from flow declaration
354        // through every observable runtime event.
355        let mut provenance_chain = Vec::with_capacity(
356            2 + exec_result.step_names.len() + exec_result.provenance_events.len(),
357        );
358        provenance_chain.push(format!("flow:{}", exec_result.flow_name));
359        for event in &exec_result.provenance_events {
360            provenance_chain.push(event.clone());
361        }
362        for step_name in &exec_result.step_names {
363            provenance_chain.push(format!("step:{}", step_name));
364        }
365        provenance_chain.push(format!("backend:{}", exec_result.backend));
366
367        // ── Pillar II — typed step_results ──
368        // Parse each stringified result as JSON if possible; fall
369        // back to a String Value preserving the raw text.
370        let step_results_typed: Vec<serde_json::Value> = exec_result
371            .step_results
372            .iter()
373            .map(|s| {
374                serde_json::from_str::<serde_json::Value>(s)
375                    .unwrap_or_else(|_| serde_json::Value::String(s.to_string()))
376            })
377            .collect();
378
379        // ── Pillar I — the `result` slot ──
380        // Canonically the last step's typed value is the flow output.
381        // When the flow has no steps (degenerate), result is Null.
382        let result = step_results_typed
383            .last()
384            .cloned()
385            .unwrap_or(serde_json::Value::Null);
386
387        // ── Pillar I — certainty (Theorem 5.1 Rust-side fallback) ──
388        // The C23 kernel in 39.c will replace this; here we apply
389        // the same algebra so wire bytes are stable across the
390        // 39.b → 39.c transition.
391        let derived =
392            exec_result.anchor_breaches > 0 || exec_result.errors > 0;
393        let certainty = if derived { 0.99 } else { 1.0 };
394
395        // ── Pillar IV — blame ──
396        // v2.0.0 — propagate the blame attribution from the
397        // runtime walk (populated by `derive_blame_from_report` in
398        // `wire_envelope_producers`). `None` on clean happy path;
399        // populated when the runtime surfaced an anchor breach,
400        // shield rejection, store breach, backend soft-fail, or
401        // recoverable type mismatch. The first-emitted (highest-
402        // priority) blame wins per `merge_blame`.
403        let blame_attribution: Option<BlameContext> =
404            exec_result.blame_attribution;
405
406        // ── Cross-cutting — trace_id ──
407        let trace_id = if exec_result.trace_id == 0 {
408            uuid::Uuid::new_v4().to_string()
409        } else {
410            // Pre-39 the trace_id was a u64; here we render it as
411            // a 16-char hex string (preserve the value semantically;
412            // future code paths will mint Uuids directly).
413            format!("{:016x}", exec_result.trace_id)
414        };
415
416        Self {
417            ontological_type,
418            result,
419            certainty,
420            // v2.7.0 — surface the IR-derived epistemic envelopes.
421            epistemic_envelopes: exec_result.epistemic_envelopes,
422            provenance_chain,
423            step_audit: StepAuditTrail {
424                step_names: exec_result.step_names.clone(),
425                step_results: step_results_typed,
426                anchor_checks: exec_result.anchor_checks,
427                anchor_breaches: exec_result.anchor_breaches,
428                errors: exec_result.errors,
429                steps_executed: exec_result.steps_executed,
430                enforcement_summaries: exec_result.enforcement_summaries,
431                effect_policies: exec_result.effect_policies,
432                runtime_warnings: exec_result.runtime_warnings,
433            },
434            audit_chain_hash: String::new(), // computed by seal()
435            blame_attribution,
436            execution_metrics: ExecutionMetrics {
437                latency_ms: exec_result.latency_ms,
438                tokens_input: exec_result.tokens_input,
439                tokens_output: exec_result.tokens_output,
440                backend: exec_result.backend,
441                flow_name: exec_result.flow_name,
442                source_file: exec_result.source_file,
443            },
444            trace_id,
445            // v2.15.0 — the honest hard-failure detail (named node + cause),
446            // verbatim from the runtime walk. `None` on the clean path.
447            error: exec_result.error,
448            // v2.46.0 — the temporal record, verbatim from the runtime walk.
449            temporal_context: exec_result.temporal_context,
450        }
451    }
452}
453
454// ════════════════════════════════════════════════════════════════════
455// FlowEnvelope::seal — single egress before HTTP serialization
456// ════════════════════════════════════════════════════════════════════
457
458impl FlowEnvelope {
459    /// v2.0.0 — apply epistemic enforcement + compute the
460    /// `audit_chain_hash` before wire serialization. This is the
461    /// ONLY public sealing surface; the wire bytes emitted by
462    /// `axon_server` MUST pass through this method (the `seal()`
463    /// invariant — v2.0.0 establishes it; 39.h grep gate locks
464    /// it structurally).
465    ///
466    /// ## v2.0.0 implementation (C23 kernel canonical)
467    ///
468    /// 1. Theorem 5.1 enforcement DELEGATES to the C23 kernel
469    ///    `axon-csys::envelope::validate_degradation`. The kernel:
470    ///    a. Defensively normalises NaN / Inf / out-of-range
471    ///       certainty into `[0.0, 1.0]`.
472    ///    b. Clamps `certainty ≤ 0.99` when `derived_status = true`.
473    ///    c. Returns the envelope with `derived_status` +
474    ///       `epistemic_kind` passed through unchanged.
475    ///    The C23 kernel is the SINGLE point of structural truth —
476    ///    no Rust path bypasses it for production code paths.
477    /// 2. `derived_status` algebra (Rust-side, matches the producer
478    ///    in [`FlowEnvelope::from_execution_result`] verbatim):
479    ///    `derived = step_audit.anchor_breaches > 0
480    ///             || step_audit.errors > 0`. The Rust side decides
481    ///    WHO is derived (semantic); the C23 kernel enforces WHAT
482    ///    the ceiling looks like (structural).
483    /// 3. `audit_chain_hash` = SHA-256 hex of the canonical-JSON
484    ///    serialization of `[provenance_chain, step_audit]`.
485    ///    Deterministic on identical inputs; tamper-evident.
486    ///    (39.c.x leaves the SHA-256 in Rust pending a future
487    /// step that moves it to axon-csys::crypto for true
488    ///    silicon-grounded tamper-evidence.)
489    pub fn seal(mut self) -> Self {
490        // v2.7.0 — epistemic ceiling propagates to the HEADLINE
491        // certainty: a flow can be no more certain than its least-certain
492        // epistemic tool. The λD lattice meet (⊓) of the per-tool ceilings
493        // is their minimum; each envelope's `confidence` IS that tool's
494        // applied ceiling (v2.7.0), so `min(confidences)` is the flow's
495        // epistemic upper bound. Applied BEFORE the C23 kernel so the kernel
496        // consolidates the full degradation (Theorem 5.1) at the single
497        // egress — no gateway can read a nominal `know` headline while an
498        // internal tool degraded the computation to `speculate`
499        // (Epistemic Transparency / taint propagation). No epistemic tool ⇒
500        // no change (D5 wire byte-compat for every pre-55 flow).
501        if let Some(min_ceiling) = self
502            .epistemic_envelopes
503            .iter()
504            .map(|e| e.confidence)
505            .reduce(f64::min)
506        {
507            self.certainty = self.certainty.min(min_ceiling);
508        }
509        // Theorem 5.1 — DELEGATE to C23 kernel. The algebra for
510        // `derived_status` matches the producer in
511        // `from_execution_result` (anchor_breaches > 0 || errors > 0)
512        // so producer + sealer agree on WHO is derived.
513        let derived = self.step_audit.anchor_breaches > 0
514            || self.step_audit.errors > 0;
515        let epistemic_kind = if !derived {
516            axon_csys::EpistemicKind::Clean
517        } else if self.blame_attribution.is_some() {
518            // Multi-source degradation — anchor/shield/store/backend
519            // surfaced an explicit blame producer (Pillar IV).
520            axon_csys::EpistemicKind::Degraded
521        } else if self.step_audit.anchor_breaches > 0 {
522            axon_csys::EpistemicKind::Breached
523        } else {
524            axon_csys::EpistemicKind::Derived
525        };
526        let env = axon_csys::EpistemicEnvelope::new(
527            self.certainty,
528            derived,
529            epistemic_kind,
530        );
531        let clamped = axon_csys::validate_degradation(env);
532        self.certainty = clamped.certainty;
533        // Audit chain hash — SHA-256 over canonical JSON of
534        // [provenance_chain, step_audit]. We use serde_json for
535        // canonicalization (sorted keys on structs by design; we
536        // accept the array-of-(provenance, audit) tuple as the
537        // canonical input).
538        let canonical = serde_json::to_string(&(
539            &self.provenance_chain,
540            &self.step_audit,
541        ))
542        .unwrap_or_default();
543        let mut hasher = Sha256::new();
544        hasher.update(canonical.as_bytes());
545        let digest = hasher.finalize();
546        self.audit_chain_hash = format!("{digest:x}");
547        self
548    }
549}
550
551// ════════════════════════════════════════════════════════════════════
552// Helpers
553// ════════════════════════════════════════════════════════════════════
554
555/// v2.0.0 — derive the `ontological_type` slug for an endpoint's
556/// declared `output: T`. When the endpoint declares
557/// `output: FlowEnvelope<T>` (the canonical form post-39.e), this
558/// extracts the inner T. For legacy declarations (pre-39.e — still
559/// in tree until atomic deploy), returns the declared type verbatim.
560/// For empty / missing declarations returns `"Any"` (the singular
561/// catch-all).
562pub fn extract_inner_ontological_type(declared: &str) -> String {
563    let t = declared.trim();
564    if t.is_empty() {
565        return "Any".to_string();
566    }
567    if let Some(rest) = t.strip_prefix("FlowEnvelope<") {
568        if let Some(inner) = rest.strip_suffix('>') {
569            return inner.trim().to_string();
570        }
571    }
572    t.to_string()
573}
574
575// ════════════════════════════════════════════════════════════════════
576// Tests
577// ════════════════════════════════════════════════════════════════════
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582    use crate::execution_result::ServerExecutionResult;
583
584    fn fixture_exec_result() -> ServerExecutionResult {
585        ServerExecutionResult {
586            success: true,
587            flow_name: "FetchTenants".to_string(),
588            source_file: "tenants.axon".to_string(),
589            backend: "stub".to_string(),
590            type_errors: Vec::new(),
591            steps_executed: 1,
592            latency_ms: 142,
593            tokens_input: 0,
594            tokens_output: 0,
595            anchor_checks: 0,
596            anchor_breaches: 0,
597            errors: 0,
598            step_names: vec!["RetrieveAll".to_string()],
599            step_results: vec![
600                r#"[{"id":1,"name":"foo"},{"id":2,"name":"bar"}]"#.to_string(),
601            ],
602            trace_id: 0,
603            effect_policies: Vec::new(),
604            enforcement_summaries: Vec::new(),
605            runtime_warnings: Vec::new(),
606            provenance_events: Vec::new(),
607            blame_attribution: None,
608            epistemic_envelopes: Vec::new(),
609            error: None,
610            temporal_context: None,
611        }
612    }
613
614    #[test]
615    fn from_execution_result_clean_happy_path() {
616        let exec = fixture_exec_result();
617        let env = FlowEnvelope::from_execution_result(
618            exec,
619            "List<TenantRecord>".to_string(),
620        );
621        assert_eq!(env.ontological_type, "List<TenantRecord>");
622        assert_eq!(env.certainty, 1.0, "clean path → certainty 1.0");
623        assert_eq!(env.execution_metrics.flow_name, "FetchTenants");
624        assert_eq!(env.execution_metrics.latency_ms, 142);
625        assert!(env.blame_attribution.is_none());
626        assert_eq!(
627            env.provenance_chain,
628            vec![
629                "flow:FetchTenants",
630                "step:RetrieveAll",
631                "backend:stub"
632            ]
633        );
634    }
635
636    #[test]
637    fn typed_result_slot_from_last_step() {
638        let exec = fixture_exec_result();
639        let env = FlowEnvelope::from_execution_result(
640            exec,
641            "List<TenantRecord>".to_string(),
642        );
643        // result is the LAST step's JSON-parsed value
644        let arr = env.result.as_array().expect("result must be array");
645        assert_eq!(arr.len(), 2);
646        assert_eq!(arr[0]["id"], 1);
647        assert_eq!(arr[0]["name"], "foo");
648        assert_eq!(arr[1]["id"], 2);
649        assert_eq!(arr[1]["name"], "bar");
650    }
651
652    #[test]
653    fn typed_step_results_parsed_when_json() {
654        let exec = fixture_exec_result();
655        let env = FlowEnvelope::from_execution_result(
656            exec,
657            "List<TenantRecord>".to_string(),
658        );
659        assert_eq!(env.step_audit.step_results.len(), 1);
660        assert!(env.step_audit.step_results[0].is_array());
661    }
662
663    #[test]
664    fn opaque_step_result_falls_back_to_string_value() {
665        let mut exec = fixture_exec_result();
666        exec.step_results = vec!["(stub model response)".to_string()];
667        let env = FlowEnvelope::from_execution_result(exec, "String".to_string());
668        // Opaque non-JSON text → Value::String wrapping the raw text
669        assert_eq!(env.step_audit.step_results.len(), 1);
670        assert_eq!(
671            env.step_audit.step_results[0],
672            serde_json::Value::String("(stub model response)".to_string())
673        );
674    }
675
676    #[test]
677    fn certainty_bounded_on_derived_state() {
678        let mut exec = fixture_exec_result();
679        exec.anchor_breaches = 1;
680        let env = FlowEnvelope::from_execution_result(exec, "Any".to_string());
681        assert_eq!(env.certainty, 0.99, "derived state → 0.99 per Theorem 5.1");
682    }
683
684    #[test]
685    fn certainty_bounded_on_errors() {
686        let mut exec = fixture_exec_result();
687        exec.errors = 1;
688        let env = FlowEnvelope::from_execution_result(exec, "Any".to_string());
689        assert_eq!(env.certainty, 0.99, "errors → derived → 0.99");
690    }
691
692    #[test]
693    fn seal_populates_audit_chain_hash() {
694        let env = FlowEnvelope::from_execution_result(
695            fixture_exec_result(),
696            "List<TenantRecord>".to_string(),
697        );
698        assert_eq!(env.audit_chain_hash, "", "pre-seal: empty");
699        let sealed = env.seal();
700        assert_eq!(
701            sealed.audit_chain_hash.len(),
702            64,
703            "post-seal: SHA-256 hex digest (64 chars)"
704        );
705        assert!(
706            sealed.audit_chain_hash.chars().all(|c| c.is_ascii_hexdigit()),
707            "post-seal: lowercase hex"
708        );
709    }
710
711    #[test]
712    fn seal_is_deterministic_on_identical_inputs() {
713        let a = FlowEnvelope::from_execution_result(
714            fixture_exec_result(),
715            "List<TenantRecord>".to_string(),
716        )
717        .seal();
718        let b = FlowEnvelope::from_execution_result(
719            fixture_exec_result(),
720            "List<TenantRecord>".to_string(),
721        )
722        .seal();
723        assert_eq!(
724            a.audit_chain_hash, b.audit_chain_hash,
725            "seal must be deterministic"
726        );
727    }
728
729    #[test]
730    fn seal_changes_hash_on_provenance_drift() {
731        let a = FlowEnvelope::from_execution_result(
732            fixture_exec_result(),
733            "List<TenantRecord>".to_string(),
734        )
735        .seal();
736        let mut exec_b = fixture_exec_result();
737        exec_b.step_names = vec!["RetrieveAllRenamed".to_string()];
738        let b = FlowEnvelope::from_execution_result(
739            exec_b,
740            "List<TenantRecord>".to_string(),
741        )
742        .seal();
743        assert_ne!(
744            a.audit_chain_hash, b.audit_chain_hash,
745            "tamper detection: provenance drift changes the hash"
746        );
747    }
748
749    #[test]
750    fn seal_clamps_certainty_on_derived() {
751        // Theorem 5.1 enforcement — even if a producer set certainty
752        // > 0.99 on a derived state, seal() clamps it.
753        // The 39.b algebra: derived ⇔ anchor_breaches > 0 || errors > 0
754        // (matches from_execution_result verbatim).
755        let mut env = FlowEnvelope {
756            ontological_type: "Any".to_string(),
757            result: serde_json::Value::Null,
758            certainty: 1.0, // misbehaving producer
759            epistemic_envelopes: Vec::new(),
760            provenance_chain: vec!["flow:Derived".to_string()],
761            step_audit: StepAuditTrail {
762                anchor_breaches: 1, // makes this derived per 39.b algebra
763                ..StepAuditTrail::default()
764            },
765            audit_chain_hash: String::new(),
766            blame_attribution: None,
767            execution_metrics: ExecutionMetrics::default(),
768            trace_id: "x".to_string(),
769            error: None,
770            temporal_context: None,
771        };
772        env.certainty = 1.0;
773        let sealed = env.seal();
774        assert!(
775            sealed.certainty <= 0.99,
776            "Theorem 5.1: certainty must be clamped to ≤ 0.99 on \
777             derived states (anchor_breaches > 0). Got: {}",
778            sealed.certainty
779        );
780    }
781
782    #[test]
783    fn seal_preserves_certainty_on_clean_path() {
784        // Theorem 5.1 — only derived states are clamped. A flow
785        // with no derivation (just the flow:_ provenance prefix and
786        // nothing else) keeps certainty = 1.0.
787        let mut exec = fixture_exec_result();
788        exec.step_names = Vec::new(); // strip the step to remove derivation
789        let env = FlowEnvelope::from_execution_result(exec, "Any".to_string());
790        // After from_execution_result the provenance chain has only
791        // ["flow:FetchTenants", "backend:stub"]. That's 2 entries
792        // (> 1), so this counts as derived per our algebra.
793        // To get a NON-derived state we'd need a flow with NO
794        // backend either — i.e. a degenerate flow. For the test we
795        // assert the algebra by directly constructing.
796        let degenerate = FlowEnvelope {
797            ontological_type: "Any".to_string(),
798            result: serde_json::Value::Null,
799            certainty: 1.0,
800            epistemic_envelopes: Vec::new(),
801            provenance_chain: vec!["flow:Empty".to_string()],
802            step_audit: StepAuditTrail::default(),
803            audit_chain_hash: String::new(),
804            blame_attribution: None,
805            execution_metrics: ExecutionMetrics::default(),
806            trace_id: "x".to_string(),
807            error: None,
808            temporal_context: None,
809        };
810        let sealed = degenerate.seal();
811        assert_eq!(sealed.certainty, 1.0);
812        let _ = env;
813    }
814
815    #[test]
816    fn extract_inner_ontological_type_unwraps_envelope() {
817        assert_eq!(
818            extract_inner_ontological_type("FlowEnvelope<List<TenantRecord>>"),
819            "List<TenantRecord>"
820        );
821        assert_eq!(
822            extract_inner_ontological_type("FlowEnvelope<TenantRecord>"),
823            "TenantRecord"
824        );
825        // Legacy: bare type — returned verbatim (pre-39.e tolerance).
826        assert_eq!(extract_inner_ontological_type("TenantRecord"), "TenantRecord");
827        assert_eq!(extract_inner_ontological_type("List<X>"), "List<X>");
828        // Missing / empty — defaults to Any (singular catch-all).
829        assert_eq!(extract_inner_ontological_type(""), "Any");
830        assert_eq!(extract_inner_ontological_type("   "), "Any");
831    }
832
833    #[test]
834    fn serialization_round_trip() {
835        let env = FlowEnvelope::from_execution_result(
836            fixture_exec_result(),
837            "List<TenantRecord>".to_string(),
838        )
839        .seal();
840        let serialized = serde_json::to_string(&env).expect("serialize");
841        let parsed: FlowEnvelope =
842            serde_json::from_str(&serialized).expect("deserialize");
843        assert_eq!(parsed.ontological_type, env.ontological_type);
844        assert_eq!(parsed.certainty, env.certainty);
845        assert_eq!(parsed.audit_chain_hash, env.audit_chain_hash);
846        assert_eq!(parsed.trace_id, env.trace_id);
847        assert_eq!(parsed.provenance_chain, env.provenance_chain);
848    }
849
850    #[test]
851    fn wire_shape_has_canonical_field_order() {
852        // v2.0.0 section 4 — the wire is the ψ-vector. Verify the
853        // serialized form carries every field the contract names.
854        let env = FlowEnvelope::from_execution_result(
855            fixture_exec_result(),
856            "List<TenantRecord>".to_string(),
857        )
858        .seal();
859        let json = serde_json::to_value(&env).expect("to_value");
860        let obj = json.as_object().expect("envelope is a JSON object");
861        // ψ = ⟨T, V, E⟩ — every component MUST be present.
862        assert!(obj.contains_key("ontological_type"), "T component");
863        assert!(obj.contains_key("result"), "V component");
864        assert!(obj.contains_key("certainty"), "E: epistemic");
865        assert!(obj.contains_key("provenance_chain"), "E: audit");
866        assert!(obj.contains_key("step_audit"), "E: audit detail");
867        assert!(obj.contains_key("audit_chain_hash"), "E: tamper-evidence");
868        assert!(obj.contains_key("blame_attribution"), "E: blame");
869        assert!(obj.contains_key("execution_metrics"), "observability");
870        assert!(obj.contains_key("trace_id"), "correlation");
871    }
872
873    #[test]
874    fn blame_kind_serializes_snake_case() {
875        // Wire-shape contract: BlameKind serializes as snake_case.
876        let blame = BlameContext {
877            kind: BlameKind::AnchorBreach,
878            party: None,
879            location: "step:Triage".to_string(),
880            message: "Confidence below threshold".to_string(),
881            d_letter: Some("39.c".to_string()),
882        };
883        let json = serde_json::to_value(&blame).expect("to_value");
884        assert_eq!(json["kind"], "anchor_breach");
885
886        let blame2 = BlameContext {
887            kind: BlameKind::BackendSoftFail,
888            party: None,
889            location: String::new(),
890            message: "Truncated".to_string(),
891            d_letter: None,
892        };
893        let json2 = serde_json::to_value(&blame2).expect("to_value");
894        assert_eq!(json2["kind"], "backend_soft_fail");
895    }
896
897    #[test]
898    fn trace_id_minted_when_legacy_is_zero() {
899        let exec = fixture_exec_result(); // trace_id = 0
900        let env = FlowEnvelope::from_execution_result(exec, "Any".to_string());
901        // Uuid v4 is 36 chars with dashes; legacy hex (16) is 16.
902        assert!(
903            env.trace_id.len() == 36 || env.trace_id.len() == 16,
904            "trace_id length must be Uuid (36) or legacy hex (16). \
905             Got len={}: {}",
906            env.trace_id.len(),
907            env.trace_id
908        );
909        assert_ne!(env.trace_id, "0");
910    }
911
912    #[test]
913    fn trace_id_carries_legacy_value_when_nonzero() {
914        let mut exec = fixture_exec_result();
915        exec.trace_id = 0xDEADBEEF;
916        let env = FlowEnvelope::from_execution_result(exec, "Any".to_string());
917        assert_eq!(env.trace_id, "00000000deadbeef");
918    }
919}