Compliance hardening: enforce measured behavior, real gates, retained evidence

Addresses the attached findings as blocking compliance failures. Each fix
removes a substitution pattern and adds a negative-control test.

Finding 9 (hash changes != enforced metamorphic expectations):
  ci_reports now enforces a sound metamorphic relation per perturbation — a
  perturbation the program CONSUMES must alter the trace — grounded in
  reachability, with a non-vacuity check. Test:
  consumed_perturbation_must_alter_trace.

Finding 10 (trace counts != causal explanation):
  New causal_explanation gate ablates each recorded causal edge's source lane
  and requires the destination delta to change. Measured: true-source 0.77 vs
  scrambled-source 0.03. Threshold 0.50. New required report
  causal_explanation_report. Test:
  causal_edges_are_intervention_confirmed_not_counted.

Finding 6 (static seed corpus != failure retention):
  replay_corpus::retention adds a committed, append-only counterexample corpus
  (retained_failures.tsv) re-verified every run against the independent runtime.
  Negative control: reintroduced_bug_is_caught_by_retention.

Finding 5 (mini mutation evaluator != real acceptance gate):
  Mutants are now killed by ci_reports' OWN acceptance-gate predicates with
  single-sourced thresholds (TRACE_EDGES_MIN, etc.); evaluate_mutants replaces
  semantic_mutation::run_suite on the acceptance path. Test:
  mutants_killed_by_real_acceptance_gates.

Findings 2 & 3 (generated report != independent attestation; merkle root !=
provenance without leaves):
  ci_reports persists every Merkle leaf (evidence/leaves.tsv) + claims. New
  `attestation` crate + `attest` binary recompute the root from the leaves in a
  SEPARATE process that never reads compliance_report.json; wired as a distinct
  merge-gates step. Negative control: tampered_leaf_breaks_attestation.

Finding 8 (structural indicators != measured behavior):
  Domain gate already requires measured influence/mutation/removal; added an
  explicit reject for "appears structurally but no measured influence".

Finding 1 (workflow != merge enforcement): BLOCKED on server-side branch
  protection. Added .github/rulesets/main-required-checks.json + apply command;
  enforcement still requires a repo admin to activate the ruleset.

Finding 7 (protocol socket E2E != rendered browser E2E): BLOCKED on a CI browser
  runner; rendered-browser E2E remains advisory-only.

Finding 4 (trace summary != full trace evidence): PARTIAL. Each retained leaf
  binds the full trace via canonical_hash over all graph edges, and the root is
  independently recomputed from the leaves; per-execution raw-trace round-trip
  reconstruction by the attestor is not yet implemented.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 21:13:48 -07:00
