1#![cfg_attr(test, allow(clippy::expect_used))]
2
3use constraint_compiler::{CompileOutput, CompiledRegion, InvalidationCone};
4use recursive_kernel_core::{ArtifactAuthorityClass, Syndrome};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use stack_ids::{ContentDigest, ConvergenceReportId, RegionId, SyndromeId};
8use std::collections::{BTreeMap, BTreeSet};
9
10const FULL_CONFIDENCE_MICROS: u64 = 1_000_000;
11const DEFAULT_FIXED_POINT_TOLERANCE_MICROS: u64 = 1_000;
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
14pub struct CandidateEvidenceRetrievalRefV1 {
15 pub retrieval_system: String,
16 pub candidate_backend: Option<String>,
17 pub generation_id: Option<String>,
18 pub evidence_ref: String,
19 pub candidate_only: bool,
20 pub exact_rerank: bool,
21 pub verified_by_oracle: bool,
22}
23
24impl CandidateEvidenceRetrievalRefV1 {
25 pub fn new_candidate(
26 retrieval_system: impl Into<String>,
27 evidence_ref: impl Into<String>,
28 ) -> Self {
29 Self {
30 retrieval_system: retrieval_system.into(),
31 candidate_backend: None,
32 generation_id: None,
33 evidence_ref: evidence_ref.into(),
34 candidate_only: true,
35 exact_rerank: true,
36 verified_by_oracle: false,
37 }
38 }
39
40 pub fn is_verified_premise(&self) -> bool {
41 !self.candidate_only && self.verified_by_oracle
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
46#[serde(rename_all = "snake_case")]
47pub enum ExecutionMode {
48 AcyclicBaseline,
49 MessagePassingBaseline,
50 DeltaPropagation,
51 ResidualCorrection,
52 MultiscaleScheduler,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
56#[serde(rename_all = "snake_case")]
57pub enum ExecutionStopReason {
58 AcyclicCompletion,
59 FixedPoint,
60 MaxIterations,
61 BudgetExhausted,
62 DeltaWindowCompleted,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
66#[serde(rename_all = "snake_case")]
67pub enum SchedulerStageKind {
68 AcyclicPass,
69 MessagePassing,
70 DeltaPropagation,
71 ResidualCorrection,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
75#[serde(rename_all = "snake_case")]
76pub enum WorkloadClass {
77 Interactive,
78 Background,
79 Heavy,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
83pub struct ExecutionBudget {
84 pub max_iterations: u32,
85 pub max_messages: usize,
86 pub max_nodes: usize,
87 pub allow_repair: bool,
88}
89
90impl Default for ExecutionBudget {
91 fn default() -> Self {
92 Self {
93 max_iterations: 6,
94 max_messages: 256,
95 max_nodes: 32,
96 allow_repair: true,
97 }
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
102pub struct NodeBelief {
103 pub node_id: String,
104 pub local_support_count: usize,
105 pub belief_micros: u64,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
109pub struct ConstraintMessage {
110 pub iteration: u32,
111 pub from_constraint_id: String,
112 pub to_node_id: String,
113 pub support_micros: u64,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
117pub struct ResidualSample {
118 pub iteration: u32,
119 pub total_residual_micros: u64,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
123pub struct ExecutionWitnessArtifact {
124 pub node_id: String,
125 pub supporting_constraint_ids: Vec<String>,
126 pub belief_micros: u64,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
130pub struct ExecutionCertificateArtifact {
131 pub node_id: String,
132 pub certificate_kind: String,
133 pub supporting_constraint_count: usize,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
137pub struct ExecutionCalibrationReport {
138 pub nuisance_node_ids: Vec<String>,
139 pub caution_markers: Vec<String>,
140 pub degraded: bool,
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
144#[serde(rename_all = "snake_case")]
145pub enum RecomputeTrigger {
146 LineageDelta,
147 ValidTimeSliceChange,
148 RecordedTimeSliceChange,
149 RepairDecision,
150 RollbackQuarantine,
151 ExplicitInvalidationManifest,
152 NuisanceStateUpdate,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
156pub struct InvalidationManifest {
157 pub trigger: RecomputeTrigger,
158 pub changed_node_ids: Vec<String>,
159 pub changed_region_ids: Vec<RegionId>,
160 pub explicit_global_rebuild: bool,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
164pub struct RegionExecutionTrace {
165 pub region_id: RegionId,
166 pub region_digest: String,
167 pub executed_node_ids: Vec<String>,
168 pub witness_node_ids: Vec<String>,
169 pub syndrome_ids: Vec<SyndromeId>,
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
173pub struct ConvergenceGovernance {
174 pub damping_factor_micros: u64,
175 pub residual_tolerance_micros: u64,
176 pub max_iterations: u32,
177 pub stop_rule: String,
178 pub escalation_rule: String,
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
182pub struct ConvergenceReport {
183 pub convergence_report_id: ConvergenceReportId,
184 pub governance: ConvergenceGovernance,
185 pub residual_monotone_nonincreasing: bool,
186 pub converged: bool,
187 pub escalated: bool,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
191pub struct DeltaPropagationReport {
192 pub changed_node_ids: Vec<String>,
193 pub recomputed_node_ids: Vec<String>,
194 pub recomputed_region_ids: Vec<RegionId>,
195 pub invalidation_manifest: InvalidationManifest,
196 pub region_traces: Vec<RegionExecutionTrace>,
197 pub execution: ExecutionReport,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
201pub struct ScheduledExecution {
202 pub workload_class: WorkloadClass,
203 pub stage_kinds: Vec<SchedulerStageKind>,
204 pub degraded_reason: Option<String>,
205 pub execution: ExecutionReport,
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
209pub struct ExecutionReport {
210 pub execution_mode: ExecutionMode,
211 pub stop_reason: ExecutionStopReason,
212 pub iteration_count: u32,
213 pub messages: Vec<ConstraintMessage>,
214 pub node_beliefs: Vec<NodeBelief>,
215 pub residuals: Vec<ResidualSample>,
216 pub syndromes: Vec<Syndrome>,
217 pub witnesses: Vec<ExecutionWitnessArtifact>,
218 pub certificates: Vec<ExecutionCertificateArtifact>,
219 pub region_traces: Vec<RegionExecutionTrace>,
220 pub calibration_report: Option<ExecutionCalibrationReport>,
221 pub convergence_report: ConvergenceReport,
222 pub advisory_only: bool,
223}
224
225impl ExecutionReport {
226 pub fn authority_class(&self) -> ArtifactAuthorityClass {
228 ArtifactAuthorityClass::NonAuthoritativeDerived
229 }
230}
231
232pub fn execute_acyclic_baseline(compiled: &CompileOutput) -> ExecutionReport {
234 let initial = initial_beliefs(compiled);
235 let step = message_iteration(compiled, &initial, None, 1);
236 finalize_execution(
237 compiled,
238 ExecutionMode::AcyclicBaseline,
239 ExecutionStopReason::AcyclicCompletion,
240 1,
241 step.messages,
242 vec![ResidualSample {
243 iteration: 1,
244 total_residual_micros: total_residual(&initial, &step.beliefs),
245 }],
246 step.beliefs,
247 )
248}
249
250pub fn execute_message_passing_baseline(
252 compiled: &CompileOutput,
253 max_iterations: u32,
254) -> ExecutionReport {
255 execute_message_passing_internal(
256 compiled,
257 None,
258 max_iterations,
259 ExecutionMode::MessagePassingBaseline,
260 )
261}
262
263pub fn execute_delta_propagation(
265 compiled: &CompileOutput,
266 changed_node_ids: &[String],
267 max_iterations: u32,
268) -> DeltaPropagationReport {
269 let recomputed_node_ids =
270 affected_nodes_for_delta(&compiled.invalidation_cones, changed_node_ids);
271 let execution = execute_message_passing_internal(
272 compiled,
273 Some(&recomputed_node_ids),
274 max_iterations,
275 ExecutionMode::DeltaPropagation,
276 );
277 let recomputed_region_ids = affected_regions_for_nodes(&compiled.regions, &recomputed_node_ids);
278 let invalidation_manifest = InvalidationManifest {
279 trigger: RecomputeTrigger::LineageDelta,
280 changed_node_ids: changed_node_ids.to_vec(),
281 changed_region_ids: recomputed_region_ids.clone(),
282 explicit_global_rebuild: false,
283 };
284 let region_traces = build_region_traces(
285 &compiled.regions,
286 &recomputed_node_ids,
287 &execution.witnesses,
288 &execution.syndromes,
289 );
290
291 DeltaPropagationReport {
292 changed_node_ids: changed_node_ids.to_vec(),
293 recomputed_node_ids,
294 recomputed_region_ids,
295 invalidation_manifest,
296 region_traces,
297 execution: ExecutionReport {
298 stop_reason: ExecutionStopReason::DeltaWindowCompleted,
299 ..execution
300 },
301 }
302}
303
304pub fn execute_residual_correction(
306 compiled: &CompileOutput,
307 max_iterations: u32,
308) -> ExecutionReport {
309 let baseline = execute_message_passing_baseline(compiled, max_iterations.max(1));
310 let mut beliefs = belief_map(&baseline.node_beliefs);
311 let mut residuals = baseline.residuals.clone();
312 let mut stop_reason = ExecutionStopReason::FixedPoint;
313 let max_iterations = max_iterations.max(1);
314 let mut last_iteration = baseline.iteration_count;
315
316 for iteration in 1..=max_iterations {
317 let sample_iteration = baseline.iteration_count + iteration;
318 let mut next = beliefs.clone();
319 for hyperedge in &compiled.hyperedges {
320 let active_members = hyperedge
321 .member_node_ids
322 .iter()
323 .filter(|node_id| !node_id.starts_with("nuisance:"))
324 .cloned()
325 .collect::<Vec<_>>();
326 if active_members.len() < 2 {
327 continue;
328 }
329 let average = active_members
330 .iter()
331 .filter_map(|node_id| beliefs.get(node_id).copied())
332 .sum::<u64>()
333 / active_members.len() as u64;
334 for node_id in active_members {
335 next.insert(node_id, average);
336 }
337 }
338
339 let residual = total_residual(&beliefs, &next);
340 residuals.push(ResidualSample {
341 iteration: sample_iteration,
342 total_residual_micros: residual,
343 });
344 beliefs = next;
345 last_iteration = sample_iteration;
346 if iteration == max_iterations {
347 stop_reason = ExecutionStopReason::MaxIterations;
348 break;
349 }
350 if residual <= DEFAULT_FIXED_POINT_TOLERANCE_MICROS {
351 stop_reason = ExecutionStopReason::FixedPoint;
352 break;
353 }
354 }
355
356 finalize_execution(
357 compiled,
358 ExecutionMode::ResidualCorrection,
359 stop_reason,
360 last_iteration,
361 baseline.messages,
362 residuals,
363 beliefs,
364 )
365}
366
367pub fn schedule_execution(
369 compiled: &CompileOutput,
370 budget: &ExecutionBudget,
371) -> ScheduledExecution {
372 if compiled.nodes.len() > budget.max_nodes {
373 let degraded = finalize_execution(
374 compiled,
375 ExecutionMode::MultiscaleScheduler,
376 ExecutionStopReason::BudgetExhausted,
377 0,
378 Vec::new(),
379 Vec::new(),
380 initial_beliefs(compiled),
381 );
382 return ScheduledExecution {
383 workload_class: WorkloadClass::Heavy,
384 stage_kinds: vec![],
385 degraded_reason: Some("budget_exhausted".into()),
386 execution: degraded,
387 };
388 }
389
390 if !compiled.invalidation_cones.is_empty() && compiled.nodes.len() > 8 {
391 let execution = execute_message_passing_baseline(compiled, budget.max_iterations);
392 return ScheduledExecution {
393 workload_class: WorkloadClass::Background,
394 stage_kinds: vec![SchedulerStageKind::MessagePassing],
395 degraded_reason: Some("explicit_changed_nodes_required_for_delta".into()),
396 execution,
397 };
398 }
399
400 if compiled.hyperedges.is_empty() {
401 let execution = execute_acyclic_baseline(compiled);
402 return ScheduledExecution {
403 workload_class: WorkloadClass::Interactive,
404 stage_kinds: vec![SchedulerStageKind::AcyclicPass],
405 degraded_reason: None,
406 execution,
407 };
408 }
409
410 let mut stage_kinds = vec![SchedulerStageKind::MessagePassing];
411 let mut execution = execute_message_passing_baseline(compiled, budget.max_iterations);
412 if budget.allow_repair && (compiled.hyperedges.len() > 1 || compiled.nodes.len() > 8) {
413 stage_kinds.push(SchedulerStageKind::ResidualCorrection);
414 execution = execute_residual_correction(compiled, budget.max_iterations);
415 }
416 ScheduledExecution {
417 workload_class: WorkloadClass::Background,
418 stage_kinds,
419 degraded_reason: None,
420 execution,
421 }
422}
423
424fn execute_message_passing_internal(
425 compiled: &CompileOutput,
426 focus_nodes: Option<&[String]>,
427 max_iterations: u32,
428 mode: ExecutionMode,
429) -> ExecutionReport {
430 let mut beliefs = initial_beliefs(compiled);
431 let mut messages = Vec::new();
432 let mut residuals = Vec::new();
433 let mut stop_reason = ExecutionStopReason::FixedPoint;
434 let focused = focus_nodes.map(|nodes| nodes.iter().cloned().collect::<BTreeSet<_>>());
435 let iterations = max_iterations.max(1);
436 let mut performed = 0;
437
438 for iteration in 1..=iterations {
439 performed = iteration;
440 let step = message_iteration(compiled, &beliefs, focused.as_ref(), iteration);
441 let residual = total_residual(&beliefs, &step.beliefs);
442 residuals.push(ResidualSample {
443 iteration,
444 total_residual_micros: residual,
445 });
446 messages.extend(step.messages);
447 if iteration == iterations {
448 beliefs = step.beliefs;
449 stop_reason = ExecutionStopReason::MaxIterations;
450 break;
451 }
452 if residual <= DEFAULT_FIXED_POINT_TOLERANCE_MICROS {
453 beliefs = step.beliefs;
454 break;
455 }
456 beliefs = step.beliefs;
457 }
458
459 finalize_execution(
460 compiled,
461 mode,
462 stop_reason,
463 performed,
464 messages,
465 residuals,
466 beliefs,
467 )
468}
469
470fn finalize_execution(
471 compiled: &CompileOutput,
472 execution_mode: ExecutionMode,
473 stop_reason: ExecutionStopReason,
474 iteration_count: u32,
475 messages: Vec<ConstraintMessage>,
476 residuals: Vec<ResidualSample>,
477 beliefs: BTreeMap<String, u64>,
478) -> ExecutionReport {
479 let node_beliefs = belief_rows(compiled, &beliefs);
480 let syndromes = emit_syndromes(compiled, &beliefs);
481 let witnesses = emit_witnesses(compiled, &beliefs);
482 let certificates = emit_certificates(&witnesses);
483 let region_traces = build_region_traces(
484 &compiled.regions,
485 &compiled
486 .nodes
487 .iter()
488 .map(|node| node.node_id.clone())
489 .collect::<Vec<_>>(),
490 &witnesses,
491 &syndromes,
492 );
493 let calibration_report = emit_calibration_report(compiled);
494 let convergence_report =
495 emit_convergence_report(stop_reason.clone(), iteration_count, &residuals);
496
497 ExecutionReport {
498 execution_mode,
499 stop_reason,
500 iteration_count,
501 messages,
502 node_beliefs,
503 residuals,
504 syndromes,
505 witnesses,
506 certificates,
507 region_traces,
508 calibration_report,
509 convergence_report,
510 advisory_only: true,
511 }
512}
513
514struct IterationStep {
515 beliefs: BTreeMap<String, u64>,
516 messages: Vec<ConstraintMessage>,
517}
518
519fn message_iteration(
520 compiled: &CompileOutput,
521 current: &BTreeMap<String, u64>,
522 focus_nodes: Option<&BTreeSet<String>>,
523 iteration: u32,
524) -> IterationStep {
525 let mut next = current.clone();
526 let mut messages = Vec::new();
527 let base = initial_beliefs(compiled);
528
529 for node in &compiled.nodes {
530 if let Some(focused) = focus_nodes {
531 if !focused.contains(&node.node_id) {
532 continue;
533 }
534 }
535
536 let local = base.get(&node.node_id).copied().unwrap_or(0);
537 let peers = peer_beliefs(compiled, current, &node.node_id);
538 let peer_average = if peers.is_empty() {
539 local
540 } else {
541 peers.iter().sum::<u64>() / peers.len() as u64
542 };
543 let next_value = if node.node_id.starts_with("nuisance:") {
544 peer_average.max(local)
545 } else {
546 (local + peer_average) / 2
547 };
548 next.insert(node.node_id.clone(), next_value);
549 }
550
551 for constraint in &compiled.constraints {
552 let signal = constraint
553 .variable_ids
554 .iter()
555 .filter_map(|node_id| next.get(node_id).copied())
556 .sum::<u64>()
557 / constraint.variable_ids.len().max(1) as u64;
558 for node_id in &constraint.variable_ids {
559 messages.push(ConstraintMessage {
560 iteration,
561 from_constraint_id: constraint.constraint_id.as_str().to_string(),
562 to_node_id: node_id.clone(),
563 support_micros: signal,
564 });
565 }
566 }
567
568 IterationStep {
569 beliefs: next,
570 messages,
571 }
572}
573
574fn initial_beliefs(compiled: &CompileOutput) -> BTreeMap<String, u64> {
575 let support = local_support_by_node(compiled);
576 let max_support = support.values().copied().max().unwrap_or(1).max(1);
577 compiled
578 .nodes
579 .iter()
580 .map(|node| {
581 let belief = support
582 .get(&node.node_id)
583 .copied()
584 .unwrap_or(0)
585 .saturating_mul(FULL_CONFIDENCE_MICROS)
586 / max_support;
587 (node.node_id.clone(), belief)
588 })
589 .collect()
590}
591
592fn local_support_by_node(compiled: &CompileOutput) -> BTreeMap<String, u64> {
593 let mut support = BTreeMap::new();
594 for node in &compiled.nodes {
595 let local_constraints = compiled
596 .constraints
597 .iter()
598 .filter(|constraint| constraint.variable_ids.iter().any(|id| id == &node.node_id))
599 .count() as u64;
600 let local_hyperedges = compiled
601 .hyperedges
602 .iter()
603 .filter(|hyperedge| {
604 hyperedge
605 .member_node_ids
606 .iter()
607 .any(|id| id == &node.node_id)
608 })
609 .count() as u64;
610 support.insert(
611 node.node_id.clone(),
612 (local_constraints + local_hyperedges).max(1),
613 );
614 }
615 support
616}
617
618fn peer_beliefs(
619 compiled: &CompileOutput,
620 current: &BTreeMap<String, u64>,
621 node_id: &str,
622) -> Vec<u64> {
623 let mut peers = Vec::new();
624 for hyperedge in &compiled.hyperedges {
625 if !hyperedge
626 .member_node_ids
627 .iter()
628 .any(|member| member == node_id)
629 {
630 continue;
631 }
632 for member in &hyperedge.member_node_ids {
633 if member == node_id {
634 continue;
635 }
636 if let Some(value) = current.get(member) {
637 peers.push(*value);
638 }
639 }
640 }
641 peers
642}
643
644fn total_residual(previous: &BTreeMap<String, u64>, next: &BTreeMap<String, u64>) -> u64 {
645 next.iter()
646 .map(|(node_id, next_value)| {
647 previous
648 .get(node_id)
649 .copied()
650 .unwrap_or(0)
651 .abs_diff(*next_value)
652 })
653 .sum()
654}
655
656fn belief_rows(compiled: &CompileOutput, beliefs: &BTreeMap<String, u64>) -> Vec<NodeBelief> {
657 let local_support = local_support_by_node(compiled);
658 compiled
659 .nodes
660 .iter()
661 .map(|node| NodeBelief {
662 node_id: node.node_id.clone(),
663 local_support_count: local_support.get(&node.node_id).copied().unwrap_or(0) as usize,
664 belief_micros: beliefs.get(&node.node_id).copied().unwrap_or(0),
665 })
666 .collect()
667}
668
669fn belief_map(rows: &[NodeBelief]) -> BTreeMap<String, u64> {
670 rows.iter()
671 .map(|row| (row.node_id.clone(), row.belief_micros))
672 .collect()
673}
674
675fn emit_syndromes(compiled: &CompileOutput, beliefs: &BTreeMap<String, u64>) -> Vec<Syndrome> {
676 let mut syndromes = Vec::new();
677 for degradation in &compiled.degradations {
678 let signature = format!("degradation:{degradation:?}").to_lowercase();
679 syndromes.push(Syndrome {
680 syndrome_id: SyndromeId::new(format!("syndrome:{signature}")),
681 signature,
682 blocked_by_degradation: true,
683 });
684 }
685
686 for constraint in &compiled.constraints {
687 let active_non_nuisance = constraint
688 .variable_ids
689 .iter()
690 .filter(|node_id| !node_id.starts_with("nuisance:"))
691 .filter(|node_id| beliefs.get(*node_id).copied().unwrap_or(0) >= 250_000)
692 .count();
693 if active_non_nuisance < 2 && constraint.variable_ids.len() > 1 {
694 let digest = ContentDigest::compute(
695 format!("{}:{:?}", constraint.kind, constraint.variable_ids).as_bytes(),
696 );
697 syndromes.push(Syndrome {
698 syndrome_id: SyndromeId::new(format!("syndrome:{}", digest.hex())),
699 signature: format!("constraint_under_supported:{}", constraint.constraint_id),
700 blocked_by_degradation: false,
701 });
702 }
703 }
704
705 syndromes.sort_by(|a, b| a.signature.cmp(&b.signature));
706 syndromes
707}
708
709fn emit_witnesses(
710 compiled: &CompileOutput,
711 beliefs: &BTreeMap<String, u64>,
712) -> Vec<ExecutionWitnessArtifact> {
713 let mut witnesses = Vec::new();
714 for node in &compiled.nodes {
715 if node.node_id.starts_with("nuisance:") {
716 continue;
717 }
718 let belief = beliefs.get(&node.node_id).copied().unwrap_or(0);
719 if belief < 500_000 {
720 continue;
721 }
722 let mut supporting_constraint_ids = compiled
723 .constraints
724 .iter()
725 .filter(|constraint| constraint.variable_ids.iter().any(|id| id == &node.node_id))
726 .map(|constraint| constraint.constraint_id.as_str().to_string())
727 .collect::<Vec<_>>();
728 supporting_constraint_ids.sort();
729 witnesses.push(ExecutionWitnessArtifact {
730 node_id: node.node_id.clone(),
731 supporting_constraint_ids,
732 belief_micros: belief,
733 });
734 }
735 witnesses
736}
737
738fn emit_certificates(witnesses: &[ExecutionWitnessArtifact]) -> Vec<ExecutionCertificateArtifact> {
739 witnesses
740 .iter()
741 .map(|witness| ExecutionCertificateArtifact {
742 node_id: witness.node_id.clone(),
743 certificate_kind: if witness.supporting_constraint_ids.len() > 1 {
744 "multi_constraint_support".into()
745 } else {
746 "single_constraint_support".into()
747 },
748 supporting_constraint_count: witness.supporting_constraint_ids.len(),
749 })
750 .collect()
751}
752
753fn emit_calibration_report(compiled: &CompileOutput) -> Option<ExecutionCalibrationReport> {
754 let nuisance_node_ids = compiled
755 .nodes
756 .iter()
757 .filter(|node| node.kind == "nuisance_state")
758 .map(|node| node.node_id.clone())
759 .collect::<Vec<_>>();
760
761 if nuisance_node_ids.is_empty() && compiled.degradations.is_empty() {
762 return None;
763 }
764
765 let mut caution_markers = Vec::new();
766 if !nuisance_node_ids.is_empty() {
767 caution_markers.push("nuisance_state_present".into());
768 }
769 if !compiled.degradations.is_empty() {
770 caution_markers.push("degraded_export".into());
771 }
772 Some(ExecutionCalibrationReport {
773 nuisance_node_ids,
774 caution_markers,
775 degraded: !compiled.degradations.is_empty(),
776 })
777}
778
779fn build_region_traces(
780 regions: &[CompiledRegion],
781 executed_node_ids: &[String],
782 witnesses: &[ExecutionWitnessArtifact],
783 syndromes: &[Syndrome],
784) -> Vec<RegionExecutionTrace> {
785 let executed = executed_node_ids.iter().cloned().collect::<BTreeSet<_>>();
786 let syndrome_ids = syndromes
787 .iter()
788 .map(|syndrome| syndrome.syndrome_id.clone())
789 .collect::<Vec<_>>();
790
791 regions
792 .iter()
793 .filter_map(|region| {
794 let executed_node_ids = region
795 .node_ids
796 .iter()
797 .filter(|node_id| executed.contains(*node_id))
798 .cloned()
799 .collect::<Vec<_>>();
800 if executed_node_ids.is_empty() {
801 return None;
802 }
803
804 let witness_node_ids = witnesses
805 .iter()
806 .filter(|witness| {
807 region
808 .node_ids
809 .iter()
810 .any(|node_id| node_id == &witness.node_id)
811 })
812 .map(|witness| witness.node_id.clone())
813 .collect::<Vec<_>>();
814
815 Some(RegionExecutionTrace {
816 region_id: region.region_id.clone(),
817 region_digest: region.region_digest_id.to_string(),
818 executed_node_ids,
819 witness_node_ids,
820 syndrome_ids: syndrome_ids.clone(),
821 })
822 })
823 .collect()
824}
825
826fn emit_convergence_report(
827 stop_reason: ExecutionStopReason,
828 iteration_count: u32,
829 residuals: &[ResidualSample],
830) -> ConvergenceReport {
831 let residual_monotone_nonincreasing = residuals
832 .windows(2)
833 .all(|window| window[1].total_residual_micros <= window[0].total_residual_micros);
834 let converged = matches!(
835 stop_reason,
836 ExecutionStopReason::AcyclicCompletion
837 | ExecutionStopReason::FixedPoint
838 | ExecutionStopReason::DeltaWindowCompleted
839 );
840 let escalated = matches!(
841 stop_reason,
842 ExecutionStopReason::BudgetExhausted | ExecutionStopReason::MaxIterations
843 ) && iteration_count > 0;
844
845 let convergence_payload =
846 serde_json::to_vec(&(stop_reason.clone(), iteration_count, residuals)).unwrap_or_else(
847 |_| format!("{stop_reason:?}:{iteration_count}:{}", residuals.len()).into_bytes(),
848 );
849 let digest = ContentDigest::compute(&convergence_payload);
850
851 ConvergenceReport {
852 convergence_report_id: ConvergenceReportId::new(format!("convergence:{}", digest.hex())),
853 governance: ConvergenceGovernance {
854 damping_factor_micros: FULL_CONFIDENCE_MICROS,
855 residual_tolerance_micros: DEFAULT_FIXED_POINT_TOLERANCE_MICROS,
856 max_iterations: iteration_count.max(1),
857 stop_rule: "fixed_point_or_explicit_stop".into(),
858 escalation_rule: "emit_failure_artifact_on_nonconvergence".into(),
859 },
860 residual_monotone_nonincreasing,
861 converged,
862 escalated,
863 }
864}
865
866fn affected_regions_for_nodes(regions: &[CompiledRegion], node_ids: &[String]) -> Vec<RegionId> {
867 let changed = node_ids.iter().cloned().collect::<BTreeSet<_>>();
868 regions
869 .iter()
870 .filter(|region| {
871 region
872 .node_ids
873 .iter()
874 .any(|node_id| changed.contains(node_id))
875 })
876 .map(|region| region.region_id.clone())
877 .collect()
878}
879
880pub fn affected_nodes_for_delta(
882 invalidation_cones: &[InvalidationCone],
883 changed_node_ids: &[String],
884) -> Vec<String> {
885 let mut affected = changed_node_ids.iter().cloned().collect::<BTreeSet<_>>();
886 for changed in changed_node_ids {
887 if let Some(cone) = invalidation_cones
888 .iter()
889 .find(|cone| cone.source_node_id == *changed)
890 {
891 affected.extend(cone.affected_node_ids.iter().cloned());
892 }
893 }
894 affected.into_iter().collect()
895}
896
897#[cfg(test)]
898mod tests {
899 use super::*;
900 use constraint_compiler::{
901 CompilationBoundary, CompileOutput, CompiledRegion, ConstraintDegradation,
902 GraphGeometryManifest, GraphSurfaceKind, InferenceHyperedge, InferenceNode,
903 InvalidationCone, OracleSliceCandidate,
904 };
905 use recursive_kernel_core::{ConstraintUnit, CONSTRAINT_COMPILER_OPERATOR_ID};
906 use stack_ids::{ConstraintId, OperatorId, OracleSliceId, RegionDigestId, RegionId, ScopeKey};
907
908 fn compiled_fixture() -> CompileOutput {
909 CompileOutput {
910 graph_hash: ContentDigest::compute(b"kernel-execution-fixture"),
911 scope_key: ScopeKey::namespace_only("kernel-execution"),
912 geometry_manifest: GraphGeometryManifest {
913 surfaces: vec![
914 GraphSurfaceKind::Storage,
915 GraphSurfaceKind::Retrieval,
916 GraphSurfaceKind::Inference,
917 GraphSurfaceKind::Repair,
918 GraphSurfaceKind::Control,
919 ],
920 compilation_boundaries: vec![CompilationBoundary {
921 from_surface: GraphSurfaceKind::Inference,
922 to_surface: GraphSurfaceKind::Repair,
923 artifact_families: vec!["syndrome".into()],
924 deterministic: true,
925 }],
926 no_silent_collapse: true,
927 },
928 nodes: vec![
929 InferenceNode {
930 node_id: "node-a".into(),
931 kind: "claim_version".into(),
932 },
933 InferenceNode {
934 node_id: "node-b".into(),
935 kind: "claim_version".into(),
936 },
937 InferenceNode {
938 node_id: "nuisance:comparability:v1".into(),
939 kind: "nuisance_state".into(),
940 },
941 ],
942 hyperedges: vec![
943 InferenceHyperedge {
944 edge_id: "assertion_group:group-1".into(),
945 member_node_ids: vec!["node-a".into(), "node-b".into()],
946 },
947 InferenceHyperedge {
948 edge_id: "nuisance_edge:node-a:nuisance:comparability:v1".into(),
949 member_node_ids: vec!["node-a".into(), "nuisance:comparability:v1".into()],
950 },
951 ],
952 constraints: vec![
953 ConstraintUnit {
954 constraint_id: ConstraintId::new("constraint:assertion_group:group-1"),
955 kind: "hyperedge".into(),
956 variable_ids: vec!["node-a".into(), "node-b".into()],
957 operator_id: OperatorId::new(CONSTRAINT_COMPILER_OPERATOR_ID),
958 },
959 ConstraintUnit {
960 constraint_id: ConstraintId::new(
961 "constraint:nuisance_edge:node-a:nuisance:comparability:v1",
962 ),
963 kind: "nuisance_disclosure".into(),
964 variable_ids: vec!["node-a".into(), "nuisance:comparability:v1".into()],
965 operator_id: OperatorId::new(CONSTRAINT_COMPILER_OPERATOR_ID),
966 },
967 ],
968 regions: vec![CompiledRegion {
969 region_id: RegionId::new("region:fixture"),
970 region_digest_id: RegionDigestId::new("region-digest:fixture"),
971 node_ids: vec![
972 "node-a".into(),
973 "node-b".into(),
974 "nuisance:comparability:v1".into(),
975 ],
976 hyperedge_ids: vec![
977 "assertion_group:group-1".into(),
978 "nuisance_edge:node-a:nuisance:comparability:v1".into(),
979 ],
980 constraint_ids: vec![
981 ConstraintId::new("constraint:assertion_group:group-1"),
982 ConstraintId::new("constraint:nuisance_edge:node-a:nuisance:comparability:v1"),
983 ],
984 bounded_default_unit_of_work: true,
985 }],
986 invalidation_cones: vec![InvalidationCone {
987 source_node_id: "node-a".into(),
988 affected_node_ids: vec!["node-a".into(), "node-b".into()],
989 affected_hyperedge_ids: vec!["assertion_group:group-1".into()],
990 affected_constraint_ids: vec![ConstraintId::new(
991 "constraint:assertion_group:group-1",
992 )],
993 }],
994 degradations: Vec::<ConstraintDegradation>::new(),
995 oracle_candidates: vec![OracleSliceCandidate {
996 oracle_slice_id: OracleSliceId::new("oracle:fixture"),
997 node_ids: vec!["node-a".into(), "node-b".into()],
998 }],
999 }
1000 }
1001
1002 fn degraded_fixture() -> CompileOutput {
1003 let mut compiled = compiled_fixture();
1004 compiled.constraints[0].variable_ids = vec!["node-a".into(), "node-b".into()];
1005 compiled.degradations = vec![ConstraintDegradation::ThinExport];
1006 compiled
1007 }
1008
1009 fn unequal_supports_fixture() -> CompileOutput {
1010 let base = compiled_fixture();
1011 CompileOutput {
1012 graph_hash: base.graph_hash,
1013 scope_key: base.scope_key,
1014 geometry_manifest: base.geometry_manifest,
1015 nodes: vec![
1016 InferenceNode {
1017 node_id: "node-a".into(),
1018 kind: "claim_version".into(),
1019 },
1020 InferenceNode {
1021 node_id: "node-b".into(),
1022 kind: "claim_version".into(),
1023 },
1024 ],
1025 hyperedges: vec![InferenceHyperedge {
1026 edge_id: "edge:imbalance".into(),
1027 member_node_ids: vec!["node-a".into(), "node-b".into()],
1028 }],
1029 constraints: vec![
1030 ConstraintUnit {
1031 constraint_id: ConstraintId::new("constraint:edge:imbalance"),
1032 kind: "hyperedge".into(),
1033 variable_ids: vec!["node-a".into(), "node-b".into()],
1034 operator_id: OperatorId::new(CONSTRAINT_COMPILER_OPERATOR_ID),
1035 },
1036 ConstraintUnit {
1037 constraint_id: ConstraintId::new("constraint:node-a-bias"),
1038 kind: "node_bias".into(),
1039 variable_ids: vec!["node-a".into()],
1040 operator_id: OperatorId::new(CONSTRAINT_COMPILER_OPERATOR_ID),
1041 },
1042 ],
1043 regions: vec![CompiledRegion {
1044 region_id: RegionId::new("region:imbalance"),
1045 region_digest_id: RegionDigestId::new("region-digest:imbalance"),
1046 node_ids: vec!["node-a".into(), "node-b".into()],
1047 hyperedge_ids: vec!["edge:imbalance".into()],
1048 constraint_ids: vec![
1049 ConstraintId::new("constraint:edge:imbalance"),
1050 ConstraintId::new("constraint:node-a-bias"),
1051 ],
1052 bounded_default_unit_of_work: true,
1053 }],
1054 invalidation_cones: Vec::new(),
1055 degradations: Vec::<ConstraintDegradation>::new(),
1056 oracle_candidates: Vec::new(),
1057 }
1058 }
1059
1060 #[test]
1061 fn acyclic_baseline_is_deterministic_and_non_authoritative() {
1062 let compiled = compiled_fixture();
1063 let a = execute_acyclic_baseline(&compiled);
1064 let b = execute_acyclic_baseline(&compiled);
1065
1066 assert_eq!(a, b);
1067 assert_eq!(
1068 a.authority_class(),
1069 ArtifactAuthorityClass::NonAuthoritativeDerived
1070 );
1071 assert_eq!(a.stop_reason, ExecutionStopReason::AcyclicCompletion);
1072 assert!(!a.witnesses.is_empty());
1073 }
1074
1075 #[test]
1076 fn message_passing_baseline_is_bounded_and_emits_messages() {
1077 let compiled = compiled_fixture();
1078 let report = execute_message_passing_baseline(&compiled, 3);
1079
1080 assert_eq!(report.execution_mode, ExecutionMode::MessagePassingBaseline);
1081 assert!(report.iteration_count <= 3);
1082 assert!(!report.messages.is_empty());
1083 assert!(report
1084 .node_beliefs
1085 .iter()
1086 .all(|belief| belief.belief_micros > 0));
1087 }
1088
1089 #[test]
1090 fn message_passing_respects_iteration_cap_as_stop_rule() {
1091 let compiled = compiled_fixture();
1092 let report = execute_message_passing_baseline(&compiled, 1);
1093
1094 assert_eq!(report.execution_mode, ExecutionMode::MessagePassingBaseline);
1095 assert_eq!(report.stop_reason, ExecutionStopReason::MaxIterations);
1096 assert!(report.iteration_count <= 1);
1097 }
1098
1099 #[test]
1100 fn delta_propagation_recomputes_only_affected_slice() {
1101 let compiled = compiled_fixture();
1102 let delta = execute_delta_propagation(&compiled, &["node-a".into()], 2);
1103
1104 assert_eq!(
1105 delta.execution.execution_mode,
1106 ExecutionMode::DeltaPropagation
1107 );
1108 assert_eq!(
1109 delta.recomputed_node_ids,
1110 vec!["node-a".to_string(), "node-b".to_string()]
1111 );
1112 assert_eq!(
1113 affected_nodes_for_delta(&compiled.invalidation_cones, &["node-a".into()]),
1114 delta.recomputed_node_ids
1115 );
1116 assert_eq!(
1117 delta.recomputed_region_ids,
1118 vec![RegionId::new("region:fixture")]
1119 );
1120 assert!(!delta.region_traces.is_empty());
1121 assert!(!delta.invalidation_manifest.explicit_global_rebuild);
1122 }
1123
1124 #[test]
1125 fn residual_correction_reduces_residuals_monotonically() {
1126 let compiled = compiled_fixture();
1127 let report = execute_residual_correction(&compiled, 3);
1128
1129 let mut previous = u64::MAX;
1130 for sample in &report.residuals {
1131 assert!(sample.total_residual_micros <= previous);
1132 previous = sample.total_residual_micros;
1133 }
1134 assert!(report.convergence_report.residual_monotone_nonincreasing);
1135 }
1136
1137 #[test]
1138 fn residual_correction_reports_max_iterations_stop_reason_when_not_converged() {
1139 let compiled = unequal_supports_fixture();
1140 let report = execute_residual_correction(&compiled, 1);
1141
1142 assert_eq!(report.stop_reason, ExecutionStopReason::MaxIterations);
1143 assert_eq!(report.iteration_count, 2);
1144 }
1145
1146 #[test]
1147 fn schedule_execution_does_not_fabricate_delta_changed_nodes() {
1148 let mut compiled = compiled_fixture();
1149 for index in 0..9 {
1150 compiled.nodes.push(InferenceNode {
1151 node_id: format!("node-extra-{index}"),
1152 kind: "claim_version".into(),
1153 });
1154 }
1155
1156 let scheduled = schedule_execution(
1157 &compiled,
1158 &ExecutionBudget {
1159 max_nodes: 20,
1160 max_iterations: 2,
1161 allow_repair: true,
1162 max_messages: 256,
1163 },
1164 );
1165
1166 assert_eq!(
1167 scheduled.degraded_reason.as_deref(),
1168 Some("explicit_changed_nodes_required_for_delta")
1169 );
1170 assert_eq!(
1171 scheduled.stage_kinds,
1172 vec![SchedulerStageKind::MessagePassing]
1173 );
1174 assert_eq!(
1175 scheduled.execution.execution_mode,
1176 ExecutionMode::MessagePassingBaseline
1177 );
1178 }
1179
1180 #[test]
1181 fn scheduler_emits_budget_degradation_and_calibration() {
1182 let scheduled = schedule_execution(
1183 °raded_fixture(),
1184 &ExecutionBudget {
1185 max_nodes: 2,
1186 ..ExecutionBudget::default()
1187 },
1188 );
1189
1190 assert_eq!(
1191 scheduled.execution.stop_reason,
1192 ExecutionStopReason::BudgetExhausted
1193 );
1194 assert_eq!(
1195 scheduled.degraded_reason.as_deref(),
1196 Some("budget_exhausted")
1197 );
1198 assert!(scheduled.execution.calibration_report.is_some());
1199 assert!(scheduled
1200 .execution
1201 .syndromes
1202 .iter()
1203 .any(|syndrome| syndrome.blocked_by_degradation));
1204 }
1205
1206 #[test]
1207 fn thin_export_execution_surfaces_degradation_artifacts() {
1208 let mut compiled = compiled_fixture();
1209 compiled.degradations = vec![ConstraintDegradation::ThinExport];
1210
1211 let report = execute_message_passing_baseline(&compiled, 3);
1212 let calibration = report
1213 .calibration_report
1214 .expect("calibration report expected for degraded compilation");
1215
1216 assert!(calibration.degraded);
1217 assert!(calibration
1218 .caution_markers
1219 .iter()
1220 .any(|marker| marker == "degraded_export"));
1221 assert!(report
1222 .syndromes
1223 .iter()
1224 .any(|syndrome| syndrome.blocked_by_degradation));
1225 assert!(report.advisory_only);
1226 }
1227
1228 #[test]
1229 fn execution_emits_region_traces_and_convergence_governance() {
1230 let compiled = compiled_fixture();
1231 let report = execute_message_passing_baseline(&compiled, 3);
1232
1233 assert!(!report.region_traces.is_empty());
1234 assert_eq!(
1235 report.region_traces[0].region_id,
1236 RegionId::new("region:fixture")
1237 );
1238 assert_eq!(
1239 report
1240 .convergence_report
1241 .governance
1242 .residual_tolerance_micros,
1243 DEFAULT_FIXED_POINT_TOLERANCE_MICROS
1244 );
1245 assert_eq!(
1246 report.convergence_report.governance.escalation_rule,
1247 "emit_failure_artifact_on_nonconvergence"
1248 );
1249 }
1250}