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 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 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 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 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}