Skip to main content

kernel_execution/
lib.rs

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    /// Returns the authority class for all execution reports emitted by this crate.
227    pub fn authority_class(&self) -> ArtifactAuthorityClass {
228        ArtifactAuthorityClass::NonAuthoritativeDerived
229    }
230}
231
232/// Executes one acyclic belief pass over the compiled graph.
233pub 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
250/// Executes bounded message passing across the full compiled graph.
251pub 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
263/// Recomputes only the regions affected by a delta and reports parity metadata.
264pub 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
304/// Runs residual-correction passes on top of the message-passing baseline.
305pub 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
367/// Chooses the execution lane that fits the supplied graph and budget.
368pub 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
880/// Computes the node set that must be recomputed for a changed-node delta.
881pub 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            &degraded_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}