Skip to main content

kernel_oracles/
lib.rs

1#![cfg_attr(test, allow(clippy::expect_used))]
2use chrono::{DateTime, FixedOffset};
3use constraint_compiler::{compile_batch, CompileOutput, CompilerPolicy};
4use forge_memory_bridge::ProjectionImportBatchV3;
5use kernel_execution::{execute_delta_propagation, execute_message_passing_baseline};
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use stack_ids::{ContentDigest, OracleSliceId, RegionId};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
11#[serde(rename_all = "snake_case")]
12pub enum OracleMode {
13    ExactBounded,
14    ConservativeFallback,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
18pub struct OracleSliceSelectionPolicy {
19    pub policy_name: String,
20    pub prefers_bounded_regions: bool,
21    pub max_exact_nodes: usize,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
25pub struct OracleAssessment {
26    pub slice_id: OracleSliceId,
27    pub mode: OracleMode,
28    pub supported: bool,
29    pub satisfied_constraint_count: usize,
30    pub selected_region_ids: Vec<RegionId>,
31    pub selection_policy: OracleSliceSelectionPolicy,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
35pub struct OracleParityCertificate {
36    pub slice_id: OracleSliceId,
37    pub region_ids: Vec<RegionId>,
38    pub parity_match: bool,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
42pub struct DeltaParityAssessment {
43    pub changed_node_ids: Vec<String>,
44    pub recomputed_node_ids: Vec<String>,
45    pub recomputed_region_ids: Vec<RegionId>,
46    pub parity_match: bool,
47    pub certificate: OracleParityCertificate,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
51pub struct TemporalReplaySnapshot {
52    pub recorded_at: String,
53    pub batch: ProjectionImportBatchV3,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
57pub struct TemporalReplayAssessment {
58    pub cutoff_recorded_at: String,
59    pub selected_recorded_at: String,
60    pub source_envelope_id: String,
61    pub graph_hash: ContentDigest,
62    pub matched_expected_hash: bool,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
66#[serde(rename_all = "snake_case")]
67pub enum RefutationMode {
68    CausalRefuter,
69    MinimalPerturbation,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
73#[serde(tag = "result", rename_all = "snake_case")]
74pub enum OracleRefutationOutcome {
75    FlipWitness { removed_node_ids: Vec<String> },
76    NoFlipFound { searched_budget: usize },
77    NotApplicable { reason: String },
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
81pub struct OracleRefutationResult {
82    pub mode: RefutationMode,
83    pub target_node_id: String,
84    pub baseline_supported: bool,
85    pub searched_budget: usize,
86    pub outcome: OracleRefutationOutcome,
87}
88
89const MAX_REMOVAL_SET_SIZE: usize = 4;
90const MAX_REMOVAL_COMBINATION_BUDGET: usize = 256;
91
92/// Returns an exact bounded-oracle assessment when the compiled graph supports one.
93pub fn evaluate_exact_bounded(compiled: &CompileOutput) -> Option<OracleAssessment> {
94    let slice = compiled.oracle_candidates.first()?;
95    let selected_region_ids = compiled
96        .regions
97        .iter()
98        .filter(|region| {
99            region
100                .node_ids
101                .iter()
102                .any(|node_id| slice.node_ids.contains(node_id))
103        })
104        .map(|region| region.region_id.clone())
105        .collect::<Vec<_>>();
106    Some(OracleAssessment {
107        slice_id: slice.oracle_slice_id.clone(),
108        mode: OracleMode::ExactBounded,
109        supported: compiled.degradations.is_empty() && compiled.nodes.len() <= 8,
110        satisfied_constraint_count: compiled.constraints.len(),
111        selected_region_ids,
112        selection_policy: OracleSliceSelectionPolicy {
113            policy_name: "bounded_region_exact_first".into(),
114            prefers_bounded_regions: true,
115            max_exact_nodes: 8,
116        },
117    })
118}
119
120/// Returns the conservative fallback oracle assessment for a compiled graph.
121pub fn evaluate_conservative(compiled: &CompileOutput) -> OracleAssessment {
122    let slice_id = compiled
123        .oracle_candidates
124        .first()
125        .map(|slice| slice.oracle_slice_id.clone())
126        .unwrap_or_else(|| OracleSliceId::new("oracle:unsupported"));
127    OracleAssessment {
128        slice_id,
129        mode: OracleMode::ConservativeFallback,
130        supported: true,
131        satisfied_constraint_count: compiled.constraints.len(),
132        selected_region_ids: compiled
133            .regions
134            .iter()
135            .map(|region| region.region_id.clone())
136            .collect(),
137        selection_policy: OracleSliceSelectionPolicy {
138            policy_name: "conservative_fallback".into(),
139            prefers_bounded_regions: true,
140            max_exact_nodes: 8,
141        },
142    }
143}
144
145/// Compares delta recomputation against a full bounded rerun over the affected focus set.
146pub fn evaluate_delta_parity(
147    compiled: &CompileOutput,
148    changed_node_ids: &[String],
149    max_iterations: u32,
150) -> DeltaParityAssessment {
151    let delta = execute_delta_propagation(compiled, changed_node_ids, max_iterations);
152    let bounded = execute_message_passing_baseline(compiled, max_iterations);
153    let recomputed_nodes = delta.recomputed_node_ids.clone();
154    let recomputed_region_ids = delta.recomputed_region_ids.clone();
155    let delta_focus = delta
156        .execution
157        .node_beliefs
158        .iter()
159        .filter(|belief| recomputed_nodes.contains(&belief.node_id))
160        .map(|belief| (belief.node_id.clone(), belief.belief_micros))
161        .collect::<Vec<_>>();
162    let bounded_focus = bounded
163        .node_beliefs
164        .iter()
165        .filter(|belief| recomputed_nodes.contains(&belief.node_id))
166        .map(|belief| (belief.node_id.clone(), belief.belief_micros))
167        .collect::<Vec<_>>();
168
169    DeltaParityAssessment {
170        changed_node_ids: changed_node_ids.to_vec(),
171        recomputed_node_ids: recomputed_nodes.clone(),
172        recomputed_region_ids: recomputed_region_ids.clone(),
173        parity_match: delta_focus == bounded_focus,
174        certificate: OracleParityCertificate {
175            slice_id: compiled
176                .oracle_candidates
177                .first()
178                .map(|slice| slice.oracle_slice_id.clone())
179                .unwrap_or_else(|| OracleSliceId::new("oracle:unsupported")),
180            region_ids: recomputed_region_ids,
181            parity_match: delta_focus == bounded_focus,
182        },
183    }
184}
185
186/// Recompiles the newest snapshot at or before the supplied cutoff time.
187pub fn replay_as_of(
188    snapshots: &[TemporalReplaySnapshot],
189    cutoff_recorded_at: &str,
190    policy: &CompilerPolicy,
191) -> Option<CompileOutput> {
192    let cutoff = parsed_utc_timestamp(cutoff_recorded_at)?;
193    let snapshot = snapshots
194        .iter()
195        .filter_map(|snapshot| {
196            Some((
197                parsed_utc_timestamp(&snapshot.recorded_at)?,
198                snapshot.recorded_at.as_str(),
199                snapshot,
200            ))
201        })
202        .filter(|(recorded_at, _, _)| *recorded_at <= cutoff)
203        .max_by_key(|(recorded_at, ..)| *recorded_at)
204        .map(|(_, _, snapshot)| snapshot)?;
205    Some(compile_batch(&snapshot.batch, policy))
206}
207
208/// Evaluates a temporal replay slice against an expected compiled graph hash.
209pub fn evaluate_temporal_replay(
210    snapshots: &[TemporalReplaySnapshot],
211    cutoff_recorded_at: &str,
212    policy: &CompilerPolicy,
213    expected_hash: &ContentDigest,
214) -> Option<TemporalReplayAssessment> {
215    let cutoff = parsed_utc_timestamp(cutoff_recorded_at)?;
216    let snapshot = snapshots
217        .iter()
218        .filter_map(|snapshot| {
219            Some((
220                parsed_utc_timestamp(&snapshot.recorded_at)?,
221                snapshot.recorded_at.as_str(),
222                snapshot,
223            ))
224        })
225        .filter(|(recorded_at, _, _)| *recorded_at <= cutoff)
226        .max_by_key(|(recorded_at, ..)| *recorded_at)
227        .map(|(_, _, snapshot)| snapshot)?;
228    let compiled = compile_batch(&snapshot.batch, policy);
229    Some(TemporalReplayAssessment {
230        cutoff_recorded_at: cutoff_recorded_at.to_string(),
231        selected_recorded_at: snapshot.recorded_at.clone(),
232        source_envelope_id: snapshot.batch.source_envelope_id.to_string(),
233        matched_expected_hash: &compiled.graph_hash == expected_hash,
234        graph_hash: compiled.graph_hash,
235    })
236}
237
238/// Searches for a causal refutation witness that flips support for the target node.
239pub fn evaluate_causal_refuter(
240    compiled: &CompileOutput,
241    target_node_id: &str,
242    max_removed_nodes: usize,
243) -> OracleRefutationResult {
244    refute_internal(
245        RefutationMode::CausalRefuter,
246        compiled,
247        target_node_id,
248        max_removed_nodes,
249    )
250}
251
252/// Searches for the smallest perturbation witness that flips support for the target node.
253pub fn minimal_perturbation_witness(
254    compiled: &CompileOutput,
255    target_node_id: &str,
256    max_removed_nodes: usize,
257) -> OracleRefutationResult {
258    refute_internal(
259        RefutationMode::MinimalPerturbation,
260        compiled,
261        target_node_id,
262        max_removed_nodes,
263    )
264}
265
266/// Evaluates the minimal-perturbation refuter mode for a target node.
267pub fn evaluate_minimal_perturbation(
268    compiled: &CompileOutput,
269    target_node_id: &str,
270    max_removed_nodes: usize,
271) -> OracleRefutationResult {
272    minimal_perturbation_witness(compiled, target_node_id, max_removed_nodes)
273}
274
275fn refute_internal(
276    mode: RefutationMode,
277    compiled: &CompileOutput,
278    target_node_id: &str,
279    max_removed_nodes: usize,
280) -> OracleRefutationResult {
281    let baseline_supported = node_is_supported(compiled, target_node_id, &[]);
282    if !baseline_supported {
283        return OracleRefutationResult {
284            mode,
285            target_node_id: target_node_id.to_string(),
286            baseline_supported,
287            searched_budget: 0,
288            outcome: OracleRefutationOutcome::NotApplicable {
289                reason: "target node is not supported in the compiled graph".into(),
290            },
291        };
292    }
293
294    let candidates = perturbation_candidates(compiled, target_node_id);
295    if candidates.is_empty() {
296        return OracleRefutationResult {
297            mode,
298            target_node_id: target_node_id.to_string(),
299            baseline_supported,
300            searched_budget: 0,
301            outcome: OracleRefutationOutcome::NotApplicable {
302                reason: "no admissible perturbation candidates are connected to the target".into(),
303            },
304        };
305    }
306
307    let mut searched_budget = 0usize;
308    let max_size = max_removed_nodes
309        .max(1)
310        .min(candidates.len())
311        .min(MAX_REMOVAL_SET_SIZE);
312
313    for size in 1..=max_size {
314        let (witness, budget_exhausted) = find_witness_by_size(
315            &candidates,
316            compiled,
317            target_node_id,
318            size,
319            MAX_REMOVAL_COMBINATION_BUDGET,
320            &mut searched_budget,
321        );
322        if let Some(removed_node_ids) = witness {
323            return OracleRefutationResult {
324                mode,
325                target_node_id: target_node_id.to_string(),
326                baseline_supported,
327                searched_budget,
328                outcome: OracleRefutationOutcome::FlipWitness { removed_node_ids },
329            };
330        }
331        if budget_exhausted {
332            return OracleRefutationResult {
333                mode,
334                target_node_id: target_node_id.to_string(),
335                baseline_supported,
336                searched_budget,
337                outcome: OracleRefutationOutcome::NoFlipFound { searched_budget },
338            };
339        }
340    }
341
342    OracleRefutationResult {
343        mode,
344        target_node_id: target_node_id.to_string(),
345        baseline_supported,
346        searched_budget,
347        outcome: OracleRefutationOutcome::NoFlipFound { searched_budget },
348    }
349}
350
351fn perturbation_candidates(compiled: &CompileOutput, target_node_id: &str) -> Vec<String> {
352    let mut candidates = compiled
353        .hyperedges
354        .iter()
355        .filter(|edge| {
356            edge.member_node_ids
357                .iter()
358                .any(|node| node == target_node_id)
359        })
360        .flat_map(|edge| edge.member_node_ids.iter())
361        .filter(|node_id| node_id.as_str() != target_node_id)
362        .filter(|node_id| !node_id.starts_with("nuisance:"))
363        .cloned()
364        .collect::<Vec<_>>();
365    candidates.sort();
366    candidates.dedup();
367    candidates
368}
369
370fn parsed_utc_timestamp(value: &str) -> Option<DateTime<FixedOffset>> {
371    DateTime::parse_from_rfc3339(value).ok()
372}
373
374fn node_is_supported(compiled: &CompileOutput, target_node_id: &str, removed: &[String]) -> bool {
375    compiled.hyperedges.iter().any(|edge| {
376        if !edge
377            .member_node_ids
378            .iter()
379            .any(|node| node == target_node_id)
380        {
381            return false;
382        }
383        edge.member_node_ids
384            .iter()
385            .filter(|node_id| node_id.as_str() != target_node_id)
386            .filter(|node_id| !removed.contains(node_id))
387            .any(|node_id| !node_id.starts_with("nuisance:"))
388    })
389}
390
391fn find_witness_by_size(
392    candidates: &[String],
393    compiled: &CompileOutput,
394    target_node_id: &str,
395    size: usize,
396    max_budget: usize,
397    searched_budget: &mut usize,
398) -> (Option<Vec<String>>, bool) {
399    let mut current = Vec::with_capacity(size);
400    search_combinations_for_witness(
401        candidates,
402        size,
403        0,
404        &mut current,
405        compiled,
406        target_node_id,
407        max_budget,
408        searched_budget,
409    )
410}
411
412#[allow(clippy::too_many_arguments)]
413fn search_combinations_for_witness(
414    items: &[String],
415    size: usize,
416    index: usize,
417    current: &mut Vec<String>,
418    compiled: &CompileOutput,
419    target_node_id: &str,
420    max_budget: usize,
421    searched_budget: &mut usize,
422) -> (Option<Vec<String>>, bool) {
423    if current.len() == size {
424        if *searched_budget >= max_budget {
425            return (None, true);
426        }
427
428        *searched_budget += 1;
429        if !node_is_supported(compiled, target_node_id, current) {
430            return (Some(current.clone()), false);
431        }
432        return (None, false);
433    }
434    if index >= items.len() {
435        return (None, false);
436    }
437
438    let mut idx = index;
439    while idx < items.len() {
440        current.push(items[idx].clone());
441        let (witness, budget_exhausted) = search_combinations_for_witness(
442            items,
443            size,
444            idx + 1,
445            current,
446            compiled,
447            target_node_id,
448            max_budget,
449            searched_budget,
450        );
451        current.pop();
452
453        if let Some(witness) = witness {
454            return (Some(witness), false);
455        }
456        if budget_exhausted {
457            return (None, true);
458        }
459
460        idx += 1;
461    }
462    (None, false)
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468    use constraint_compiler::{
469        CompilationBoundary, CompiledRegion, GraphGeometryManifest, GraphSurfaceKind,
470        InferenceHyperedge, InferenceNode,
471    };
472    use forge_memory_bridge::transform_envelope_v3;
473    use proptest::prelude::*;
474    use recursive_kernel_core::ConstraintUnit;
475    use semantic_memory_forge::{
476        ConstraintSeedKind, ExportAuthority, ExportClaim, ExportConfidenceClass, ExportEnvelopeV3,
477        ExportRecord, ExportRecordSemanticsV3, ExportRecordV3, ForgeExportMeta,
478        ProjectionVisibilityClass, EXPORT_ENVELOPE_V3_SCHEMA,
479    };
480    use stack_ids::{
481        AssertionGroupId, ClaimFamilyId, ConstraintId, EntityId, EnvelopeId, OperatorId,
482        RegionDigestId, RegionId, ScopeKey,
483    };
484
485    fn rich_batch(envelope_id: &str, claim_suffix: &str) -> ProjectionImportBatchV3 {
486        let scope = ScopeKey::namespace_only("oracle");
487        let records = vec![
488            ExportRecordV3 {
489                record: ExportRecord::Claim(ExportClaim {
490                    claim_id: None,
491                    claim_version_id: Some(format!("claim-version-{claim_suffix}-1").into()),
492                    subject_entity_id: EntityId::new(format!("entity-{claim_suffix}-1")),
493                    predicate: "supports".into(),
494                    object_anchor: serde_json::json!("result"),
495                    valid_from: None,
496                    valid_to: None,
497                    confidence: 1.0,
498                    content: "entity supports result".into(),
499                    projection_family: "forge_verification".into(),
500                    supersedes_claim_id: None,
501                    supersedes_claim_version_id: None,
502                    metadata: None,
503                }),
504                semantics: Some(ExportRecordSemanticsV3 {
505                    claim_family_id: Some(ClaimFamilyId::new(format!("family-{claim_suffix}"))),
506                    assertion_group_id: Some(AssertionGroupId::new(format!(
507                        "group-{claim_suffix}"
508                    ))),
509                    relation_group_id: None,
510                    joint_evidence_group_id: None,
511                    constraint_seed_kind: Some(ConstraintSeedKind::Hyperedge),
512                    treatment_hint: None,
513                    outcome_hint: None,
514                    confounder_hint: None,
515                    instrument_hint: None,
516                    effect_modifier_hint: None,
517                    contradiction_candidate_group_id: None,
518                    mutual_exclusion_group_id: None,
519                    comparability_snapshot_version: None,
520                    nuisance_snapshot: None,
521                    projection_visibility_class: ProjectionVisibilityClass::Standard,
522                    export_confidence_class: ExportConfidenceClass::Verified,
523                    derivation_seed_ids: vec![],
524                    review_priority_hint: None,
525                }),
526            },
527            ExportRecordV3 {
528                record: ExportRecord::Claim(ExportClaim {
529                    claim_id: None,
530                    claim_version_id: Some(format!("claim-version-{claim_suffix}-2").into()),
531                    subject_entity_id: EntityId::new(format!("entity-{claim_suffix}-2")),
532                    predicate: "supports".into(),
533                    object_anchor: serde_json::json!("result"),
534                    valid_from: None,
535                    valid_to: None,
536                    confidence: 0.95,
537                    content: "entity 2 supports result".into(),
538                    projection_family: "forge_verification".into(),
539                    supersedes_claim_id: None,
540                    supersedes_claim_version_id: None,
541                    metadata: None,
542                }),
543                semantics: Some(ExportRecordSemanticsV3 {
544                    claim_family_id: Some(ClaimFamilyId::new(format!("family-{claim_suffix}"))),
545                    assertion_group_id: Some(AssertionGroupId::new(format!(
546                        "group-{claim_suffix}"
547                    ))),
548                    relation_group_id: None,
549                    joint_evidence_group_id: None,
550                    constraint_seed_kind: Some(ConstraintSeedKind::Hyperedge),
551                    treatment_hint: None,
552                    outcome_hint: None,
553                    confounder_hint: None,
554                    instrument_hint: None,
555                    effect_modifier_hint: None,
556                    contradiction_candidate_group_id: None,
557                    mutual_exclusion_group_id: None,
558                    comparability_snapshot_version: None,
559                    nuisance_snapshot: None,
560                    projection_visibility_class: ProjectionVisibilityClass::Standard,
561                    export_confidence_class: ExportConfidenceClass::Verified,
562                    derivation_seed_ids: vec![],
563                    review_priority_hint: None,
564                }),
565            },
566        ];
567        let export_meta = ForgeExportMeta {
568            authority: ExportAuthority::Forge,
569            run_id: Some("run-1".into()),
570            direct_write: false,
571            comparability_snapshot_version: None,
572            exported_at: "2026-03-10T00:00:00Z".into(),
573        };
574        let digest =
575            ExportEnvelopeV3::compute_digest("forge", &scope, &records, Some(&export_meta), None)
576                .unwrap();
577        let envelope = ExportEnvelopeV3 {
578            envelope_id: EnvelopeId::new(envelope_id),
579            schema_version: EXPORT_ENVELOPE_V3_SCHEMA.into(),
580            content_digest: digest,
581            source_authority: "forge".into(),
582            scope_key: scope,
583            trace_ctx: None,
584            exported_at: "2026-03-10T00:00:00Z".into(),
585            export_meta: Some(export_meta),
586            evidence_bundle: None,
587            support_sets: vec![],
588            contradiction_witnesses: vec![],
589            retraction_records: vec![],
590            claim_states_v13: vec![],
591            intervention_bundles_v14: vec![],
592            outcome_schemas_v14: vec![],
593            cohort_contracts_v14: vec![],
594            counterfactual_slices_v14: vec![],
595            experiment_cases_v14: vec![],
596            comparability_matrices_v14: vec![],
597            decision_traces_v14: vec![],
598            refuter_suites_v14: vec![],
599            refuter_results_v14: vec![],
600            experiment_budgets_v14: vec![],
601            rollout_decisions_v14: vec![],
602            rollback_decisions_v14: vec![],
603            attestation_envelopes_v15: vec![],
604            trust_root_sets_v15: vec![],
605            artifact_admission_policies_v15: vec![],
606            transparency_receipts_v15: vec![],
607            attestation_revocations_v15: vec![],
608            attestation_supersessions_v15: vec![],
609            remote_oracle_leases_v15: vec![],
610            remote_slice_requests_v15: vec![],
611            remote_slice_results_v15: vec![],
612            cross_runtime_replay_tickets_v15: vec![],
613            dispute_bundles_v15: vec![],
614            disclosure_policies_v15: vec![],
615            disclosure_budgets_v15: vec![],
616            records,
617        };
618        transform_envelope_v3(&envelope).unwrap()
619    }
620
621    fn explicit_refute_fixture() -> CompileOutput {
622        CompileOutput {
623            graph_hash: ContentDigest::compute(b"oracle-causal-cost-fixture"),
624            scope_key: ScopeKey::namespace_only("oracle"),
625            geometry_manifest: GraphGeometryManifest {
626                surfaces: vec![
627                    GraphSurfaceKind::Storage,
628                    GraphSurfaceKind::Retrieval,
629                    GraphSurfaceKind::Inference,
630                    GraphSurfaceKind::Repair,
631                    GraphSurfaceKind::Control,
632                ],
633                compilation_boundaries: vec![CompilationBoundary {
634                    from_surface: GraphSurfaceKind::Inference,
635                    to_surface: GraphSurfaceKind::Repair,
636                    artifact_families: vec!["syndrome".into()],
637                    deterministic: true,
638                }],
639                no_silent_collapse: true,
640            },
641            nodes: vec![
642                InferenceNode {
643                    node_id: "target".into(),
644                    kind: "claim_version".into(),
645                },
646                InferenceNode {
647                    node_id: "peer-b".into(),
648                    kind: "claim_version".into(),
649                },
650                InferenceNode {
651                    node_id: "peer-c".into(),
652                    kind: "claim_version".into(),
653                },
654            ],
655            hyperedges: vec![InferenceHyperedge {
656                edge_id: "edge:triad".into(),
657                member_node_ids: vec!["target".into(), "peer-b".into(), "peer-c".into()],
658            }],
659            constraints: vec![ConstraintUnit {
660                constraint_id: ConstraintId::new("constraint:edge:triad"),
661                kind: "hyperedge".into(),
662                variable_ids: vec!["target".into(), "peer-b".into(), "peer-c".into()],
663                operator_id: OperatorId::new("kernel-oracles:tests"),
664            }],
665            regions: vec![CompiledRegion {
666                region_id: RegionId::new("region:oracle-triad"),
667                region_digest_id: RegionDigestId::new("region-digest:oracle-triad"),
668                node_ids: vec!["target".into(), "peer-b".into(), "peer-c".into()],
669                hyperedge_ids: vec!["edge:triad".into()],
670                constraint_ids: vec![ConstraintId::new("constraint:edge:triad")],
671                bounded_default_unit_of_work: true,
672            }],
673            invalidation_cones: vec![],
674            degradations: vec![],
675            oracle_candidates: vec![],
676        }
677    }
678
679    fn compiled_fixture() -> CompileOutput {
680        compile_batch(
681            &rich_batch("env-1", "alpha"),
682            &CompilerPolicy {
683                policy_version: "policy-1".into(),
684                include_hyperedges: true,
685            },
686        )
687    }
688
689    fn compile_many_claim_fixture(claim_count: usize) -> (CompileOutput, Vec<String>) {
690        let scope = ScopeKey::namespace_only("oracle");
691        let claims: Vec<String> = (0..claim_count)
692            .map(|index| format!("claim-large-{index}"))
693            .collect();
694        let records: Vec<ExportRecordV3> = claims
695            .iter()
696            .map(|claim_id| ExportRecordV3 {
697                record: ExportRecord::Claim(ExportClaim {
698                    claim_id: None,
699                    claim_version_id: Some(claim_id.clone().into()),
700                    subject_entity_id: EntityId::new(format!("entity-{claim_id}")),
701                    predicate: "supports".into(),
702                    object_anchor: serde_json::json!("result"),
703                    valid_from: None,
704                    valid_to: None,
705                    confidence: 1.0,
706                    content: format!("{claim_id} supports result"),
707                    projection_family: "forge_verification".into(),
708                    supersedes_claim_id: None,
709                    supersedes_claim_version_id: None,
710                    metadata: None,
711                }),
712                semantics: Some(ExportRecordSemanticsV3 {
713                    claim_family_id: Some(ClaimFamilyId::new("family-large")),
714                    assertion_group_id: Some(AssertionGroupId::new("group-large")),
715                    relation_group_id: None,
716                    joint_evidence_group_id: None,
717                    constraint_seed_kind: Some(ConstraintSeedKind::Hyperedge),
718                    treatment_hint: None,
719                    outcome_hint: None,
720                    confounder_hint: None,
721                    instrument_hint: None,
722                    effect_modifier_hint: None,
723                    contradiction_candidate_group_id: None,
724                    mutual_exclusion_group_id: None,
725                    comparability_snapshot_version: None,
726                    nuisance_snapshot: None,
727                    projection_visibility_class: ProjectionVisibilityClass::Standard,
728                    export_confidence_class: ExportConfidenceClass::Verified,
729                    derivation_seed_ids: vec![],
730                    review_priority_hint: None,
731                }),
732            })
733            .collect();
734        let export_meta = ForgeExportMeta {
735            authority: ExportAuthority::Forge,
736            run_id: Some("run-large".into()),
737            direct_write: false,
738            comparability_snapshot_version: None,
739            exported_at: "2026-03-10T00:00:00Z".into(),
740        };
741        let digest =
742            ExportEnvelopeV3::compute_digest("forge", &scope, &records, Some(&export_meta), None)
743                .unwrap();
744        let envelope = ExportEnvelopeV3 {
745            envelope_id: EnvelopeId::new("env-large"),
746            schema_version: EXPORT_ENVELOPE_V3_SCHEMA.into(),
747            content_digest: digest,
748            source_authority: "forge".into(),
749            scope_key: scope,
750            trace_ctx: None,
751            exported_at: "2026-03-10T00:00:00Z".into(),
752            export_meta: Some(export_meta),
753            evidence_bundle: None,
754            support_sets: vec![],
755            contradiction_witnesses: vec![],
756            retraction_records: vec![],
757            claim_states_v13: vec![],
758            intervention_bundles_v14: vec![],
759            outcome_schemas_v14: vec![],
760            cohort_contracts_v14: vec![],
761            counterfactual_slices_v14: vec![],
762            experiment_cases_v14: vec![],
763            comparability_matrices_v14: vec![],
764            decision_traces_v14: vec![],
765            refuter_suites_v14: vec![],
766            refuter_results_v14: vec![],
767            experiment_budgets_v14: vec![],
768            rollout_decisions_v14: vec![],
769            rollback_decisions_v14: vec![],
770            attestation_envelopes_v15: vec![],
771            trust_root_sets_v15: vec![],
772            artifact_admission_policies_v15: vec![],
773            transparency_receipts_v15: vec![],
774            attestation_revocations_v15: vec![],
775            attestation_supersessions_v15: vec![],
776            remote_oracle_leases_v15: vec![],
777            remote_slice_requests_v15: vec![],
778            remote_slice_results_v15: vec![],
779            cross_runtime_replay_tickets_v15: vec![],
780            dispute_bundles_v15: vec![],
781            disclosure_policies_v15: vec![],
782            disclosure_budgets_v15: vec![],
783            records,
784        };
785        let batch = transform_envelope_v3(&envelope).unwrap();
786        let compiled = compile_batch(
787            &batch,
788            &CompilerPolicy {
789                policy_version: "policy-1".into(),
790                include_hyperedges: true,
791            },
792        );
793        (compiled, claims)
794    }
795
796    #[test]
797    fn exact_bounded_path_exists_for_small_supported_slice() {
798        let compiled = compiled_fixture();
799        let exact = evaluate_exact_bounded(&compiled).expect("expected exact oracle slice");
800        let conservative = evaluate_conservative(&compiled);
801
802        assert!(exact.supported);
803        assert_eq!(
804            exact.satisfied_constraint_count,
805            conservative.satisfied_constraint_count
806        );
807        assert!(!compiled.oracle_candidates[0]
808            .node_ids
809            .iter()
810            .any(|node_id| node_id.starts_with("nuisance:")));
811        assert!(exact.selection_policy.prefers_bounded_regions);
812        assert!(!exact.selected_region_ids.is_empty());
813        assert_eq!(exact.selected_region_ids, conservative.selected_region_ids);
814    }
815
816    #[test]
817    fn temporal_replay_selects_latest_snapshot_before_cutoff() {
818        let policy = CompilerPolicy {
819            policy_version: "policy-1".into(),
820            include_hyperedges: true,
821        };
822        let older = rich_batch("env-old", "old");
823        let newer = rich_batch("env-new", "new");
824        let expected = compile_batch(&older, &policy);
825        let replay = evaluate_temporal_replay(
826            &[
827                TemporalReplaySnapshot {
828                    recorded_at: "2026-03-08T00:00:00Z".into(),
829                    batch: older,
830                },
831                TemporalReplaySnapshot {
832                    recorded_at: "2026-03-10T00:00:00Z".into(),
833                    batch: newer,
834                },
835            ],
836            "2026-03-09T00:00:00Z",
837            &policy,
838            &expected.graph_hash,
839        )
840        .expect("replay result expected");
841
842        assert!(replay.matched_expected_hash);
843        assert_eq!(replay.selected_recorded_at, "2026-03-08T00:00:00Z");
844        assert_eq!(replay.source_envelope_id, "env-old");
845    }
846
847    #[test]
848    fn temporal_replay_handles_timezone_offsets_without_string_ordering_bugs() {
849        let policy = CompilerPolicy {
850            policy_version: "policy-1".into(),
851            include_hyperedges: true,
852        };
853        let earlier = rich_batch("env-offset-earlier", "older");
854        let newer = rich_batch("env-offset-newer", "newer");
855        let expected = compile_batch(&newer, &policy);
856        let replay = evaluate_temporal_replay(
857            &[
858                TemporalReplaySnapshot {
859                    recorded_at: "2026-03-08T15:00:00-05:00".into(),
860                    batch: earlier,
861                },
862                TemporalReplaySnapshot {
863                    recorded_at: "2026-03-08T21:00:00+02:00".into(),
864                    batch: newer,
865                },
866            ],
867            "2026-03-08T19:30:00+00:00",
868            &policy,
869            &expected.graph_hash,
870        )
871        .expect("replay result expected");
872
873        assert_eq!(replay.selected_recorded_at, "2026-03-08T21:00:00+02:00");
874    }
875
876    #[test]
877    fn temporal_replay_returns_none_for_malformed_cutoff_timestamp() {
878        let policy = CompilerPolicy {
879            policy_version: "policy-1".into(),
880            include_hyperedges: true,
881        };
882        let replay = replay_as_of(
883            &[TemporalReplaySnapshot {
884                recorded_at: "2026-03-08T00:00:00Z".into(),
885                batch: rich_batch("env-old", "old"),
886            }],
887            "not-a-real-timestamp",
888            &policy,
889        );
890
891        assert!(replay.is_none());
892    }
893
894    #[test]
895    fn temporal_replay_returns_none_for_malformed_snapshot_timestamp() {
896        let policy = CompilerPolicy {
897            policy_version: "policy-1".into(),
898            include_hyperedges: true,
899        };
900        let replay = replay_as_of(
901            &[TemporalReplaySnapshot {
902                recorded_at: "not-a-real-timestamp".into(),
903                batch: rich_batch("env-old", "old"),
904            }],
905            "2026-03-09T00:00:00Z",
906            &policy,
907        );
908
909        assert!(replay.is_none());
910    }
911
912    #[test]
913    fn causal_refuter_emits_flip_witness_for_single_peer_removal() {
914        let compiled = compiled_fixture();
915        let result = evaluate_causal_refuter(&compiled, "claim-version-alpha-1", 1);
916
917        assert_eq!(result.mode, RefutationMode::CausalRefuter);
918        assert!(matches!(
919            result.outcome,
920            OracleRefutationOutcome::FlipWitness { .. }
921        ));
922    }
923
924    #[test]
925    fn minimal_perturbation_search_defaults_to_one_step_budget() {
926        let compiled = compiled_fixture();
927        let result = minimal_perturbation_witness(&compiled, "claim-version-alpha-1", 0);
928
929        assert!(matches!(
930            result.outcome,
931            OracleRefutationOutcome::FlipWitness { .. }
932        ));
933        assert_eq!(result.searched_budget, 1);
934    }
935
936    #[test]
937    fn causal_refuter_budget_is_exhausted_before_flip_is_possible() {
938        let (compiled, claims) = compile_many_claim_fixture(11);
939        let target = &claims[0];
940        let result = evaluate_causal_refuter(&compiled, target, 10);
941
942        match result.outcome {
943            OracleRefutationOutcome::NoFlipFound { searched_budget } => {
944                assert_eq!(
945                    searched_budget, MAX_REMOVAL_COMBINATION_BUDGET,
946                    "combinatorial search should stop at bounded budget"
947                );
948            }
949            other => unreachable!("OracleRefutationOutcome::NoFlipFound expected, got {other:?}"),
950        }
951        assert_eq!(
952            result.mode,
953            RefutationMode::CausalRefuter,
954            "mode should remain causal refuter"
955        );
956        assert!(result.searched_budget >= MAX_REMOVAL_COMBINATION_BUDGET);
957    }
958
959    #[test]
960    fn causal_refuter_reports_actual_witness_search_budget() {
961        let compiled = explicit_refute_fixture();
962        let result = evaluate_causal_refuter(&compiled, "target", 2);
963
964        match result.outcome {
965            OracleRefutationOutcome::FlipWitness { removed_node_ids } => {
966                assert_eq!(
967                    removed_node_ids.len(),
968                    2,
969                    "expected two-node witness to break support"
970                );
971            }
972            other => unreachable!("OracleRefutationOutcome::FlipWitness expected, got {other:?}"),
973        }
974        assert_eq!(result.searched_budget, 3);
975    }
976
977    #[test]
978    fn minimal_perturbation_preserves_small_graph_flip_witness() {
979        let compiled = compiled_fixture();
980        let result = minimal_perturbation_witness(&compiled, "claim-version-alpha-1", 4);
981
982        assert!(matches!(
983            result.outcome,
984            OracleRefutationOutcome::FlipWitness { .. }
985        ));
986        assert_eq!(result.searched_budget, 1);
987    }
988
989    #[test]
990    fn delta_parity_matches_full_recompute_on_recomputed_nodes() {
991        let compiled = compiled_fixture();
992        let parity = evaluate_delta_parity(&compiled, &["claim-version-alpha-2".into()], 4);
993
994        assert!(parity.parity_match);
995        assert!(!parity.recomputed_node_ids.is_empty());
996        assert!(!parity.recomputed_region_ids.is_empty());
997        assert!(parity.certificate.parity_match);
998    }
999
1000    proptest! {
1001        #[test]
1002        fn malformed_rfc3339_timestamps_are_rejected(value in ".{1,64}") {
1003            prop_assume!(chrono::DateTime::parse_from_rfc3339(&value).is_err());
1004            prop_assert!(parsed_utc_timestamp(&value).is_none());
1005        }
1006    }
1007}