Skip to main content

kernel_conformance/
lib.rs

1#![cfg_attr(test, allow(deprecated))]
2#![cfg_attr(test, allow(clippy::expect_used))]
3
4pub mod reference_interpreters;
5
6/// Returns the baseline phase-1 conformance gate identifiers.
7pub fn phase_1_gates() -> &'static [&'static str] {
8    &["CF-A1", "CF-C1", "CF-C2", "CF-O1", "CF-R1"]
9}
10
11/// Returns the baseline phase-2 conformance gate identifiers.
12pub fn phase_2_gates() -> &'static [&'static str] {
13    &["CF-A3", "CF-C3", "CF-C4", "CF-O2"]
14}
15
16/// Returns the phase-3-and-beyond conformance gate identifiers.
17pub fn phase_3_plus_gates() -> &'static [&'static str] {
18    &[
19        "CF-O3", "CF-O4", "CF-R2", "CF-R3", "CF-R4", "CF-B1", "CF-B2", "CF-B3", "CF-P1", "CF-P2",
20    ]
21}
22
23/// Returns the v16-v20 bounded-surface conformance gate identifiers.
24pub fn v16_v20_gates() -> &'static [&'static str] {
25    &[
26        "FED-001", "FED-004", "THY-001", "POR-001", "GOV-001", "SFX-001",
27    ]
28}
29
30/// Returns the v9 constitutional freeze conformance gate identifiers.
31///
32/// These gates enforce the v9 finish-line invariants:
33/// - CF-V9-C001: Canonical bundle round-trip freeze
34/// - CF-V9-C002: Execution evidence artifact freeze
35/// - CF-V9-C004: Schema governance enforcement
36/// - CF-V9-C005: Bridge atomicity proof
37/// - CF-V9-C008: Repair law hardening
38/// - CF-V9-C009: Runtime provenance completeness
39/// - CF-V9-C017: Cross-crate conformance harness
40pub fn v9_constitutional_gates() -> &'static [&'static str] {
41    &[
42        "CF-V9-C001",
43        "CF-V9-C002",
44        "CF-V9-C004",
45        "CF-V9-C005",
46        "CF-V9-C008",
47        "CF-V9-C009",
48        "CF-V9-C017",
49    ]
50}
51
52#[cfg(test)]
53mod tests {
54    use constraint_compiler::{
55        compile_batch, CompilerPolicy, ConstraintDegradation, GraphSurfaceKind,
56    };
57    use forge_memory_bridge::{
58        transform_envelope_v2, transform_envelope_v3, BridgeImportFailureArtifact,
59        ImportProjectionRecord,
60    };
61    use kernel_execution::{
62        affected_nodes_for_delta, execute_delta_propagation, execute_message_passing_baseline,
63        execute_residual_correction, schedule_execution, ExecutionBudget, ExecutionStopReason,
64        WorkloadClass,
65    };
66    use kernel_oracles::{
67        evaluate_causal_refuter, evaluate_conservative, evaluate_exact_bounded,
68        evaluate_temporal_replay, minimal_perturbation_witness, OracleRefutationOutcome,
69        TemporalReplaySnapshot,
70    };
71    use knowledge_runtime::{
72        adapters::semantic_memory::SemanticMemoryAdapter, config::ProjectionConfig,
73        KnowledgeRuntime, RuntimeConfig, Scope,
74    };
75    use recursive_kernel_core::{ArtifactAuthorityClass, KernelRun};
76    use semantic_memory::{MemoryConfig, MemoryStore, MockEmbedder, ProjectionQuery};
77    use semantic_memory_forge::{
78        ConstraintSeedKind, ExportAuthority, ExportClaim, ExportConfidenceClass,
79        ExportEnvelopeError, ExportEnvelopeV2, ExportEnvelopeV3, ExportRecord,
80        ExportRecordSemanticsV3, ExportRecordV3, ForgeExportMeta, NuisanceSnapshot,
81        ProjectionVisibilityClass, EXPORT_ENVELOPE_V2_SCHEMA, EXPORT_ENVELOPE_V3_SCHEMA,
82    };
83    use stack_ids::{
84        AssertionGroupId, ClaimFamilyId, ClaimId, ClaimVersionId, ContentDigest, EntityId,
85        EnvelopeId, KernelRunId, OperatorId, ScopeKey,
86    };
87    use std::{convert::TryFrom, future::Future, path::PathBuf};
88    use tempfile::TempDir;
89    use verification_calibration::NuisanceStateArtifact;
90    use verification_control::{RegionArtifactKind, RegionArtifactTransport, SyndromeRouteRecord};
91
92    fn block_on<F: Future>(future: F) -> F::Output {
93        tokio::runtime::Runtime::new().unwrap().block_on(future)
94    }
95
96    fn make_batch(
97        namespace: &str,
98        claim_ids: &[&str],
99        thin: bool,
100        nuisance: bool,
101    ) -> forge_memory_bridge::ProjectionImportBatchV3 {
102        let scope = ScopeKey::namespace_only(namespace);
103        let records = claim_ids
104            .iter()
105            .enumerate()
106            .map(|(index, claim_id)| ExportRecordV3 {
107                record: ExportRecord::Claim(ExportClaim {
108                    claim_id: None,
109                    claim_version_id: Some((*claim_id).into()),
110                    subject_entity_id: EntityId::new(format!("entity-{index}")),
111                    predicate: "supports".into(),
112                    object_anchor: serde_json::json!("result"),
113                    valid_from: None,
114                    valid_to: None,
115                    confidence: 1.0,
116                    content: format!("{claim_id} supports result"),
117                    projection_family: "forge_verification".into(),
118                    supersedes_claim_id: None,
119                    supersedes_claim_version_id: None,
120                    metadata: None,
121                }),
122                semantics: (!thin).then(|| ExportRecordSemanticsV3 {
123                    claim_family_id: Some(ClaimFamilyId::new("family-1")),
124                    assertion_group_id: Some(AssertionGroupId::new("group-1")),
125                    relation_group_id: None,
126                    joint_evidence_group_id: None,
127                    constraint_seed_kind: Some(ConstraintSeedKind::Hyperedge),
128                    treatment_hint: None,
129                    outcome_hint: None,
130                    confounder_hint: None,
131                    instrument_hint: None,
132                    effect_modifier_hint: None,
133                    contradiction_candidate_group_id: None,
134                    mutual_exclusion_group_id: None,
135                    comparability_snapshot_version: nuisance.then(|| "cmp-1".into()),
136                    nuisance_snapshot: nuisance.then(|| NuisanceSnapshot {
137                        environment_fingerprint: Some("linux".into()),
138                        toolchain_version: Some("rust-1.85".into()),
139                        dependency_set_hash: Some("deps-1".into()),
140                        scope_mismatch_markers: vec![],
141                        measurement_notes: vec!["measurement drift".into()],
142                        selection_bias_markers: vec!["selection bias".into()],
143                    }),
144                    projection_visibility_class: ProjectionVisibilityClass::Standard,
145                    export_confidence_class: ExportConfidenceClass::Verified,
146                    derivation_seed_ids: vec![],
147                    review_priority_hint: None,
148                }),
149            })
150            .collect::<Vec<_>>();
151        let export_meta = ForgeExportMeta {
152            authority: ExportAuthority::Forge,
153            run_id: Some("run-1".into()),
154            direct_write: false,
155            comparability_snapshot_version: nuisance.then(|| "cmp-1".into()),
156            exported_at: "2026-03-10T00:00:00Z".into(),
157        };
158        let digest =
159            ExportEnvelopeV3::compute_digest("forge", &scope, &records, Some(&export_meta), None)
160                .unwrap();
161        let envelope = ExportEnvelopeV3 {
162            envelope_id: EnvelopeId::new(format!("env-{namespace}")),
163            schema_version: EXPORT_ENVELOPE_V3_SCHEMA.into(),
164            content_digest: digest,
165            source_authority: "forge".into(),
166            scope_key: scope,
167            trace_ctx: None,
168            exported_at: "2026-03-10T00:00:00Z".into(),
169            export_meta: Some(export_meta),
170            evidence_bundle: None,
171            support_sets: vec![],
172            contradiction_witnesses: vec![],
173            retraction_records: vec![],
174            claim_states_v13: vec![],
175            intervention_bundles_v14: vec![],
176            outcome_schemas_v14: vec![],
177            cohort_contracts_v14: vec![],
178            counterfactual_slices_v14: vec![],
179            experiment_cases_v14: vec![],
180            comparability_matrices_v14: vec![],
181            decision_traces_v14: vec![],
182            refuter_suites_v14: vec![],
183            refuter_results_v14: vec![],
184            experiment_budgets_v14: vec![],
185            rollout_decisions_v14: vec![],
186            rollback_decisions_v14: vec![],
187            attestation_envelopes_v15: vec![],
188            trust_root_sets_v15: vec![],
189            artifact_admission_policies_v15: vec![],
190            transparency_receipts_v15: vec![],
191            attestation_revocations_v15: vec![],
192            attestation_supersessions_v15: vec![],
193            remote_oracle_leases_v15: vec![],
194            remote_slice_requests_v15: vec![],
195            remote_slice_results_v15: vec![],
196            cross_runtime_replay_tickets_v15: vec![],
197            dispute_bundles_v15: vec![],
198            disclosure_policies_v15: vec![],
199            disclosure_budgets_v15: vec![],
200            records,
201        };
202        transform_envelope_v3(&envelope).unwrap()
203    }
204
205    fn make_v2_envelope(namespace: &str, claim_ids: &[&str]) -> ExportEnvelopeV2 {
206        let scope = ScopeKey::namespace_only(namespace);
207        let records = claim_ids
208            .iter()
209            .enumerate()
210            .map(|(index, claim_version_id)| {
211                ExportRecord::Claim(ExportClaim {
212                    claim_id: Some(ClaimId::new(format!("claim-{index}"))),
213                    claim_version_id: Some(ClaimVersionId::new(*claim_version_id)),
214                    subject_entity_id: EntityId::new(format!("entity-{index}")),
215                    predicate: "supports".into(),
216                    object_anchor: serde_json::json!("result"),
217                    valid_from: None,
218                    valid_to: None,
219                    confidence: 1.0,
220                    content: format!("{claim_version_id} supports result"),
221                    projection_family: "forge_verification".into(),
222                    supersedes_claim_id: None,
223                    supersedes_claim_version_id: None,
224                    metadata: None,
225                })
226            })
227            .collect::<Vec<_>>();
228        let export_meta = ForgeExportMeta {
229            authority: ExportAuthority::Forge,
230            run_id: Some("run-v2".into()),
231            direct_write: false,
232            comparability_snapshot_version: Some("cmp-v2".into()),
233            exported_at: "2026-03-10T00:00:00Z".into(),
234        };
235        let digest =
236            ExportEnvelopeV2::compute_digest("forge", &scope, &records, Some(&export_meta), None)
237                .unwrap();
238
239        ExportEnvelopeV2 {
240            envelope_id: EnvelopeId::new(format!("env-v2-{namespace}")),
241            schema_version: EXPORT_ENVELOPE_V2_SCHEMA.into(),
242            content_digest: digest,
243            source_authority: "forge".into(),
244            scope_key: scope,
245            trace_ctx: None,
246            exported_at: "2026-03-10T00:00:00Z".into(),
247            export_meta: Some(export_meta),
248            evidence_bundle: None,
249            records,
250        }
251    }
252
253    fn compile_fixture(
254        namespace: &str,
255        claim_ids: &[&str],
256        thin: bool,
257        nuisance: bool,
258    ) -> constraint_compiler::CompileOutput {
259        compile_batch(
260            &make_batch(namespace, claim_ids, thin, nuisance),
261            &CompilerPolicy {
262                policy_version: "policy-1".into(),
263                include_hyperedges: true,
264            },
265        )
266    }
267
268    fn open_runtime(
269        batch: &forge_memory_bridge::ProjectionImportBatchV3,
270        namespace: &str,
271    ) -> KnowledgeRuntime {
272        let dir = TempDir::new().unwrap();
273        let base_dir = dir.path().to_path_buf();
274        std::mem::forget(dir);
275        let config = MemoryConfig {
276            base_dir,
277            ..Default::default()
278        };
279        let embedder = Box::new(MockEmbedder::new(config.embedding.dimensions));
280        let store = MemoryStore::open_with_embedder(config, embedder).unwrap();
281        block_on(store.import_projection_batch(batch)).unwrap();
282        let adapter = SemanticMemoryAdapter::new(store);
283        KnowledgeRuntime::new(
284            RuntimeConfig {
285                default_scope: Scope::new(namespace),
286                query: Default::default(),
287                entity: Default::default(),
288                projection: ProjectionConfig {
289                    staleness_threshold_secs: 3600,
290                    import_staleness_threshold_secs: 0,
291                    persist: false,
292                },
293                strict_temporal: false,
294                strict_scope: false,
295            },
296            adapter,
297        )
298        .unwrap()
299    }
300
301    fn open_store() -> MemoryStore {
302        let dir = TempDir::new().unwrap();
303        let base_dir = dir.path().to_path_buf();
304        std::mem::forget(dir);
305        open_store_with_path(base_dir)
306    }
307
308    fn open_store_with_path(base_dir: PathBuf) -> MemoryStore {
309        let config = MemoryConfig {
310            base_dir,
311            ..Default::default()
312        };
313        let embedder = Box::new(MockEmbedder::new(config.embedding.dimensions));
314        MemoryStore::open_with_embedder(config, embedder).unwrap()
315    }
316
317    #[test]
318    fn cf_a1_kernel_artifacts_are_non_authoritative() {
319        let run = KernelRun {
320            run_id: KernelRunId::new("run-1"),
321            policy_version: "policy-1".into(),
322            scope_key: ScopeKey::namespace_only("ns"),
323            operator_ids: vec![OperatorId::new(
324                recursive_kernel_core::CONSTRAINT_COMPILER_OPERATOR_ID,
325            )],
326            oracle_slice_id: None,
327            degradation_active: false,
328        };
329
330        assert_eq!(
331            run.authority_class(),
332            ArtifactAuthorityClass::NonAuthoritativeDerived
333        );
334    }
335
336    #[test]
337    fn cf_a3_advisory_outputs_do_not_self_promote() {
338        let runtime = open_runtime(
339            &make_batch("cf-a3", &["claim-a", "claim-b", "claim-c"], false, false),
340            "cf-a3",
341        );
342        let advisory = block_on(runtime.latest_inference_advisory(Some(&Scope::new("cf-a3"))))
343            .unwrap()
344            .expect("advisory expected");
345        let gate = block_on(runtime.latest_risk_gate(Some(&Scope::new("cf-a3"))))
346            .unwrap()
347            .expect("gate expected");
348
349        assert!(advisory.advisory_only);
350        assert!(gate.advisory_only);
351        assert_ne!(gate.status, "authoritative");
352    }
353
354    #[test]
355    fn cf_c1_deterministic_compile() {
356        let a = compile_fixture("cf-c1", &["claim-a", "claim-b"], false, false);
357        let b = compile_fixture("cf-c1", &["claim-a", "claim-b"], false, false);
358        assert_eq!(a.graph_hash, b.graph_hash);
359    }
360
361    #[test]
362    fn cf_c2_thin_export_degrades_explicitly() {
363        let compiled = compile_fixture("cf-c2", &["claim-a"], true, false);
364        assert!(compiled
365            .degradations
366            .contains(&ConstraintDegradation::ThinExport));
367    }
368
369    #[test]
370    fn cf_c2_mixed_semantics_do_not_hallucinate_hyperedge_membership() {
371        let mut batch = make_batch("cf-c2-mixed", &["claim-a", "claim-b"], false, false);
372        batch.records[0].semantics = None;
373        let compiled = compile_batch(
374            &batch,
375            &CompilerPolicy {
376                policy_version: "policy-1".into(),
377                include_hyperedges: true,
378            },
379        );
380
381        assert!(compiled
382            .degradations
383            .contains(&ConstraintDegradation::ThinExport));
384        assert!(compiled.oracle_candidates.is_empty());
385
386        let group_hyperedge = compiled
387            .hyperedges
388            .iter()
389            .find(|edge| edge.edge_id == "assertion_group:group-1")
390            .expect("thin export should keep only explicitly provided grouping");
391        assert_eq!(group_hyperedge.member_node_ids, vec!["claim-b".to_string()]);
392    }
393
394    #[test]
395    fn cf_c2a_thin_export_forces_conservative_oracle_mode() {
396        let compiled = compile_fixture("cf-c2a", &["claim-a"], true, false);
397
398        assert!(compiled
399            .degradations
400            .contains(&ConstraintDegradation::ThinExport));
401        assert!(compiled.oracle_candidates.is_empty());
402        assert!(evaluate_exact_bounded(&compiled).is_none());
403        let conservative = evaluate_conservative(&compiled);
404        assert!(conservative.supported);
405        assert_eq!(
406            conservative.mode,
407            kernel_oracles::OracleMode::ConservativeFallback
408        );
409    }
410
411    #[test]
412    fn cf_c3_nuisance_state_is_included() {
413        let compiled = compile_fixture("cf-c3", &["claim-a", "claim-b"], false, true);
414        assert!(compiled
415            .nodes
416            .iter()
417            .any(|node| node.kind == "nuisance_state"));
418    }
419
420    #[test]
421    fn cf_c4_hyperedge_correctness_preserves_group_membership() {
422        let compiled = compile_fixture("cf-c4", &["claim-a", "claim-b"], false, false);
423        let hyperedge = compiled
424            .hyperedges
425            .iter()
426            .find(|edge| edge.edge_id == "assertion_group:group-1")
427            .expect("group hyperedge expected");
428        assert_eq!(hyperedge.member_node_ids.len(), 2);
429    }
430
431    #[test]
432    fn cf_g1_v2_to_v3_upgrade_preserves_bridgeable_projection_content() {
433        let v2 = make_v2_envelope("cf-g1", &["claim-g1-a", "claim-g1-b"]);
434        let v3 = ExportEnvelopeV3::try_from(v2.clone()).unwrap();
435        let v2_batch = transform_envelope_v2(&v2).unwrap();
436        let v3_batch = transform_envelope_v3(&v3).unwrap();
437        let v3_records = v3_batch
438            .records
439            .iter()
440            .map(|record| record.record.clone())
441            .collect::<Vec<_>>();
442
443        assert_eq!(v2_batch.source_envelope_id, v3_batch.source_envelope_id);
444        assert_eq!(v2_batch.scope_key, v3_batch.scope_key);
445        assert_eq!(v2_batch.source_authority, v3_batch.source_authority);
446        assert_eq!(v2_batch.records.len(), v3_batch.records.len());
447        assert_eq!(
448            serde_json::to_value(&v2_batch.records).unwrap(),
449            serde_json::to_value(&v3_records).unwrap()
450        );
451    }
452
453    #[test]
454    fn cf_g4_imported_projection_data_survives_drop_and_reopen() {
455        let dir = TempDir::new().unwrap();
456        let base_dir = dir.path().to_path_buf();
457        std::mem::forget(dir);
458
459        let batch = make_batch("cf-g4", &["claim-g4-a", "claim-g4-b"], false, false);
460        let imported_count = {
461            let store = open_store_with_path(base_dir.clone());
462            let imported = block_on(store.import_projection_batch(&batch)).unwrap();
463            assert_eq!(imported.status, "complete");
464            let claims = block_on(
465                store.query_claim_versions(ProjectionQuery::new(ScopeKey::namespace_only("cf-g4"))),
466            )
467            .unwrap();
468            assert!(
469                claims
470                    .iter()
471                    .any(|claim| claim.claim_version_id == ClaimVersionId::new("claim-g4-a")),
472                "imported data should be queryable before drop"
473            );
474            claims.len()
475        };
476
477        let reopened_count = {
478            let store = open_store_with_path(base_dir);
479            let claims = block_on(
480                store.query_claim_versions(ProjectionQuery::new(ScopeKey::namespace_only("cf-g4"))),
481            )
482            .unwrap();
483            claims.len()
484        };
485
486        assert_eq!(reopened_count, imported_count);
487    }
488
489    #[test]
490    fn cf_g2_invalid_upgrade_fails_explicitly() {
491        let mut v2 = make_v2_envelope("cf-g2", &["claim-g2-a"]);
492        v2.content_digest = ContentDigest::compute(b"tampered");
493
494        let err = ExportEnvelopeV3::try_from(v2).unwrap_err();
495        assert!(matches!(err, ExportEnvelopeError::DigestMismatch { .. }));
496    }
497
498    #[test]
499    fn cf_g3_retry_import_of_upgraded_batch_is_idempotent() {
500        let v2 = make_v2_envelope("cf-g3", &["claim-g3-a"]);
501        let v3 = ExportEnvelopeV3::try_from(v2).unwrap();
502        let batch = transform_envelope_v3(&v3).unwrap();
503        let store = open_store();
504
505        let first = block_on(store.import_projection_batch(&batch)).unwrap();
506        let second = block_on(store.import_projection_batch(&batch)).unwrap();
507        let claims = block_on(
508            store.query_claim_versions(ProjectionQuery::new(ScopeKey::namespace_only("cf-g3"))),
509        )
510        .unwrap();
511
512        assert_eq!(first.status, "complete");
513        assert_eq!(second.status, "already_imported");
514        assert_eq!(claims.len(), 1);
515        assert!(matches!(
516            batch.records.first().map(|record| &record.record),
517            Some(ImportProjectionRecord::ClaimVersion(_))
518        ));
519    }
520
521    #[test]
522    fn cf_o1_oracle_parity_on_supported_slice() {
523        let compiled = compile_fixture("cf-o1", &["claim-a", "claim-b"], false, false);
524        let exact = evaluate_exact_bounded(&compiled).expect("exact oracle slice expected");
525        let conservative = evaluate_conservative(&compiled);
526        assert_eq!(
527            exact.satisfied_constraint_count,
528            conservative.satisfied_constraint_count
529        );
530    }
531
532    #[test]
533    fn cf_o1_exact_oracle_mode_is_reproducible() {
534        let compiled = compile_fixture("cf-o1-repro", &["claim-a", "claim-b"], false, false);
535        let first = evaluate_exact_bounded(&compiled).expect("exact oracle slice expected");
536        let second = evaluate_exact_bounded(&compiled).expect("exact oracle slice expected");
537
538        assert_eq!(first, second);
539    }
540
541    #[test]
542    fn cf_o2_delta_parity_matches_bounded_recompute() {
543        let compiled = compile_fixture("cf-o2", &["claim-a", "claim-b"], false, false);
544        let delta = execute_delta_propagation(&compiled, &["claim-a".into()], 3);
545        let full = execute_message_passing_baseline(&compiled, 3);
546        let affected = affected_nodes_for_delta(&compiled.invalidation_cones, &["claim-a".into()]);
547
548        for node_id in affected {
549            let delta_belief = delta
550                .execution
551                .node_beliefs
552                .iter()
553                .find(|belief| belief.node_id == node_id)
554                .expect("delta belief expected")
555                .belief_micros;
556            let full_belief = full
557                .node_beliefs
558                .iter()
559                .find(|belief| belief.node_id == node_id)
560                .expect("full belief expected")
561                .belief_micros;
562            assert_eq!(delta_belief, full_belief);
563        }
564        assert!(!delta.recomputed_region_ids.is_empty());
565        assert!(!delta.region_traces.is_empty());
566        assert!(!delta.invalidation_manifest.explicit_global_rebuild);
567    }
568
569    #[test]
570    fn cf_v10_geometry_and_regions_are_explicit() {
571        let compiled = compile_fixture("cf-v10-geometry", &["claim-a", "claim-b"], false, false);
572
573        assert_eq!(
574            compiled.geometry_manifest.surfaces,
575            vec![
576                GraphSurfaceKind::Storage,
577                GraphSurfaceKind::Retrieval,
578                GraphSurfaceKind::Inference,
579                GraphSurfaceKind::Repair,
580                GraphSurfaceKind::Control,
581            ]
582        );
583        assert!(compiled.geometry_manifest.no_silent_collapse);
584        assert!(compiled
585            .regions
586            .iter()
587            .all(|region| region.bounded_default_unit_of_work));
588    }
589
590    #[test]
591    fn cf_o3_temporal_replay_matches_expected_snapshot() {
592        let older = make_batch("cf-o3-old", &["claim-old-a", "claim-old-b"], false, false);
593        let newer = make_batch("cf-o3-new", &["claim-new-a", "claim-new-b"], false, false);
594        let policy = CompilerPolicy {
595            policy_version: "policy-1".into(),
596            include_hyperedges: true,
597        };
598        let expected = compile_batch(&older, &policy);
599        let replay = evaluate_temporal_replay(
600            &[
601                TemporalReplaySnapshot {
602                    recorded_at: "2026-03-08T00:00:00Z".into(),
603                    batch: older,
604                },
605                TemporalReplaySnapshot {
606                    recorded_at: "2026-03-10T00:00:00Z".into(),
607                    batch: newer,
608                },
609            ],
610            "2026-03-09T00:00:00Z",
611            &policy,
612            &expected.graph_hash,
613        )
614        .expect("replay result expected");
615
616        assert!(replay.matched_expected_hash);
617    }
618
619    #[test]
620    fn cf_o4_refuter_integration_emits_structured_result() {
621        let compiled = compile_fixture("cf-o4", &["claim-a", "claim-b"], false, false);
622        let result = evaluate_causal_refuter(&compiled, "claim-a", 1);
623        assert!(matches!(
624            result.outcome,
625            OracleRefutationOutcome::FlipWitness { .. }
626                | OracleRefutationOutcome::NoFlipFound { .. }
627                | OracleRefutationOutcome::NotApplicable { .. }
628        ));
629    }
630
631    #[test]
632    fn cf_r1_operator_requires_explicit_stop_rule() {
633        let operator = recursive_kernel_core::constraint_compiler_operator();
634        operator.validate().unwrap();
635    }
636
637    #[test]
638    fn cf_r2_instability_terminates_explicitly() {
639        let compiled = compile_fixture("cf-r2", &["claim-a", "claim-b"], false, true);
640        let report = execute_message_passing_baseline(&compiled, 1);
641        assert_eq!(report.stop_reason, ExecutionStopReason::MaxIterations);
642    }
643
644    #[test]
645    fn cf_r3_residual_repair_is_monotone() {
646        let compiled = compile_fixture("cf-r3", &["claim-a", "claim-b"], false, false);
647        let report = execute_residual_correction(&compiled, 3);
648        let mut previous = u64::MAX;
649        for sample in &report.residuals {
650            assert!(sample.total_residual_micros <= previous);
651            previous = sample.total_residual_micros;
652        }
653        assert!(report.convergence_report.residual_monotone_nonincreasing);
654    }
655
656    #[test]
657    fn cf_r4_syndromes_are_reproducible() {
658        let compiled = compile_fixture("cf-r4", &["claim-a"], true, false);
659        let a = execute_message_passing_baseline(&compiled, 2);
660        let b = execute_message_passing_baseline(&compiled, 2);
661        assert_eq!(a.syndromes, b.syndromes);
662    }
663
664    #[test]
665    fn cf_b1_minimal_perturbation_returns_structured_witness() {
666        let compiled = compile_fixture("cf-b1", &["claim-a", "claim-b"], false, false);
667        let result = minimal_perturbation_witness(&compiled, "claim-a", 1);
668        assert!(matches!(
669            result.outcome,
670            OracleRefutationOutcome::FlipWitness { .. }
671                | OracleRefutationOutcome::NoFlipFound { .. }
672        ));
673    }
674
675    #[test]
676    fn cf_b2_witness_and_certificate_emission_exist() {
677        let compiled = compile_fixture("cf-b2", &["claim-a", "claim-b", "claim-c"], false, false);
678        let scheduled = schedule_execution(&compiled, &ExecutionBudget::default());
679        assert!(!scheduled.execution.witnesses.is_empty());
680        assert!(!scheduled.execution.certificates.is_empty());
681    }
682
683    #[test]
684    fn cf_b3_calibration_disclosure_surfaces_on_nuisance_runs() {
685        let runtime = open_runtime(
686            &make_batch("cf-b3", &["claim-a", "claim-b"], false, true),
687            "cf-b3",
688        );
689        let explanation =
690            block_on(runtime.latest_inference_explanation(Some(&Scope::new("cf-b3"))))
691                .unwrap()
692                .expect("explanation expected");
693
694        assert!(explanation
695            .calibration_caveats
696            .iter()
697            .any(|caveat| caveat.contains("nuisance state")));
698    }
699
700    #[test]
701    fn cf_v10_syndrome_transport_and_nuisance_artifacts_are_typed() {
702        let compiled = compile_fixture("cf-v10-artifacts", &["claim-a", "claim-b"], false, true);
703        let report = execute_message_passing_baseline(&compiled, 2);
704        let first_region = compiled.regions.first().expect("region expected");
705        let first_syndrome = report.syndromes.first().expect("syndrome expected");
706
707        let route = SyndromeRouteRecord::new(
708            first_syndrome.syndrome_id.clone(),
709            first_region.region_id.clone(),
710            first_region.node_ids.clone(),
711            vec!["witness:claim-a".into()],
712        );
713        let transport_payload = serde_json::to_string(first_syndrome).unwrap();
714        let transport = RegionArtifactTransport::new(
715            RegionArtifactKind::Syndrome,
716            first_syndrome.syndrome_id.to_string(),
717            transport_payload.as_bytes(),
718            first_region.region_id.clone(),
719            first_region.region_id.clone(),
720            "inference",
721            "repair",
722            "replay:syndrome-route",
723        );
724        let nuisance = NuisanceStateArtifact::new(
725            "2026-03-13T00:00:00Z",
726            Some(first_region.region_id.clone()),
727            "provider_instability",
728            vec!["retry_storm".into(), "comparability_shift".into()],
729            true,
730            vec!["kernel_execution".into(), "verification_control".into()],
731        );
732
733        assert!(route.routed_before_global_invalidation);
734        assert_eq!(transport.from_surface, "inference");
735        assert!(nuisance.semantic_change_confounded);
736    }
737
738    #[test]
739    fn cf_p1_workload_classes_remain_separate() {
740        let small = schedule_execution(
741            &compile_fixture("cf-p1-small", &["claim-a", "claim-b"], false, false),
742            &ExecutionBudget::default(),
743        );
744        let large = schedule_execution(
745            &compile_fixture(
746                "cf-p1-large",
747                &[
748                    "claim-a", "claim-b", "claim-c", "claim-d", "claim-e", "claim-f", "claim-g",
749                    "claim-h", "claim-i",
750                ],
751                false,
752                false,
753            ),
754            &ExecutionBudget {
755                max_nodes: 4,
756                ..ExecutionBudget::default()
757            },
758        );
759
760        assert_eq!(small.workload_class, WorkloadClass::Background);
761        assert_eq!(large.workload_class, WorkloadClass::Heavy);
762    }
763
764    #[test]
765    fn cf_p2_budgeted_degradation_is_explicit() {
766        let scheduled = schedule_execution(
767            &compile_fixture(
768                "cf-p2",
769                &["claim-a", "claim-b", "claim-c", "claim-d", "claim-e"],
770                false,
771                false,
772            ),
773            &ExecutionBudget {
774                max_nodes: 2,
775                ..ExecutionBudget::default()
776            },
777        );
778        assert_eq!(
779            scheduled.execution.stop_reason,
780            ExecutionStopReason::BudgetExhausted
781        );
782        assert_eq!(
783            scheduled.degraded_reason.as_deref(),
784            Some("budget_exhausted")
785        );
786    }
787
788    // ═══════════════════════════════════════════════════════════════
789    // v9 Constitutional Freeze and Conformance Hardening
790    // LIB-C001, LIB-C002, LIB-C005, LIB-C008, LIB-C009, LIB-C017
791    // ═══════════════════════════════════════════════════════════════
792
793    // ─── LIB-C001: Canonical bundle round-trip freeze ─────────────
794
795    #[test]
796    fn cf_v9_c001_episode_bundle_round_trips() {
797        // LIB-C001: EpisodeBundleV1 must round-trip through serde without data loss.
798        use semantic_memory_forge::{
799            DispatchOutcomeV1, EpisodeBundleV1, ExecutionContextV1, VerificationSummary,
800            EPISODE_BUNDLE_V1_SCHEMA, EXECUTION_CONTEXT_V1_SCHEMA,
801        };
802        use stack_ids::{EpisodeId, TraceCtx};
803
804        let trace = TraceCtx::generate();
805        let ctx = ExecutionContextV1 {
806            schema_version: EXECUTION_CONTEXT_V1_SCHEMA.into(),
807            trace_ctx: trace.clone(),
808            attempt_id: Some(stack_ids::AttemptId::new("attempt-c001")),
809            trial_id: Some(stack_ids::TrialId::new("trial-c001")),
810            replay_link: Some("replay://link".into()),
811            workload_class: Some("forge_export".into()),
812            queue_hops: vec!["hop-1".into(), "hop-2".into()],
813            deadline: Some("2026-04-01T00:00:00Z".into()),
814            cost_budget_units: Some(1000),
815            degradation_markers: vec!["marker-a".into()],
816            dispatch_outcome: DispatchOutcomeV1::Succeeded,
817            environment_fingerprint: Some("linux-amd64".into()),
818            provider_route: Some("forge".into()),
819            cancellation_reason: None,
820            tool_receipt_ref: Some("receipt://ref".into()),
821            provider_call_ref: Some("call://ref".into()),
822            replay_parent_ref: Some("parent://ref".into()),
823        };
824        let bundle = EpisodeBundleV1 {
825            schema_version: EPISODE_BUNDLE_V1_SCHEMA.into(),
826            bundle_id: "bundle-c001".into(),
827            episode_id: EpisodeId::new("ep-c001"),
828            primary_document_id: "doc-c001".into(),
829            namespace: "ns-c001".into(),
830            scope_key: ScopeKey::namespace_only("ns-c001"),
831            valid_from: Some("2026-01-01T00:00:00Z".into()),
832            valid_to: Some("2026-12-31T23:59:59Z".into()),
833            exported_at: "2026-03-15T00:00:00Z".into(),
834            recorded_at: Some("2026-03-15T00:00:01Z".into()),
835            source_envelope_id: EnvelopeId::new("env-c001"),
836            content_digest: ContentDigest::compute(b"c001-content"),
837            source_evidence_pointers: vec!["evidence://ptr-1".into()],
838            source_receipt_digests: vec!["digest-1".into()],
839            claim_version_ids: vec!["claim-v1".into(), "claim-v2".into()],
840            relation_version_ids: vec!["rel-v1".into()],
841            verification_summary: Some(VerificationSummary {
842                lifecycle_state: semantic_memory_forge::VerificationLifecycleState::Verified,
843                promotion_state: semantic_memory_forge::PromotionState::Promoted {
844                    version_id: Some("v1".into()),
845                    promoted_at: Some("2026-03-15T00:00:00Z".into()),
846                },
847                completed_trial_count: 3,
848                passed_refutation_count: 2,
849                failed_refutation_count: 0,
850                comparability_snapshot_version: None,
851                notes: vec!["clean".into()],
852            }),
853            refutation_artifact_ids: vec!["refutation-1".into()],
854            control_plane_refs: vec!["forge_run:run-c001".into()],
855            execution_context: ctx,
856            thin_export: false,
857            supersedes_bundle_id: Some("bundle-prev".into()),
858            evidence_bundle_id: Some("ev-bundle-1".into()),
859        };
860
861        let serialized = serde_json::to_string(&bundle).unwrap();
862        let deserialized: EpisodeBundleV1 = serde_json::from_str(&serialized).unwrap();
863
864        assert_eq!(deserialized.bundle_id, bundle.bundle_id);
865        assert_eq!(deserialized.episode_id, bundle.episode_id);
866        assert_eq!(deserialized.claim_version_ids.len(), 2);
867        assert_eq!(deserialized.relation_version_ids.len(), 1);
868        assert!(deserialized.verification_summary.is_some());
869        assert_eq!(
870            deserialized.execution_context.attempt_id,
871            bundle.execution_context.attempt_id
872        );
873        assert_eq!(
874            deserialized.execution_context.queue_hops.len(),
875            bundle.execution_context.queue_hops.len()
876        );
877        assert_eq!(
878            deserialized.supersedes_bundle_id,
879            bundle.supersedes_bundle_id
880        );
881        assert_eq!(deserialized.evidence_bundle_id, bundle.evidence_bundle_id);
882    }
883
884    #[test]
885    fn cf_v9_c001_execution_context_round_trips() {
886        // LIB-C001/C002: ExecutionContextV1 must round-trip without data loss.
887        use semantic_memory_forge::{
888            DispatchOutcomeV1, ExecutionContextV1, EXECUTION_CONTEXT_V1_SCHEMA,
889        };
890        use stack_ids::TraceCtx;
891
892        let ctx = ExecutionContextV1 {
893            schema_version: EXECUTION_CONTEXT_V1_SCHEMA.into(),
894            trace_ctx: TraceCtx::generate(),
895            attempt_id: Some(stack_ids::AttemptId::new("a-1")),
896            trial_id: Some(stack_ids::TrialId::new("t-1")),
897            replay_link: Some("replay://x".into()),
898            workload_class: Some("heavy".into()),
899            queue_hops: vec!["q1".into(), "q2".into(), "q3".into()],
900            deadline: Some("2026-04-01T12:00:00Z".into()),
901            cost_budget_units: Some(500),
902            degradation_markers: vec!["thin_export".into(), "missing_nuisance".into()],
903            dispatch_outcome: DispatchOutcomeV1::Degraded,
904            environment_fingerprint: Some("env-fp".into()),
905            provider_route: Some("openrouter".into()),
906            cancellation_reason: None,
907            tool_receipt_ref: Some("receipt://1".into()),
908            provider_call_ref: Some("call://1".into()),
909            replay_parent_ref: Some("parent://1".into()),
910        };
911
912        let json = serde_json::to_value(&ctx).unwrap();
913        let restored: ExecutionContextV1 = serde_json::from_value(json).unwrap();
914
915        assert_eq!(restored.attempt_id, ctx.attempt_id);
916        assert_eq!(restored.trial_id, ctx.trial_id);
917        assert_eq!(restored.queue_hops.len(), 3);
918        assert_eq!(restored.degradation_markers.len(), 2);
919        assert_eq!(restored.dispatch_outcome, DispatchOutcomeV1::Degraded);
920        assert_eq!(restored.tool_receipt_ref, ctx.tool_receipt_ref);
921        assert_eq!(restored.provider_call_ref, ctx.provider_call_ref);
922        assert_eq!(restored.replay_parent_ref, ctx.replay_parent_ref);
923    }
924
925    // ─── LIB-C005: Bridge atomicity proof conformance ─────────────
926
927    #[test]
928    fn cf_v9_c005_tampered_digest_produces_typed_error() {
929        // LIB-C005: Bridge must produce a typed, replayable error (not a log message)
930        // when digest verification fails during import.
931        let mut envelope = make_v2_envelope("cf-c005-tamper", &["claim-c005"]);
932        envelope.content_digest = ContentDigest::compute(b"tampered-data");
933
934        let err = transform_envelope_v2(&envelope).unwrap_err();
935        assert!(
936            matches!(err, forge_memory_bridge::BridgeError::DigestMismatch { .. }),
937            "LIB-C005: tampered digest must produce BridgeError::DigestMismatch, got: {err:?}"
938        );
939        // Error kind must be stable and machine-readable.
940        assert_eq!(err.kind(), "digest_mismatch");
941    }
942
943    #[test]
944    fn cf_v9_c005_v3_tampered_digest_produces_typed_error() {
945        // LIB-C005: Same atomicity proof for V3 path.
946        let scope = ScopeKey::namespace_only("c005-v3");
947        let records = vec![ExportRecordV3 {
948            record: ExportRecord::Claim(ExportClaim {
949                claim_id: None,
950                claim_version_id: Some(ClaimVersionId::new("claim-c005-v3")),
951                subject_entity_id: EntityId::new("entity-c005"),
952                predicate: "supports".into(),
953                object_anchor: serde_json::json!("result"),
954                valid_from: None,
955                valid_to: None,
956                confidence: 1.0,
957                content: "claim supports result".into(),
958                projection_family: "forge_verification".into(),
959                supersedes_claim_id: None,
960                supersedes_claim_version_id: None,
961                metadata: None,
962            }),
963            semantics: None,
964        }];
965        let envelope = ExportEnvelopeV3 {
966            envelope_id: EnvelopeId::new("env-c005-v3"),
967            schema_version: EXPORT_ENVELOPE_V3_SCHEMA.into(),
968            content_digest: ContentDigest::compute(b"wrong-digest"),
969            source_authority: "forge".into(),
970            scope_key: scope,
971            trace_ctx: None,
972            exported_at: "2026-04-01T00:00:00Z".into(),
973            export_meta: None,
974            evidence_bundle: None,
975            support_sets: vec![],
976            contradiction_witnesses: vec![],
977            retraction_records: vec![],
978            claim_states_v13: vec![],
979            intervention_bundles_v14: vec![],
980            outcome_schemas_v14: vec![],
981            cohort_contracts_v14: vec![],
982            counterfactual_slices_v14: vec![],
983            experiment_cases_v14: vec![],
984            comparability_matrices_v14: vec![],
985            decision_traces_v14: vec![],
986            refuter_suites_v14: vec![],
987            refuter_results_v14: vec![],
988            experiment_budgets_v14: vec![],
989            rollout_decisions_v14: vec![],
990            rollback_decisions_v14: vec![],
991            attestation_envelopes_v15: vec![],
992            trust_root_sets_v15: vec![],
993            artifact_admission_policies_v15: vec![],
994            transparency_receipts_v15: vec![],
995            attestation_revocations_v15: vec![],
996            attestation_supersessions_v15: vec![],
997            remote_oracle_leases_v15: vec![],
998            remote_slice_requests_v15: vec![],
999            remote_slice_results_v15: vec![],
1000            cross_runtime_replay_tickets_v15: vec![],
1001            dispute_bundles_v15: vec![],
1002            disclosure_policies_v15: vec![],
1003            disclosure_budgets_v15: vec![],
1004            records,
1005        };
1006
1007        let err = transform_envelope_v3(&envelope).unwrap_err();
1008        assert!(
1009            matches!(err, forge_memory_bridge::BridgeError::DigestMismatch { .. }),
1010            "LIB-C005: V3 tampered digest must produce BridgeError::DigestMismatch, got: {err:?}"
1011        );
1012        assert_eq!(err.kind(), "digest_mismatch");
1013    }
1014
1015    #[test]
1016    fn cf_v9_c005_bridge_preserves_digest_through_v3_transform() {
1017        // LIB-C005: The bridge must preserve the source envelope digest into the batch.
1018        let batch = make_batch(
1019            "c005-digest",
1020            &["claim-c005-a", "claim-c005-b"],
1021            false,
1022            false,
1023        );
1024        // The digest in the batch must match the source envelope's computed digest.
1025        assert!(
1026            !batch.content_digest.0.is_empty(),
1027            "LIB-C005: content_digest must be carried through from source envelope"
1028        );
1029        // Verify it's actually the Blake3 format (hex, 64 chars).
1030        assert_eq!(
1031            batch.content_digest.0.len(),
1032            64,
1033            "LIB-C005: content_digest must be a 64-char hex string (BLAKE3)"
1034        );
1035    }
1036
1037    #[test]
1038    fn cf_v9_c005_bridge_error_kinds_are_stable() {
1039        // LIB-C005: All bridge error kinds must be stable machine-readable strings.
1040        use forge_memory_bridge::BridgeError;
1041        let cases: Vec<BridgeError> = vec![
1042            BridgeError::InvalidEnvelope {
1043                reason: "test".into(),
1044            },
1045            BridgeError::IncompatibleVersion {
1046                expected: "a".into(),
1047                actual: "b".into(),
1048            },
1049            BridgeError::DigestMismatch {
1050                expected: "a".into(),
1051                actual: "b".into(),
1052            },
1053            BridgeError::DigestComputationFailed {
1054                reason: "test".into(),
1055            },
1056            BridgeError::InvalidRecord {
1057                reason: "test".into(),
1058            },
1059            BridgeError::TransformFailed {
1060                reason: "test".into(),
1061            },
1062            BridgeError::MissingEpisodeIdentity {
1063                record_context: "test".into(),
1064            },
1065        ];
1066        let expected_kinds = [
1067            "invalid_envelope",
1068            "incompatible_version",
1069            "digest_mismatch",
1070            "digest_computation_failed",
1071            "invalid_record",
1072            "transform_failed",
1073            "missing_episode_identity",
1074        ];
1075        for (error, expected_kind) in cases.iter().zip(expected_kinds.iter()) {
1076            assert_eq!(
1077                error.kind(),
1078                *expected_kind,
1079                "LIB-C005: BridgeError kind must be stable"
1080            );
1081        }
1082    }
1083
1084    #[test]
1085    fn cf_v9_c005_bridge_failure_produces_replayable_artifact() {
1086        // LIB-C005: Bridge failures must produce typed, replayable artifacts.
1087        let mut envelope = make_v2_envelope("cf-c005-artifact", &["claim-c005"]);
1088        envelope.content_digest = ContentDigest::compute(b"tampered");
1089
1090        let err = transform_envelope_v2(&envelope).unwrap_err();
1091        let artifact = BridgeImportFailureArtifact::from_error(
1092            &err,
1093            envelope.envelope_id.as_str(),
1094            &envelope.source_authority,
1095            &envelope.scope_key.namespace,
1096        );
1097
1098        assert_eq!(
1099            artifact.schema_version,
1100            forge_memory_bridge::BRIDGE_IMPORT_FAILURE_ARTIFACT_V1_SCHEMA
1101        );
1102        assert_eq!(artifact.error_kind, "digest_mismatch");
1103        assert!(!artifact.error_message.is_empty());
1104        assert_eq!(artifact.source_envelope_id, envelope.envelope_id.as_str());
1105        assert!(!artifact.failed_at.is_empty());
1106
1107        // Must round-trip through serde.
1108        let json = serde_json::to_value(&artifact).unwrap();
1109        let restored: BridgeImportFailureArtifact = serde_json::from_value(json).unwrap();
1110        assert_eq!(restored.error_kind, artifact.error_kind);
1111        assert_eq!(restored.source_envelope_id, artifact.source_envelope_id);
1112    }
1113
1114    // ─── LIB-C008: Repair law conformance ─────────────────────────
1115
1116    #[test]
1117    fn cf_v9_c008_repair_record_has_required_fields() {
1118        // LIB-C008: Every RepairRecordV1 must carry blast_radius, reversibility,
1119        // execution_context, repair_class, and an unchanged_statement.
1120        use forge_pilot::{RepairClassV1, RepairRecordV1};
1121        use semantic_memory_forge::{
1122            DispatchOutcomeV1, ExecutionContextV1, EXECUTION_CONTEXT_V1_SCHEMA,
1123        };
1124        use stack_ids::TraceCtx;
1125
1126        let record = RepairRecordV1 {
1127            schema_version: "repair_record_v1".into(),
1128            repair_record_id: "repair-c008".into(),
1129            affected_identities: vec!["claim-1".into(), "claim-2".into()],
1130            repair_class: RepairClassV1::SupersessionRepair,
1131            trigger_artifacts: vec!["contradiction-witness-1".into()],
1132            blast_radius: "scoped:ns-c008".into(),
1133            reversibility: "reversible_scoped".into(),
1134            action: "supersede claim-1 with claim-2".into(),
1135            execution_context: ExecutionContextV1 {
1136                schema_version: EXECUTION_CONTEXT_V1_SCHEMA.into(),
1137                trace_ctx: TraceCtx::generate(),
1138                attempt_id: None,
1139                trial_id: None,
1140                replay_link: None,
1141                workload_class: Some("repair".into()),
1142                queue_hops: vec![],
1143                deadline: None,
1144                cost_budget_units: None,
1145                degradation_markers: vec![],
1146                dispatch_outcome: DispatchOutcomeV1::Succeeded,
1147                environment_fingerprint: None,
1148                provider_route: None,
1149                cancellation_reason: None,
1150                tool_receipt_ref: None,
1151                provider_call_ref: None,
1152                replay_parent_ref: None,
1153            },
1154            opened_at: "2026-04-01T00:00:00Z".into(),
1155            resolved_at: Some("2026-04-01T00:00:01Z".into()),
1156            unchanged_statement: "no semantic widening applied; supersession only".into(),
1157        };
1158
1159        // Required fields must be non-empty.
1160        assert!(
1161            !record.blast_radius.is_empty(),
1162            "LIB-C008: blast_radius required"
1163        );
1164        assert!(
1165            !record.reversibility.is_empty(),
1166            "LIB-C008: reversibility required"
1167        );
1168        assert!(
1169            !record.unchanged_statement.is_empty(),
1170            "LIB-C008: unchanged_statement required"
1171        );
1172        assert!(!record.action.is_empty(), "LIB-C008: action required");
1173        assert_eq!(
1174            record.execution_context.schema_version, EXECUTION_CONTEXT_V1_SCHEMA,
1175            "LIB-C008: repair record must carry a valid execution context"
1176        );
1177
1178        // Round-trip through serde.
1179        let json = serde_json::to_value(&record).unwrap();
1180        let restored: RepairRecordV1 = serde_json::from_value(json).unwrap();
1181        assert_eq!(restored.repair_record_id, record.repair_record_id);
1182        assert_eq!(restored.repair_class, record.repair_class);
1183        assert_eq!(restored.blast_radius, record.blast_radius);
1184        assert_eq!(restored.reversibility, record.reversibility);
1185        assert_eq!(restored.unchanged_statement, record.unchanged_statement);
1186        assert_eq!(restored.affected_identities.len(), 2);
1187    }
1188
1189    #[test]
1190    fn cf_v9_c008_repair_classes_are_exhaustive() {
1191        // LIB-C008: Repair classes must cover the canonical set.
1192        use forge_pilot::RepairClassV1;
1193
1194        let classes = [
1195            RepairClassV1::IdentityRepair,
1196            RepairClassV1::TemporalRepair,
1197            RepairClassV1::SupersessionRepair,
1198            RepairClassV1::RollbackRepair,
1199            RepairClassV1::BundleImportRepair,
1200            RepairClassV1::ScopeWideningRepair,
1201            RepairClassV1::VerificationStateRepair,
1202        ];
1203        // Each class must serialize to a distinct stable string.
1204        let mut seen = std::collections::HashSet::new();
1205        for class in &classes {
1206            let wire = serde_json::to_value(class).unwrap();
1207            let label = wire.as_str().unwrap().to_string();
1208            assert!(
1209                seen.insert(label.clone()),
1210                "LIB-C008: duplicate repair class wire label: {label}"
1211            );
1212        }
1213        assert_eq!(
1214            seen.len(),
1215            7,
1216            "LIB-C008: expected 7 distinct repair classes"
1217        );
1218    }
1219
1220    // ─── LIB-C009: Runtime provenance completeness ─────────────────
1221
1222    #[test]
1223    fn cf_v9_c009_query_trace_carries_view_model_and_widening() {
1224        // LIB-C009: Every query result must carry declared view model and widening status.
1225        let runtime = open_runtime(
1226            &make_batch("cf-c009", &["claim-a", "claim-b"], false, false),
1227            "cf-c009",
1228        );
1229        let (_results, trace) =
1230            block_on(runtime.query("claim-a supports result", Some(&Scope::new("cf-c009"))))
1231                .unwrap();
1232
1233        // View model declarations must be present.
1234        // For a non-temporal, non-entity query, executed_views should be populated.
1235        // Even if empty (no degradation), the field itself is well-defined.
1236        assert!(
1237            !trace.requested_views.is_empty()
1238                || !trace.executed_views.is_empty()
1239                || !trace.plan.legs.is_empty(),
1240            "LIB-C009: query trace must declare requested/executed views or have at least one leg"
1241        );
1242
1243        // Widening disclosures must be a well-typed list (even if empty = no widening).
1244        // This asserts the field is structurally present, not that widening happened.
1245        let _ = &trace.widenings;
1246
1247        // Temporal mode must be explicitly declared.
1248        // For non-temporal queries, this is typically "none".
1249        assert!(
1250            trace.temporal_mode.is_some() || !trace.has_temporal_downgrade(),
1251            "LIB-C009: temporal_mode must be explicitly set or temporal downgrade absent"
1252        );
1253
1254        // The provenance snapshot must be constructible.
1255        let provenance = trace.runtime_query_provenance();
1256        assert_eq!(
1257            provenance.schema_version, "runtime_query_provenance_v1",
1258            "LIB-C009: provenance schema version must be runtime_query_provenance_v1"
1259        );
1260        assert!(!provenance.scope.namespace.is_empty());
1261        assert!(!provenance.classification_mode.is_empty());
1262    }
1263
1264    #[test]
1265    fn cf_v9_c009_runtime_query_provenance_round_trips() {
1266        // LIB-C009: RuntimeQueryProvenanceV1 must round-trip through serde.
1267        use knowledge_runtime::RuntimeQueryProvenanceV1;
1268
1269        let runtime = open_runtime(
1270            &make_batch("cf-c009-rt", &["claim-a"], false, false),
1271            "cf-c009-rt",
1272        );
1273        let (_results, trace) =
1274            block_on(runtime.query("claim-a supports result", Some(&Scope::new("cf-c009-rt"))))
1275                .unwrap();
1276        let provenance = trace.runtime_query_provenance();
1277
1278        let json = serde_json::to_value(&provenance).unwrap();
1279        let restored: RuntimeQueryProvenanceV1 = serde_json::from_value(json).unwrap();
1280
1281        assert_eq!(restored.schema_version, provenance.schema_version);
1282        assert_eq!(restored.scope, provenance.scope);
1283        assert_eq!(restored.classification_mode, provenance.classification_mode);
1284        assert_eq!(restored.raw_result_count, provenance.raw_result_count);
1285        assert_eq!(restored.merged_result_count, provenance.merged_result_count);
1286        assert_eq!(restored.total_duration_ms, provenance.total_duration_ms);
1287    }
1288
1289    #[test]
1290    fn cf_v9_c009_degraded_query_surfaces_widening() {
1291        // LIB-C009: When the runtime degrades a temporal query, it must surface
1292        // an explicit widening disclosure in the provenance record.
1293        let runtime = open_runtime(
1294            &make_batch("cf-c009-deg", &["claim-a", "claim-b"], false, false),
1295            "cf-c009-deg",
1296        );
1297        // Query with temporal expression that will be degraded.
1298        let (_results, trace) = block_on(runtime.query(
1299            "what happened last week with claim-a",
1300            Some(&Scope::new("cf-c009-deg")),
1301        ))
1302        .unwrap();
1303
1304        if trace.has_temporal_downgrade() {
1305            // If temporal was downgraded, widenings must contain a disclosure.
1306            assert!(
1307                !trace.widenings.is_empty(),
1308                "LIB-C009: temporal downgrade must produce an explicit widening disclosure"
1309            );
1310            let provenance = trace.runtime_query_provenance();
1311            assert!(
1312                !provenance.widenings.is_empty(),
1313                "LIB-C009: provenance must carry widening disclosures"
1314            );
1315        }
1316        // Whether degraded or not, warnings must be present and typed.
1317        // (A non-temporal path may not degrade, which is also valid.)
1318    }
1319
1320    // ─── LIB-C017: Cross-crate conformance gate registry ─────────
1321
1322    #[test]
1323    fn cf_v9_c017_conformance_gate_registry_is_exhaustive() {
1324        // LIB-C017: Every conformance gate from phase 1-3 and v16-v20 must be
1325        // accounted for in the gate registry.
1326        let all_gates: Vec<&str> = [
1327            super::phase_1_gates(),
1328            super::phase_2_gates(),
1329            super::phase_3_plus_gates(),
1330            super::v16_v20_gates(),
1331        ]
1332        .into_iter()
1333        .flat_map(|gates| gates.iter().copied())
1334        .collect();
1335
1336        // Must have no duplicates.
1337        let mut seen = std::collections::HashSet::new();
1338        for gate in &all_gates {
1339            assert!(
1340                seen.insert(*gate),
1341                "LIB-C017: duplicate conformance gate: {gate}"
1342            );
1343        }
1344
1345        // Smoke check: minimum expected gate count across all phases.
1346        assert!(
1347            all_gates.len() >= 19,
1348            "LIB-C017: expected at least 19 conformance gates, got {}",
1349            all_gates.len()
1350        );
1351    }
1352
1353    #[test]
1354    fn cf_v9_c017_v3_envelope_digest_is_deterministic() {
1355        // LIB-C017: Two identical envelopes must produce the same content digest.
1356        let batch_a = make_batch("c017-det", &["claim-a", "claim-b"], false, false);
1357        let batch_b = make_batch("c017-det", &["claim-a", "claim-b"], false, false);
1358        assert_eq!(
1359            batch_a.content_digest, batch_b.content_digest,
1360            "LIB-C017: identical envelopes must produce identical digests"
1361        );
1362    }
1363
1364    #[test]
1365    fn cf_v9_c017_bridge_transform_preserves_execution_context() {
1366        // LIB-C017: The V3 bridge must preserve execution context through transform.
1367        let batch = make_batch("c017-ctx", &["claim-a"], false, false);
1368        assert!(
1369            batch.execution_context.is_some(),
1370            "LIB-C017: V3 bridge must always emit an execution context"
1371        );
1372        let ctx = batch.execution_context.as_ref().unwrap();
1373        assert_eq!(
1374            ctx.schema_version, "execution_context_v1",
1375            "LIB-C017: execution context schema version must be execution_context_v1"
1376        );
1377        assert!(
1378            ctx.workload_class.is_some(),
1379            "LIB-C017: execution context must declare workload class"
1380        );
1381    }
1382
1383    #[test]
1384    fn cf_v9_c017_v2_to_v3_upgrade_preserves_record_count() {
1385        // LIB-C017: Upgrading V2 to V3 must preserve the number of records.
1386        let v2 = make_v2_envelope("c017-upgrade", &["claim-a", "claim-b", "claim-c"]);
1387        let v3 = ExportEnvelopeV3::try_from(v2.clone()).unwrap();
1388        assert_eq!(
1389            v2.records.len(),
1390            v3.records.len(),
1391            "LIB-C017: V2->V3 upgrade must preserve record count"
1392        );
1393    }
1394}