Skip to main content

knowledge_runtime/
inference.rs

1use crate::error::RuntimeError;
2use crate::ids::Scope;
3use crate::runtime::KnowledgeRuntime;
4use constraint_compiler::{compile_batch, CompilerPolicy, ConstraintDegradation};
5use kernel_execution::{
6    schedule_execution, ExecutionBudget, ExecutionMode, ExecutionReport, ExecutionStopReason,
7    ScheduledExecution, SchedulerStageKind,
8};
9use kernel_oracles::{
10    evaluate_causal_refuter, evaluate_conservative, evaluate_exact_bounded,
11    minimal_perturbation_witness, OracleAssessment, OracleRefutationOutcome,
12    OracleRefutationResult,
13};
14use schemars::JsonSchema;
15use semantic_memory::ProjectionImportLogEntry;
16use serde::{Deserialize, Serialize};
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
19#[schemars(title = "InferenceAdvisoryV1")]
20pub struct InferenceAdvisory {
21    pub batch_id: String,
22    pub source_envelope_id: String,
23    pub graph_hash: String,
24    pub execution_mode: String,
25    pub iteration_count: u32,
26    pub message_count: usize,
27    pub degradation_markers: Vec<String>,
28    pub oracle_mode: String,
29    pub oracle_supported: bool,
30    pub satisfied_constraint_count: usize,
31    pub advisory_only: bool,
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub degraded_reason: Option<String>,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
37#[schemars(title = "InferenceExplanationV1")]
38pub struct InferenceExplanation {
39    pub batch_id: String,
40    pub source_envelope_id: String,
41    pub graph_hash: String,
42    pub execution_mode: String,
43    pub stop_reason: String,
44    pub stage_kinds: Vec<String>,
45    pub degradation_markers: Vec<String>,
46    pub witness_count: usize,
47    pub witness_node_ids: Vec<String>,
48    pub certificate_count: usize,
49    pub certificate_node_ids: Vec<String>,
50    pub residual_count: usize,
51    pub residual_micros: Vec<u64>,
52    pub syndrome_signatures: Vec<String>,
53    pub calibration_caveats: Vec<String>,
54    pub refutation_outcome: String,
55    pub causal_refutation_outcome: String,
56    pub minimal_perturbation_outcome: String,
57    pub oracle_mode: String,
58    pub oracle_supported: bool,
59    pub advisory_only: bool,
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub degraded_reason: Option<String>,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
65#[schemars(title = "RiskGateDecisionV1")]
66pub struct RiskGateDecision {
67    pub batch_id: String,
68    pub source_envelope_id: String,
69    pub status: String,
70    pub reasons: Vec<String>,
71    pub advisory_only: bool,
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub degraded_reason: Option<String>,
74}
75
76struct KernelAdvisoryContext {
77    import_log: ProjectionImportLogEntry,
78    compiled: constraint_compiler::CompileOutput,
79    oracle: OracleAssessment,
80    scheduled: ScheduledExecution,
81    causal_refutation: OracleRefutationResult,
82    minimal_perturbation: OracleRefutationResult,
83}
84
85fn degradation_marker_name(marker: &ConstraintDegradation) -> &'static str {
86    match marker {
87        ConstraintDegradation::MissingClaimFamily => "missing_claim_family",
88        ConstraintDegradation::MissingAssertionGroup => "missing_assertion_group",
89        ConstraintDegradation::MissingRelationGroup => "missing_relation_group",
90        ConstraintDegradation::ThinExport => "thin_export",
91    }
92}
93
94fn oracle_mode_name(oracle: &OracleAssessment) -> &'static str {
95    match oracle.mode {
96        kernel_oracles::OracleMode::ExactBounded => "exact_bounded",
97        kernel_oracles::OracleMode::ConservativeFallback => "conservative_fallback",
98    }
99}
100
101fn execution_mode_name(report: &ExecutionReport) -> &'static str {
102    match report.execution_mode {
103        ExecutionMode::AcyclicBaseline => "acyclic_baseline",
104        ExecutionMode::MessagePassingBaseline => "message_passing_baseline",
105        ExecutionMode::DeltaPropagation => "delta_propagation",
106        ExecutionMode::ResidualCorrection => "residual_correction",
107        ExecutionMode::MultiscaleScheduler => "multiscale_scheduler",
108    }
109}
110
111fn execution_stop_reason_name(reason: &ExecutionStopReason) -> &'static str {
112    match reason {
113        ExecutionStopReason::AcyclicCompletion => "acyclic_completion",
114        ExecutionStopReason::FixedPoint => "fixed_point",
115        ExecutionStopReason::MaxIterations => "max_iterations",
116        ExecutionStopReason::BudgetExhausted => "budget_exhausted",
117        ExecutionStopReason::DeltaWindowCompleted => "delta_window_completed",
118    }
119}
120
121fn scheduler_stage_name(stage: &SchedulerStageKind) -> &'static str {
122    match stage {
123        SchedulerStageKind::AcyclicPass => "acyclic_pass",
124        SchedulerStageKind::MessagePassing => "message_passing",
125        SchedulerStageKind::DeltaPropagation => "delta_propagation",
126        SchedulerStageKind::ResidualCorrection => "residual_correction",
127    }
128}
129
130fn refutation_outcome_name(outcome: &OracleRefutationOutcome) -> &'static str {
131    match outcome {
132        OracleRefutationOutcome::FlipWitness { .. } => "flip_witness_found",
133        OracleRefutationOutcome::NoFlipFound { .. } => "no_flip_found_within_budget",
134        OracleRefutationOutcome::NotApplicable { .. } => "unsupported",
135    }
136}
137
138impl KnowledgeRuntime {
139    async fn latest_kernel_advisory_context(
140        &self,
141        scope: &Scope,
142    ) -> Result<Option<KernelAdvisoryContext>, RuntimeError> {
143        let Some(import_log) = self
144            .adapter_ref()
145            .latest_kernel_projection_import(&scope.key())
146            .await?
147        else {
148            return Ok(None);
149        };
150        let Some(batch) = import_log.rebuildable_kernel_batch_v3()? else {
151            return Ok(None);
152        };
153
154        let compiled = compile_batch(
155            &batch,
156            &CompilerPolicy {
157                policy_version: "knowledge-runtime.phase1.v1".into(),
158                include_hyperedges: true,
159            },
160        );
161        let oracle =
162            evaluate_exact_bounded(&compiled).unwrap_or_else(|| evaluate_conservative(&compiled));
163        let scheduled = schedule_execution(&compiled, &ExecutionBudget::default());
164        let primary_node_id = scheduled
165            .execution
166            .witnesses
167            .first()
168            .map(|witness| witness.node_id.clone())
169            .or_else(|| {
170                compiled
171                    .nodes
172                    .iter()
173                    .find(|node| node.kind != "nuisance_state")
174                    .map(|node| node.node_id.clone())
175            })
176            .unwrap_or_else(|| "unsupported".into());
177        let causal_refutation = evaluate_causal_refuter(&compiled, &primary_node_id, 1);
178        let minimal_perturbation = minimal_perturbation_witness(&compiled, &primary_node_id, 1);
179
180        Ok(Some(KernelAdvisoryContext {
181            import_log,
182            compiled,
183            oracle,
184            scheduled,
185            causal_refutation,
186            minimal_perturbation,
187        }))
188    }
189
190    /// Compile the latest rebuildable kernel payload for a scope into a
191    /// non-authoritative inference advisory.
192    pub async fn latest_inference_advisory(
193        &self,
194        scope: Option<&Scope>,
195    ) -> Result<Option<InferenceAdvisory>, RuntimeError> {
196        let scope = scope.unwrap_or(self.default_scope());
197        let Some(context) = self.latest_kernel_advisory_context(scope).await? else {
198            return Ok(None);
199        };
200
201        Ok(Some(InferenceAdvisory {
202            batch_id: context.import_log.batch_id,
203            source_envelope_id: context.import_log.source_envelope_id,
204            graph_hash: context.compiled.graph_hash.hex().to_string(),
205            execution_mode: execution_mode_name(&context.scheduled.execution).to_string(),
206            iteration_count: context.scheduled.execution.iteration_count,
207            message_count: context.scheduled.execution.messages.len(),
208            degradation_markers: context
209                .compiled
210                .degradations
211                .iter()
212                .map(degradation_marker_name)
213                .map(str::to_string)
214                .collect(),
215            oracle_mode: oracle_mode_name(&context.oracle).to_string(),
216            oracle_supported: context.oracle.supported,
217            satisfied_constraint_count: context.oracle.satisfied_constraint_count,
218            advisory_only: context.scheduled.execution.advisory_only,
219            degraded_reason: context.scheduled.degraded_reason.clone(),
220        }))
221    }
222
223    /// Expose witnesses, residuals, syndromes, calibration, and refutation
224    /// state for the latest rebuildable kernel payload.
225    pub async fn latest_inference_explanation(
226        &self,
227        scope: Option<&Scope>,
228    ) -> Result<Option<InferenceExplanation>, RuntimeError> {
229        let scope = scope.unwrap_or(self.default_scope());
230        let Some(context) = self.latest_kernel_advisory_context(scope).await? else {
231            return Ok(None);
232        };
233
234        Ok(Some(InferenceExplanation {
235            batch_id: context.import_log.batch_id,
236            source_envelope_id: context.import_log.source_envelope_id,
237            graph_hash: context.compiled.graph_hash.hex().to_string(),
238            execution_mode: execution_mode_name(&context.scheduled.execution).to_string(),
239            stop_reason: execution_stop_reason_name(&context.scheduled.execution.stop_reason)
240                .to_string(),
241            stage_kinds: context
242                .scheduled
243                .stage_kinds
244                .iter()
245                .map(scheduler_stage_name)
246                .map(str::to_string)
247                .collect(),
248            degradation_markers: context
249                .compiled
250                .degradations
251                .iter()
252                .map(degradation_marker_name)
253                .map(str::to_string)
254                .collect(),
255            witness_count: context.scheduled.execution.witnesses.len(),
256            witness_node_ids: context
257                .scheduled
258                .execution
259                .witnesses
260                .iter()
261                .map(|witness| witness.node_id.clone())
262                .collect(),
263            certificate_count: context.scheduled.execution.certificates.len(),
264            certificate_node_ids: context
265                .scheduled
266                .execution
267                .certificates
268                .iter()
269                .map(|certificate| certificate.node_id.clone())
270                .collect(),
271            residual_count: context.scheduled.execution.residuals.len(),
272            residual_micros: context
273                .scheduled
274                .execution
275                .residuals
276                .iter()
277                .map(|sample| sample.total_residual_micros)
278                .collect(),
279            syndrome_signatures: context
280                .scheduled
281                .execution
282                .syndromes
283                .iter()
284                .map(|syndrome| syndrome.signature.clone())
285                .collect(),
286            calibration_caveats: context
287                .scheduled
288                .execution
289                .calibration_report
290                .as_ref()
291                .map(|report| {
292                    report
293                        .caution_markers
294                        .iter()
295                        .map(|marker| marker.replace('_', " "))
296                        .collect()
297                })
298                .unwrap_or_default(),
299            refutation_outcome: refutation_outcome_name(&context.causal_refutation.outcome)
300                .to_string(),
301            causal_refutation_outcome: refutation_outcome_name(&context.causal_refutation.outcome)
302                .to_string(),
303            minimal_perturbation_outcome: refutation_outcome_name(
304                &context.minimal_perturbation.outcome,
305            )
306            .to_string(),
307            oracle_mode: oracle_mode_name(&context.oracle).to_string(),
308            oracle_supported: context.oracle.supported,
309            advisory_only: context.scheduled.execution.advisory_only,
310            degraded_reason: context.scheduled.degraded_reason.clone(),
311        }))
312    }
313
314    /// Gate risk-influencing use of runtime kernel output on explicit artifacts.
315    pub async fn latest_risk_gate_decision(
316        &self,
317        scope: Option<&Scope>,
318    ) -> Result<Option<RiskGateDecision>, RuntimeError> {
319        let scope = scope.unwrap_or(self.default_scope());
320        let Some(context) = self.latest_kernel_advisory_context(scope).await? else {
321            return Ok(None);
322        };
323        let mut reasons = Vec::new();
324        if !context.oracle.supported {
325            reasons.push("oracle_not_supported".to_string());
326        }
327        if !context.compiled.degradations.is_empty() {
328            reasons.push("degradation_active".to_string());
329        }
330        if let Some(reason) = &context.scheduled.degraded_reason {
331            reasons.push(format!("scheduled_degraded:{reason}"));
332        }
333        if context.scheduled.execution.witnesses.is_empty() {
334            reasons.push("missing_witness".to_string());
335        }
336        if context.scheduled.execution.certificates.is_empty() {
337            reasons.push("missing_certificate".to_string());
338        }
339        if context
340            .scheduled
341            .execution
342            .calibration_report
343            .as_ref()
344            .is_some_and(|report| !report.caution_markers.is_empty())
345        {
346            reasons.push("calibration_caveat_active".to_string());
347        }
348        if matches!(
349            context.causal_refutation.outcome,
350            OracleRefutationOutcome::NotApplicable { .. }
351        ) || matches!(
352            context.minimal_perturbation.outcome,
353            OracleRefutationOutcome::NotApplicable { .. }
354        ) {
355            reasons.push("refutation_harness_incomplete".to_string());
356        }
357
358        Ok(Some(RiskGateDecision {
359            batch_id: context.import_log.batch_id,
360            source_envelope_id: context.import_log.source_envelope_id,
361            status: if reasons.is_empty() {
362                "allowed".to_string()
363            } else {
364                "blocked".to_string()
365            },
366            reasons,
367            advisory_only: context.scheduled.execution.advisory_only,
368            degraded_reason: context.scheduled.degraded_reason.clone(),
369        }))
370    }
371
372    /// Compatibility wrapper for callers using the earlier method name.
373    pub async fn latest_risk_gate(
374        &self,
375        scope: Option<&Scope>,
376    ) -> Result<Option<RiskGateDecision>, RuntimeError> {
377        self.latest_risk_gate_decision(scope).await
378    }
379}