co-authored by Claude Opus 4.8
parent 93c78d9c76
commit 1e50c80627
14 changed files with 1028 additions and 26 deletions
+3 -1
View File
@@ -15,6 +15,8 @@ use reference_runtime::{execute, EngineConfig, ResolutionInput};
use std::path::PathBuf;
use world_model::Hash;
pub mod retention;
/// Format version of the persisted corpus file. Bump only with a deliberate,
/// reviewed migration of the committed corpus.
pub const CORPUS_VERSION: u32 = 1;
@@ -33,7 +35,7 @@ pub struct ReplayCase {
pub expected_future_hash: Hash,
}
fn input_for(master_seed: u64) -> (ResolutionInput, u64, u64, u64, u64) {
pub(crate) fn input_for(master_seed: u64) -> (ResolutionInput, u64, u64, u64, u64) {
let (case, _accepted_seed) = generate_accepted_case(master_seed);
let input = ResolutionInput {
world: case.world.clone(),
+162
View File
@@ -0,0 +1,162 @@
//! Failure retention (finding 6). A static seed corpus proves the engine is
//! stable on a *fixed* sample; it does not retain *discovered* counterexamples.
//! This module is the regression memory: every counterexample CI ever finds
//! (e.g. a reference/runtime divergence) is curated into a committed,
//! append-only file and **re-verified on every run**, so a fixed bug can never
//! silently reappear.
//!
//! Two halves, both real:
//! * The committed `corpus/retained_failures.tsv` is loaded and each case is
//! re-executed under the reference and the independent runtime-under-test; any
//! case where they disagree is a *regression* and fails CI.
//! * When a run discovers a NEW divergence, it is serialized to the run's output
//! so it must be triaged and added to the committed set (the run also fails).
//! Discovered failures are therefore never lost.
use crate::input_for;
use reference_runtime::{canonical, execute, EngineConfig, ResolutionInput, ResolutionResult};
use runtime_under_test::native_resolve;
use std::path::PathBuf;
/// A retained counterexample: the seed needed to regenerate it, the gate it
/// originally tripped, and a human note.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RetainedFailure {
pub master_seed: u64,
pub gate: String,
pub note: String,
}
/// Path to the committed retained-failures file.
pub fn retained_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("corpus/retained_failures.tsv")
}
/// Serialize a retained set (with a provenance header).
pub fn serialize(failures: &[RetainedFailure]) -> String {
let mut s = String::from("# magicka-retained-failures v1\n");
s.push_str("seed\tgate\tnote\n");
for f in failures {
// Tabs/newlines are stripped from free text to keep the TSV well-formed.
let gate = f.gate.replace(['\t', '\n'], " ");
let note = f.note.replace(['\t', '\n'], " ");
s.push_str(&format!("{:016x}\t{}\t{}\n", f.master_seed, gate, note));
}
s
}
/// Parse a retained set.
pub fn parse(text: &str) -> Vec<RetainedFailure> {
let mut out = Vec::new();
for line in text.lines() {
if line.starts_with('#') || line.starts_with("seed") || line.trim().is_empty() {
continue;
}
let f: Vec<&str> = line.splitn(3, '\t').collect();
if f.len() < 1 {
continue;
}
if let Ok(seed) = u64::from_str_radix(f[0].trim(), 16) {
out.push(RetainedFailure {
master_seed: seed,
gate: f.get(1).unwrap_or(&"").to_string(),
note: f.get(2).unwrap_or(&"").to_string(),
});
}
}
out
}
/// Load the committed retained set. Missing file => empty set + `present=false`.
pub fn load() -> (Vec<RetainedFailure>, bool) {
match std::fs::read_to_string(retained_path()) {
Ok(t) => (parse(&t), true),
Err(_) => (Vec::new(), false),
}
}
/// Append a newly-discovered failure to the committed set (append-only).
pub fn append(failure: &RetainedFailure) -> std::io::Result<()> {
let (mut set, _present) = load();
if !set.iter().any(|f| f.master_seed == failure.master_seed) {
set.push(failure.clone());
}
let path = retained_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, serialize(&set))
}
/// Does a retained case still agree between the reference and `resolve`? A
/// retained counterexample is "fixed" iff the two implementations now produce
/// identical canonical views on it.
pub fn agrees(
failure: &RetainedFailure,
resolve: impl Fn(&ResolutionInput) -> ResolutionResult,
) -> bool {
let (input, ..) = input_for(failure.master_seed);
canonical(&execute(&EngineConfig::reference(), &input)) == canonical(&resolve(&input))
}
/// Result of verifying the retained set.
#[derive(Clone, Debug)]
pub struct RetentionReport {
pub present: bool,
pub total: usize,
/// Seeds that regressed (reference and runtime-under-test disagree again).
pub regressions: Vec<u64>,
}
impl RetentionReport {
pub fn ok(&self) -> bool {
self.present && self.regressions.is_empty()
}
}
/// Verify the committed retained set against the independent runtime-under-test.
pub fn verify() -> RetentionReport {
let (set, present) = load();
let regressions = set
.iter()
.filter(|f| !agrees(f, |inp| native_resolve(inp)))
.map(|f| f.master_seed)
.collect();
RetentionReport { present, total: set.len(), regressions }
}
#[cfg(test)]
mod tests {
use super::*;
use runtime_under_test::buggy_resolve;
#[test]
fn serialize_roundtrips() {
let set = vec![
RetainedFailure { master_seed: 0xABC, gate: "runtime_equivalence".into(), note: "dropped edge".into() },
RetainedFailure { master_seed: 0x1, gate: "replay".into(), note: "drift".into() },
];
assert_eq!(parse(&serialize(&set)), set);
}
#[test]
fn committed_retained_set_exists_and_holds() {
// The committed file must be present (the retention mechanism is wired),
// and every retained counterexample must still be fixed.
let report = verify();
assert!(report.present, "committed retained_failures.tsv is missing");
assert!(report.regressions.is_empty(), "regressions: {:?}", report.regressions);
}
/// A retained counterexample re-checked against a *buggy* runtime must be
/// flagged as a regression. Proves the retention check is not vacuous: if a
/// fixed bug reappears, retention catches it.
#[test]
fn reintroduced_bug_is_caught_by_retention() {
let f = RetainedFailure { master_seed: 42, gate: "runtime_equivalence".into(), note: "synthetic".into() };
// Against the real runtime the case agrees (the bug is fixed)...
assert!(agrees(&f, |inp| native_resolve(inp)));
// ...but a runtime that reintroduces the bug is caught as a regression.
assert!(!agrees(&f, |inp| buggy_resolve(inp)));
}
}