update
magicka-merge-gates / advisory-fast (push) Has been skipped
magicka-merge-gates / merge-gates (push) Failing after 38s

This commit is contained in:
2026-06-21 19:21:48 -07:00
parent 2fe989bcb3
commit 659544f0b2
16 changed files with 12375 additions and 419 deletions
+515 -79
View File
@@ -1,83 +1,183 @@
//! `ci_reports` — the CI orchestrator. Runs the whole adversarial framework
//! against the reference runtime and the runtime under test, evaluates every
//! gate, and emits machine-readable JSON plus a human-readable markdown
//! summary. Merge is blocked unless all reports pass.
//! against the reference runtime and the (independently implemented) runtime
//! under test, evaluates every gate, and emits machine-readable JSON plus a
//! human-readable markdown summary. Merge is blocked unless all reports pass.
//!
//! Governing rule (enforced structurally, not by naming): *no gate may pass from
//! configuration, naming, shared implementation, smoke-scale runs, regenerated
//! expectations, or proxy metrics.* Concretely:
//! - the equivalence gate compares two independent implementations;
//! - the merge profile's scale floor cannot be lowered by environment overrides,
//! and the executions actually performed are counted and checked;
//! - the replay corpus is loaded from a committed file, not regenerated;
//! - collapse analysis measures real trace information;
//! - mutants must fail their named gate;
//! - every report carries provenance binding its numbers to executed work.
pub mod json;
use collapse_analysis::{analyze, BehaviorCorpus, CollapseSummary};
use collapse_analysis::{analyze, BehaviorCorpus, CollapseSummary, FEATURE_W};
use generators::{generate_accepted_case, generated_gate_failures, GeneratedCase};
use reference_runtime::{canonical, execute, EngineConfig, ResolutionInput, ResolutionResult, Runtime};
use runtime_under_test::RuntimeUnderTest;
use reference_runtime::{
canonical, execute, Canonical, EngineConfig, ResolutionInput, ResolutionResult, Runtime,
};
use runtime_under_test::{native_resolve, RuntimeUnderTest};
use semantic_mutation::{run_suite, MutationOutcome};
use std::collections::HashMap;
use world_model::{Hash, WorldSnapshot, HIDDEN_LANES, LANES, NUM_DOMAINS};
use world_model::{Hash, Hasher, WorldSnapshot, NUM_DOMAINS};
// ---------------------------------------------------------------------------
// Scale configuration.
// Run profile + scale, with an unbypassable merge floor.
// ---------------------------------------------------------------------------
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Profile {
/// Representative slice; explicitly NOT merge-blocking.
Fast,
/// Full, merge-blocking gates with hard floors that overrides cannot lower.
Merge,
}
impl Profile {
pub fn name(self) -> &'static str {
match self {
Profile::Fast => "fast",
Profile::Merge => "merge",
}
}
}
/// The hard minimums the merge profile mandates (spec "Hard CI Gates").
#[derive(Clone, Copy, Debug)]
pub struct ScaleFloor {
pub worlds: usize,
pub programs: usize,
pub executions: usize,
pub perturbations_per_exec: usize,
pub mutants: usize,
pub replay_cases: usize,
}
impl ScaleFloor {
pub const NONE: ScaleFloor = ScaleFloor {
worlds: 0,
programs: 0,
executions: 0,
perturbations_per_exec: 0,
mutants: 0,
replay_cases: 0,
};
pub const MERGE: ScaleFloor = ScaleFloor {
worlds: 50_000,
programs: 250_000,
executions: 1_000_000,
perturbations_per_exec: 10,
mutants: 500,
replay_cases: 10_000,
};
}
#[derive(Clone, Debug)]
pub struct Scale {
pub profile: Profile,
pub floor: ScaleFloor,
pub executions: usize,
pub mutants: usize,
pub replay_cases: usize,
pub collapse_samples: usize,
pub domain_probe_cases: usize,
pub domain_probe_variations: usize,
/// Overrides that tried to lower a merge floor (recorded, never applied).
pub override_violations: Vec<String>,
}
impl Scale {
/// The full, merge-blocking gates mandated by the spec.
pub fn full() -> Self {
pub fn merge() -> Self {
Scale {
profile: Profile::Merge,
floor: ScaleFloor::MERGE,
executions: 1_000_000,
mutants: 600,
replay_cases: 10_000,
collapse_samples: 5_000,
domain_probe_cases: 2_000,
domain_probe_variations: 64,
override_violations: Vec::new(),
}
}
/// Fast CI (~10% spirit): small but exercises every gate.
pub fn fast() -> Self {
Scale {
profile: Profile::Fast,
floor: ScaleFloor::NONE,
executions: 600,
mutants: 520,
replay_cases: 600,
collapse_samples: 600,
domain_probe_cases: 200,
domain_probe_variations: 48,
override_violations: Vec::new(),
}
}
pub fn tiny() -> Self {
Scale {
profile: Profile::Fast,
floor: ScaleFloor::NONE,
executions: 120,
mutants: 520,
replay_cases: 120,
collapse_samples: 120,
domain_probe_cases: 60,
domain_probe_variations: 32,
override_violations: Vec::new(),
}
}
/// Apply an override to a field. On the merge profile an override may only
/// raise the value; attempting to lower it below the floor is recorded as a
/// violation and the floor is kept.
fn apply_override(&mut self, name: &str, value: usize, field_floor: usize, set: impl Fn(&mut Scale, usize)) {
if self.profile == Profile::Merge && value < field_floor {
self.override_violations.push(format!(
"override {name}={value} would lower merge floor {field_floor}; ignored"
));
set(self, field_floor);
} else {
set(self, value);
}
}
pub fn from_env() -> Self {
let mut s = match std::env::var("MAGICKA_SCALE").as_deref() {
Ok("full") => Scale::full(),
Ok("tiny") => Scale::tiny(),
_ => Scale::fast(),
let scale_var = std::env::var("MAGICKA_SCALE").ok();
let profile_var = std::env::var("MAGICKA_PROFILE").ok();
let profile = if profile_var.as_deref() == Some("merge")
|| scale_var.as_deref() == Some("full")
{
Profile::Merge
} else {
Profile::Fast
};
let mut s = match (profile, scale_var.as_deref()) {
(Profile::Merge, _) => Scale::merge(),
(Profile::Fast, Some("tiny")) => Scale::tiny(),
(Profile::Fast, _) => Scale::fast(),
};
if let Some(v) = env_usize("MAGICKA_EXECUTIONS") {
s.executions = v;
let f = s.floor.executions;
s.apply_override("MAGICKA_EXECUTIONS", v, f, |s, x| s.executions = x);
}
if let Some(v) = env_usize("MAGICKA_MUTANTS") {
s.mutants = v;
let f = s.floor.mutants;
s.apply_override("MAGICKA_MUTANTS", v, f, |s, x| s.mutants = x);
}
if let Some(v) = env_usize("MAGICKA_REPLAY") {
s.replay_cases = v;
let f = s.floor.replay_cases;
s.apply_override("MAGICKA_REPLAY", v, f, |s, x| s.replay_cases = x);
}
if let Some(v) = env_usize("MAGICKA_COLLAPSE") {
s.collapse_samples = v;
// collapse sample count has no spec floor; never below 1.
s.collapse_samples = v.max(1);
}
s
}
@@ -87,6 +187,81 @@ fn env_usize(key: &str) -> Option<usize> {
std::env::var(key).ok().and_then(|v| v.parse().ok())
}
// ---------------------------------------------------------------------------
// Provenance: bind the reported numbers to executed work.
// ---------------------------------------------------------------------------
/// A tamper-evident record of what actually ran. The Merkle root is computed
/// over the per-execution replay-record hashes, so the report cannot claim more
/// executions than were performed. The two engine identities are independent
/// fingerprints of the reference and the runtime under test; they must agree,
/// which is evidence the two implementations match.
#[derive(Clone, Debug)]
pub struct Provenance {
pub profile: Profile,
pub floor: ScaleFloor,
pub worlds_generated: usize,
pub programs_generated: usize,
pub actual_executions: usize,
pub min_perturbations_per_exec: usize,
pub total_comparisons: usize,
pub actual_mutants: usize,
pub replay_total: usize,
pub reference_engine_id: Hash,
pub rut_engine_id: Hash,
pub engines_agree: bool,
pub execution_merkle_root: Hash,
pub merkle_leaf_count: usize,
pub collapse_feature_width: usize,
pub failures: Vec<String>,
}
/// Merkle root over execution leaves (FNV-combined binary tree).
pub fn merkle_root(leaves: &[Hash]) -> Hash {
if leaves.is_empty() {
return Hash(0);
}
let mut level: Vec<Hash> = leaves.to_vec();
while level.len() > 1 {
let mut next = Vec::with_capacity(level.len().div_ceil(2));
for pair in level.chunks(2) {
let mut h = Hasher::new();
h.write_tag("merkle");
h.write_u64(pair[0].0);
h.write_u64(if pair.len() > 1 { pair[1].0 } else { pair[0].0 });
next.push(h.finish());
}
level = next;
}
level[0]
}
/// Fixed probe inputs for engine fingerprinting (independent of the corpus).
fn probe_inputs(count: usize) -> Vec<ResolutionInput> {
(0..count)
.map(|i| {
let seed = 0xF1A9_E000_u64 ^ (i as u64).wrapping_mul(0x9E3779B97F4A7C15);
let (case, _) = generate_accepted_case(seed);
input_from_case(&case)
})
.collect()
}
/// Fingerprint of a runtime: hash of its canonical outputs over the probe set.
fn engine_fingerprint(probes: &[ResolutionInput], resolve: impl Fn(&ResolutionInput) -> ResolutionResult) -> Hash {
let mut h = Hasher::new();
h.write_tag("engine-id");
for inp in probes {
let c = canonical(&resolve(inp));
h.write_u64(c.delta_hash.0);
h.write_u64(c.trace_hash.0);
h.write_u64(c.fault_hash.0);
h.write_u64(c.replay_hash.0);
h.write_u64(c.future_hash.0);
}
h.finish()
}
// ---------------------------------------------------------------------------
// Helpers.
// ---------------------------------------------------------------------------
@@ -165,26 +340,50 @@ fn compressibility(features: &[i64]) -> f64 {
(1.0 - h / 8.0).clamp(0.0, 1.0)
}
fn world_input_features(w: &WorldSnapshot) -> Vec<f64> {
let mut out = Vec::with_capacity(NUM_DOMAINS * (LANES + HIDDEN_LANES));
for d in &w.domains {
for &v in &d.observed {
out.push(v as f64);
}
for &v in &d.hidden {
out.push(v as f64);
}
/// Real serialized-trace feature row used by the collapse analysis. Layout per
/// domain block (width 9): `[infl_out, infl_in, flow_out, flow_in, read, write,
/// temporal, obs_delta, hid_delta]`, followed by globals `[causal_rank,
/// edge_count, touched, divergence_mean]`. This is genuine trace structure, not
/// a hash-derived proxy.
fn trace_feature_row(r: &ResolutionResult) -> Vec<f64> {
let infl = r.trace.causal_graph.influence_matrix();
let mut flow = [[0.0f64; NUM_DOMAINS]; NUM_DOMAINS];
for &(a, b, bits) in &r.trace.information_flow.edges {
flow[a as usize % NUM_DOMAINS][b as usize % NUM_DOMAINS] += bits as f64;
}
let mut temporal = [0.0f64; NUM_DOMAINS];
for &(_s, _off, d) in &r.trace.temporal_graph.edges {
temporal[d as usize % NUM_DOMAINS] += 1.0;
}
out
}
fn behavior_output_features(r: &ResolutionResult) -> Vec<f64> {
r.trace
.behavior_fingerprint
.features
.iter()
.map(|&v| v as f64)
.collect()
let mut row = Vec::with_capacity(FEATURE_W);
for d in 0..NUM_DOMAINS {
let infl_out: f64 = (0..NUM_DOMAINS).map(|j| infl[d][j]).sum();
let infl_in: f64 = (0..NUM_DOMAINS).map(|i| infl[i][d]).sum();
let flow_out: f64 = (0..NUM_DOMAINS).map(|j| flow[d][j]).sum();
let flow_in: f64 = (0..NUM_DOMAINS).map(|i| flow[i][d]).sum();
let read = r.trace.read_graph.access_count[d] as f64;
let write = r.trace.write_graph.access_count[d] as f64;
let temp = temporal[d];
let obs_delta: f64 = r.delta.domain_deltas[d]
.observed
.iter()
.map(|&v| (v as f64).abs())
.sum();
let hid_delta: f64 = r.delta.domain_deltas[d]
.hidden
.iter()
.map(|&v| (v as f64).abs())
.sum();
row.extend_from_slice(&[
infl_out, infl_in, flow_out, flow_in, read, write, temp, obs_delta, hid_delta,
]);
}
row.push(r.trace.causal_rank() as f64);
row.push(r.trace.causal_edge_count() as f64);
row.push(r.trace.touched_domain_count() as f64);
row.push(r.trace.context_divergence());
row
}
// ---------------------------------------------------------------------------
@@ -207,6 +406,7 @@ pub struct TraceGates {
pub struct EquivalenceGates {
pub total: usize,
pub matched: usize,
pub independent: bool,
pub failures: Vec<String>,
}
@@ -244,6 +444,7 @@ pub struct ReplayGates {
pub total: usize,
pub deterministic: usize,
pub drift: usize,
pub loaded_from_disk: bool,
pub failures: Vec<String>,
}
@@ -259,6 +460,7 @@ pub struct CoverageGates {
pub struct CiResults {
pub scale: Scale,
pub provenance: Provenance,
pub trace: TraceGates,
pub equivalence: EquivalenceGates,
pub domain: DomainGates,
@@ -281,6 +483,7 @@ impl CiResults {
("contract", &self.contract.failures),
("replay", &self.replay.failures),
("coverage", &self.coverage.failures),
("provenance", &self.provenance.failures),
]
}
@@ -297,7 +500,26 @@ pub fn run_all(scale: Scale) -> CiResults {
let cfg = EngineConfig::reference();
let rut = RuntimeUnderTest::new();
// Main execution + metamorphic + contract pass.
// Progress logging: a long merge run must be trackable, not a silent black
// box. Emit periodic stderr lines with elapsed time and counts. Suppressed
// for small runs (tests) to keep their output clean.
let started = std::time::Instant::now();
let report_progress = scale.executions >= 10_000;
let prog_step = (scale.executions / 20).max(1);
macro_rules! progress {
($($arg:tt)*) => {
if report_progress {
eprintln!(" [{:7.1}s] {}", started.elapsed().as_secs_f64(), format_args!($($arg)*));
}
};
}
progress!(
"start: {} executions x (1 base + {} perturbations) = {} comparisons planned",
scale.executions,
scale.floor.perturbations_per_exec.max(10),
scale.executions * 11
);
let mut ranks = Vec::with_capacity(scale.executions);
let mut edges = Vec::with_capacity(scale.executions);
let mut touched = Vec::with_capacity(scale.executions);
@@ -316,25 +538,40 @@ pub fn run_all(scale: Scale) -> CiResults {
let mut meta_alt_delta = 0usize;
let mut meta_alt_future = 0usize;
let mut meta_neutral_unexpl = 0usize;
let mut min_perturbations = usize::MAX;
let mut contract_pass = 0usize;
let mut contract_failures: Vec<String> = Vec::new();
let mut collapse_inputs: Vec<Vec<f64>> = Vec::new();
let mut collapse_outputs: Vec<Vec<f64>> = Vec::new();
let mut collapse_fps: Vec<Hash> = Vec::new();
let mut collapse_rows: Vec<Vec<f64>> = Vec::new();
let mut mutation_inputs: Vec<ResolutionInput> = Vec::new();
let mut generated_rejected = 0usize;
let mut perturbation_runs = 0usize;
let mut contract_rejected = 0usize;
// Provenance leaves: one per actually-committed execution (base + every
// perturbation), so the Merkle root binds to all compared executions.
let mut merkle_leaves: Vec<Hash> = Vec::with_capacity(scale.executions);
let mut actual_executions = 0usize;
let mut worlds_generated = 0usize;
let mut programs_generated = 0usize;
// 100% reference/runtime comparison covers EVERY execution, base and
// perturbation alike — never base executions only.
let mut equiv_total = 0usize;
for i in 0..scale.executions {
// Admission: a case is admitted only if its *measured* trace behavior
// satisfies its contract. Structurally-flat cases are already rejected
// by `generate_accepted_case`; here we additionally reject cases whose
// behavior fails the contract, retrying with fresh seeds.
if report_progress && i > 0 && i % prog_step == 0 {
progress!(
"main loop {}/{} ({:.0}%): {} comparisons, {} matched, {} contract-retries",
i,
scale.executions,
100.0 * i as f64 / scale.executions as f64,
equiv_total,
equiv_matched,
contract_rejected
);
}
let mut seed = case_seed(i);
for attempt in 0..48 {
let last_attempt = attempt == 47;
@@ -350,7 +587,6 @@ pub fn run_all(scale: Scale) -> CiResults {
let comp = compressibility(&r.trace.behavior_fingerprint.features);
let div = r.trace.context_divergence();
// Run the 10 perturbations for metamorphic stats + future sensitivity.
let base_trace_h = r.trace.canonical_hash();
let base_delta_h = r.delta.hash();
let base_future_h = r.replay.future_hash;
@@ -359,8 +595,13 @@ pub fn run_all(scale: Scale) -> CiResults {
let mut c_alt_future = 0usize;
let mut c_neutral = 0usize;
let mut c_pert = 0usize;
// Capture each perturbation execution so the committed case can run
// the full 100% reference/runtime comparison without recomputing the
// reference side.
let mut pert_execs: Vec<(ResolutionInput, Canonical, Hash)> = Vec::new();
for pc in &case.perturbations {
let pr = execute(&cfg, &input_with_world(&case, pc.world.clone()));
let pinput = input_with_world(&case, pc.world.clone());
let pr = execute(&cfg, &pinput);
c_pert += 1;
let at = pr.trace.canonical_hash() != base_trace_h;
let ad = pr.delta.hash() != base_delta_h;
@@ -377,6 +618,7 @@ pub fn run_all(scale: Scale) -> CiResults {
if !at && pc.expectation.neutral_explanation.is_none() {
c_neutral += 1;
}
pert_execs.push((pinput, canonical(&pr), pr.replay.hash()));
}
let future_sensitivity = if c_pert > 0 {
c_alt_future as f64 / c_pert as f64
@@ -408,12 +650,34 @@ pub fn run_all(scale: Scale) -> CiResults {
}
// ---- Commit the admitted case ----
let r2 = rut.resolve(input.clone());
if canonical(&r) == canonical(&r2) {
// Every admitted case used exactly one generated world + program.
worlds_generated += 1;
programs_generated += 1;
// The runtime under test is an INDEPENDENT implementation. The
// equivalence gate compares 100% of executions: the base AND every
// perturbation execution, never base executions only.
let base_rut = rut.resolve(input.clone());
equiv_total += 1;
if canonical(&r) == canonical(&base_rut) {
equiv_matched += 1;
} else if equiv_failures.len() < 16 {
equiv_failures.push(format!("case {} reference != runtime_under_test", i));
equiv_failures.push(format!("case {} (base) reference != runtime_under_test", i));
}
merkle_leaves.push(r.replay.hash());
for (pinput, pref_canon, pleaf) in &pert_execs {
let prut = rut.resolve(pinput.clone());
equiv_total += 1;
if *pref_canon == canonical(&prut) {
equiv_matched += 1;
} else if equiv_failures.len() < 16 {
equiv_failures
.push(format!("case {} (perturbation) reference != runtime_under_test", i));
}
merkle_leaves.push(*pleaf);
}
actual_executions += 1;
ranks.push(rank as f64);
edges.push(r.trace.causal_edge_count() as f64);
@@ -445,6 +709,7 @@ pub fn run_all(scale: Scale) -> CiResults {
meta_alt_future += c_alt_future;
meta_neutral_unexpl += c_neutral;
perturbation_runs += c_pert;
min_perturbations = min_perturbations.min(c_pert);
if creasons.is_empty() {
contract_pass += 1;
@@ -452,10 +717,8 @@ pub fn run_all(scale: Scale) -> CiResults {
contract_failures.push(format!("case {} violates {:?}", i, creasons));
}
if collapse_inputs.len() < scale.collapse_samples {
collapse_inputs.push(world_input_features(&case.world));
collapse_outputs.push(behavior_output_features(&r));
collapse_fps.push(r.trace.behavior_fingerprint.hash);
if collapse_rows.len() < scale.collapse_samples {
collapse_rows.push(trace_feature_row(&r));
}
if mutation_inputs.len() < 64 {
mutation_inputs.push(input.clone());
@@ -464,6 +727,15 @@ pub fn run_all(scale: Scale) -> CiResults {
}
}
if min_perturbations == usize::MAX {
min_perturbations = 0;
}
progress!(
"main loop complete: {} executions, {} comparisons, {} matched",
actual_executions,
equiv_total,
equiv_matched
);
let n = scale.executions.max(1) as f64;
// ---- Trace gates ----
@@ -471,7 +743,7 @@ pub fn run_all(scale: Scale) -> CiResults {
let largest_cluster = fp_counts.values().copied().max().unwrap_or(0) as f64 / n;
let mut trace_failures = Vec::new();
let med_edges = median(&edges);
let p95_rank = percentile(&ranks, 0.05); // 95% of executions have rank >= this
let p95_rank = percentile(&ranks, 0.05);
let med_touched = median(&touched);
let p95_touched = percentile(&touched, 0.05);
let med_rank = median(&ranks);
@@ -505,22 +777,31 @@ pub fn run_all(scale: Scale) -> CiResults {
failures: trace_failures,
};
// ---- Equivalence gates ----
if equiv_matched != scale.executions {
// ---- Equivalence gates (two independent implementations, 100% coverage) ----
let expected_comparisons = actual_executions + perturbation_runs;
if equiv_total != expected_comparisons {
equiv_failures.push(format!(
"compared {} executions but {} ran (must compare 100%, base + perturbations)",
equiv_total, expected_comparisons
));
}
if equiv_matched != equiv_total {
equiv_failures.push(format!(
"{}/{} executions matched (require 100%)",
equiv_matched, scale.executions
equiv_matched, equiv_total
));
}
let equivalence = EquivalenceGates {
total: scale.executions,
total: equiv_total,
matched: equiv_matched,
independent: true,
failures: equiv_failures,
};
// ---- Domain participation gates ----
progress!("domain participation probes (influence/removal/merge)...");
let domain = domain_gates(
scale,
&scale,
&domain_appear,
&domain_mutated,
&domain_read_any,
@@ -556,11 +837,13 @@ pub fn run_all(scale: Scale) -> CiResults {
failures: meta_failures,
};
// ---- Collapse gates ----
let corpus = BehaviorCorpus::build(collapse_inputs, collapse_outputs, collapse_fps);
// ---- Collapse gates (real trace information) ----
progress!("collapse analysis (11 attacks over real trace features)...");
let corpus = BehaviorCorpus::build(collapse_rows);
let collapse = analyze(&corpus);
// ---- Mutation gates ----
// ---- Mutation gates (killed by named gate) ----
progress!("mutation suite ({} mutants, killed by named gate)...", scale.mutants);
let mutation = run_suite(scale.mutants, &mutation_inputs);
// ---- Contract gates ----
@@ -578,10 +861,11 @@ pub fn run_all(scale: Scale) -> CiResults {
failures: contract_gate_failures,
};
// ---- Replay gates ----
let replay = replay_gates(scale);
// ---- Replay gates (persisted corpus) ----
progress!("replay corpus verification (committed file)...");
let replay = replay_gates(&scale);
// ---- Coverage gates ----
// ---- Coverage gates + merge floor enforcement ----
let mut cov_failures = Vec::new();
if generated_rejected != 0 {
cov_failures.push(format!(
@@ -601,8 +885,93 @@ pub fn run_all(scale: Scale) -> CiResults {
failures: cov_failures,
};
// ---- Provenance ----
progress!("engine fingerprints + provenance (merkle over {} leaves)...", merkle_leaves.len());
let probes = probe_inputs(32);
let reference_engine_id = engine_fingerprint(&probes, |inp| execute(&cfg, inp));
let rut_engine_id = engine_fingerprint(&probes, native_resolve);
let engines_agree = reference_engine_id == rut_engine_id;
let root = merkle_root(&merkle_leaves);
let mut prov_failures = Vec::new();
prov_failures.extend(scale.override_violations.iter().cloned());
if !engines_agree {
prov_failures.push("reference and runtime-under-test engine identities differ".into());
}
if merkle_leaves.len() != equiv_total {
prov_failures.push("merkle leaf count does not match executions compared".into());
}
// Merge-floor enforcement: the floor cannot be bypassed.
if scale.profile == Profile::Merge {
if worlds_generated < scale.floor.worlds {
prov_failures.push(format!(
"worlds generated {} < merge floor {}",
worlds_generated, scale.floor.worlds
));
}
if programs_generated < scale.floor.programs {
prov_failures.push(format!(
"programs generated {} < merge floor {}",
programs_generated, scale.floor.programs
));
}
if actual_executions < scale.floor.executions {
prov_failures.push(format!(
"executions {} < merge floor {}",
actual_executions, scale.floor.executions
));
}
if min_perturbations < scale.floor.perturbations_per_exec {
prov_failures.push(format!(
"min perturbations/exec {} < merge floor {}",
min_perturbations, scale.floor.perturbations_per_exec
));
}
// 100% comparison must cover base + all perturbations.
let required_comparisons =
scale.floor.executions * (1 + scale.floor.perturbations_per_exec);
if equiv_total < required_comparisons {
prov_failures.push(format!(
"comparisons {} < merge floor {} (100% of base + perturbations)",
equiv_total, required_comparisons
));
}
if mutation.total < scale.floor.mutants {
prov_failures.push(format!(
"mutants {} < merge floor {}",
mutation.total, scale.floor.mutants
));
}
if replay.total < scale.floor.replay_cases {
prov_failures.push(format!(
"replay cases {} < merge floor {}",
replay.total, scale.floor.replay_cases
));
}
}
let provenance = Provenance {
profile: scale.profile,
floor: scale.floor,
worlds_generated,
programs_generated,
actual_executions,
min_perturbations_per_exec: min_perturbations,
total_comparisons: equiv_total,
actual_mutants: mutation.total,
replay_total: replay.total,
reference_engine_id,
rut_engine_id,
engines_agree,
execution_merkle_root: root,
merkle_leaf_count: merkle_leaves.len(),
collapse_feature_width: FEATURE_W,
failures: prov_failures,
};
CiResults {
scale,
provenance,
trace,
equivalence,
domain,
@@ -616,7 +985,7 @@ pub fn run_all(scale: Scale) -> CiResults {
}
fn domain_gates(
scale: Scale,
scale: &Scale,
appear: &[u64; NUM_DOMAINS],
mutated: &[u64; NUM_DOMAINS],
read_any: &[bool; NUM_DOMAINS],
@@ -632,7 +1001,6 @@ fn domain_gates(
mutated_f[d] = mutated[d] as f64 / n;
}
// Influence probe: mask each domain and see how often the trace changes.
let mut influences = [0.0; NUM_DOMAINS];
let probe_cases = scale.domain_probe_cases.max(1);
for d in 0..NUM_DOMAINS {
@@ -652,8 +1020,6 @@ fn domain_gates(
influences[d] = changed as f64 / probe_cases as f64;
}
// Removal diversity probe: vary only domain d across K worlds; compare the
// count of distinct behaviors with the domain present vs masked.
let mut removal_loss = [0.0; NUM_DOMAINS];
let k = scale.domain_probe_variations.max(8);
for d in 0..NUM_DOMAINS {
@@ -667,7 +1033,7 @@ fn domain_gates(
let mcfg = masked_config(d);
for v in 0..k {
let mut w = base_world.clone();
for l in 0..LANES {
for l in 0..world_model::LANES {
w.domains[d].observed[l] = (v as i64 + 1).wrapping_mul(7919 + l as i64);
}
w.domains[d].hidden[0] = (v as i64).wrapping_mul(104729);
@@ -680,7 +1046,6 @@ fn domain_gates(
removal_loss[d] = (1.0 - dm / df).clamp(0.0, 1.0);
}
// Merge probe: alias domain b := domain a, see how often behavior changes.
let mut min_merge_loss = 1.0f64;
let merge_cases = (scale.domain_probe_cases / 2).max(20);
for a in 0..NUM_DOMAINS {
@@ -749,21 +1114,29 @@ fn domain_gates(
}
}
fn replay_gates(scale: Scale) -> ReplayGates {
let corpus = replay_corpus::build_corpus(scale.replay_cases, 0x5EED);
let report = replay_corpus::verify_corpus(&corpus);
fn replay_gates(scale: &Scale) -> ReplayGates {
let report = replay_corpus::verify_persisted_corpus();
let mut failures = Vec::new();
if !report.loaded_from_disk {
failures.push(format!(
"committed replay corpus not found at {}",
replay_corpus::corpus_path().display()
));
}
if !report.drift.is_empty() {
failures.push(format!("{} replay cases drifted", report.drift.len()));
}
// Minimum corpus size is a merge-blocking gate only at full scale.
if matches!(std::env::var("MAGICKA_SCALE").as_deref(), Ok("full")) && report.total < 10_000 {
failures.push(format!("replay corpus {} < 10000", report.total));
if scale.profile == Profile::Merge && report.total < scale.floor.replay_cases {
failures.push(format!(
"replay corpus {} < merge floor {}",
report.total, scale.floor.replay_cases
));
}
ReplayGates {
total: report.total,
deterministic: report.deterministic,
drift: report.drift.len(),
loaded_from_disk: report.loaded_from_disk,
failures,
}
}
@@ -779,6 +1152,69 @@ mod tests {
assert!(f.is_empty(), "{name} failed: {:?}", f);
}
assert!(r.mutation.passed(), "mutants survived: {:?}", r.mutation.survivors);
assert!(r.provenance.engines_agree, "engine identities disagree");
assert!(r.passed());
}
/// Negative control: a smoke-scale run that claims the merge profile must be
/// REJECTED, because the merge floors (1M executions, 10k replay, etc.) are
/// not met. Proves a default/small profile cannot stand in for acceptance.
#[test]
fn merge_profile_at_smoke_scale_is_rejected() {
let mut s = Scale::tiny();
s.profile = Profile::Merge;
s.floor = ScaleFloor::MERGE;
let r = run_all(s);
assert!(
!r.passed(),
"a smoke-scale run was accepted as a merge run (floors not enforced)"
);
assert!(
!r.provenance.failures.is_empty(),
"merge floors were not flagged in provenance"
);
}
#[test]
fn merge_floor_cannot_be_lowered_by_override() {
// Simulate an attempt to lower the merge execution floor.
let mut s = Scale::merge();
s.apply_override("MAGICKA_EXECUTIONS", 10, s.floor.executions, |s, x| s.executions = x);
assert_eq!(s.executions, s.floor.executions, "floor was lowered by override");
assert!(!s.override_violations.is_empty(), "violation not recorded");
}
/// Negative control: a decorative (never-appearing, never-mutated) domain
/// must be rejected by the domain-participation gate. Proves the gate can
/// fail and discriminates a decorative domain from a live one.
#[test]
fn decorative_domain_is_rejected() {
let scale = Scale::tiny();
let n = 100.0;
let mut appear = [100u64; NUM_DOMAINS];
let mut mutated = [100u64; NUM_DOMAINS];
appear[3] = 0; // domain 3 is decorative
mutated[3] = 0;
let read_any = [true; NUM_DOMAINS];
let write_any = [true; NUM_DOMAINS];
let gates = domain_gates(&scale, &appear, &mutated, &read_any, &write_any, n);
assert!(
!gates.failures.is_empty(),
"domain gate failed to reject a decorative domain"
);
assert!(
gates.failures.iter().any(|f| f.contains("domain 3")),
"decorative domain 3 not flagged: {:?}",
gates.failures
);
}
#[test]
fn merkle_root_binds_to_leaves() {
let a = merkle_root(&[Hash(1), Hash(2), Hash(3)]);
let b = merkle_root(&[Hash(1), Hash(2), Hash(3)]);
let c = merkle_root(&[Hash(1), Hash(2), Hash(4)]);
assert_eq!(a, b);
assert_ne!(a, c);
}
}
+307 -21
View File
@@ -1,9 +1,10 @@
//! The `ci` binary: runs the full adversarial framework against the reference
//! and runtime-under-test, writes the eight required reports (JSON + markdown),
//! and exits nonzero if any gate fails.
//! and the independently-implemented runtime under test, writes the required
//! reports (JSON + markdown) including a provenance report binding the numbers
//! to executed work, and exits nonzero if any gate fails.
use ci_reports::json::Json;
use ci_reports::{run_all, CiResults, Scale};
use ci_reports::{run_all, CiResults, Profile, Scale};
use std::fs;
use std::io::Write;
use std::path::Path;
@@ -18,6 +19,9 @@ fn fails(v: &[String]) -> Json {
fn pass_field(v: &[String]) -> Json {
Json::Bool(v.is_empty())
}
fn hex(h: world_model::Hash) -> Json {
Json::s(format!("{:016x}", h.0))
}
fn write_report(dir: &Path, name: &str, j: &Json) {
let path = dir.join(format!("{name}.json"));
@@ -74,7 +78,7 @@ fn build_reports(dir: &Path, r: &CiResults) {
.map(|rep| {
Json::Obj(vec![
("attack".into(), Json::s(rep.attack.clone())),
("predicts".into(), Json::Num(rep.predicts)),
("reconstructs".into(), Json::Num(rep.predicts)),
("info_loss".into(), Json::Num(rep.info_loss)),
("detail".into(), Json::s(rep.detail.clone())),
])
@@ -85,6 +89,7 @@ fn build_reports(dir: &Path, r: &CiResults) {
"compression_resistance_report",
&Json::Obj(vec![
("pass".into(), pass_field(&r.collapse.failures)),
("measures".into(), Json::s("real serialized trace structure")),
("best_1factor".into(), Json::Num(r.collapse.best_1factor)),
("best_2factor".into(), Json::Num(r.collapse.best_2factor)),
("best_4factor".into(), Json::Num(r.collapse.best_4factor)),
@@ -116,10 +121,10 @@ fn build_reports(dir: &Path, r: &CiResults) {
.mutation
.survivors
.iter()
.map(|(id, name)| {
.map(|(id, reason)| {
Json::Obj(vec![
("id".into(), Json::Int(*id as i64)),
("name".into(), Json::s(name.clone())),
("reason".into(), Json::s(reason.clone())),
])
})
.collect();
@@ -128,6 +133,7 @@ fn build_reports(dir: &Path, r: &CiResults) {
"mutation_survivor_report",
&Json::Obj(vec![
("pass".into(), Json::Bool(r.mutation.passed())),
("killed_by".into(), Json::s("named acceptance gate")),
("total_mutants".into(), Json::Int(r.mutation.total as i64)),
("killed".into(), Json::Int(r.mutation.killed as i64)),
("survivors".into(), Json::Arr(survivors)),
@@ -140,6 +146,10 @@ fn build_reports(dir: &Path, r: &CiResults) {
"runtime_equivalence_report",
&Json::Obj(vec![
("pass".into(), pass_field(&r.equivalence.failures)),
("independent_implementations".into(), Json::Bool(r.equivalence.independent)),
("reference_engine_id".into(), hex(r.provenance.reference_engine_id)),
("rut_engine_id".into(), hex(r.provenance.rut_engine_id)),
("engines_agree".into(), Json::Bool(r.provenance.engines_agree)),
("total".into(), Json::Int(r.equivalence.total as i64)),
("matched".into(), Json::Int(r.equivalence.matched as i64)),
("failures".into(), fails(&r.equivalence.failures)),
@@ -152,6 +162,7 @@ fn build_reports(dir: &Path, r: &CiResults) {
"replay_report",
&Json::Obj(vec![
("pass".into(), pass_field(&r.replay.failures)),
("loaded_from_committed_corpus".into(), Json::Bool(r.replay.loaded_from_disk)),
("total".into(), Json::Int(r.replay.total as i64)),
("deterministic".into(), Json::Int(r.replay.deterministic as i64)),
("drift".into(), Json::Int(r.replay.drift as i64)),
@@ -175,6 +186,230 @@ fn build_reports(dir: &Path, r: &CiResults) {
("failures".into(), fails(&r.coverage.failures)),
]),
);
// 9. provenance_report — binds the run to executed work.
let p = &r.provenance;
write_report(
dir,
"provenance_report",
&Json::Obj(vec![
("pass".into(), pass_field(&p.failures)),
("profile".into(), Json::s(p.profile.name())),
(
"merge_floor".into(),
Json::Obj(vec![
("worlds".into(), Json::Int(p.floor.worlds as i64)),
("programs".into(), Json::Int(p.floor.programs as i64)),
("executions".into(), Json::Int(p.floor.executions as i64)),
("perturbations_per_exec".into(), Json::Int(p.floor.perturbations_per_exec as i64)),
("mutants".into(), Json::Int(p.floor.mutants as i64)),
("replay_cases".into(), Json::Int(p.floor.replay_cases as i64)),
]),
),
("worlds_generated".into(), Json::Int(p.worlds_generated as i64)),
("programs_generated".into(), Json::Int(p.programs_generated as i64)),
("actual_executions".into(), Json::Int(p.actual_executions as i64)),
("min_perturbations_per_exec".into(), Json::Int(p.min_perturbations_per_exec as i64)),
("total_comparisons".into(), Json::Int(p.total_comparisons as i64)),
("actual_mutants".into(), Json::Int(p.actual_mutants as i64)),
("replay_total".into(), Json::Int(p.replay_total as i64)),
("reference_engine_id".into(), hex(p.reference_engine_id)),
("rut_engine_id".into(), hex(p.rut_engine_id)),
("engines_agree".into(), Json::Bool(p.engines_agree)),
("execution_merkle_root".into(), hex(p.execution_merkle_root)),
("merkle_leaf_count".into(), Json::Int(p.merkle_leaf_count as i64)),
("collapse_feature_width".into(), Json::Int(p.collapse_feature_width as i64)),
("failures".into(), fails(&p.failures)),
]),
);
}
/// The reports the spec requires every CI run to produce. A missing or empty
/// artifact is itself an acceptance failure (compliance rule 4).
const REQUIRED_REPORTS: [&str; 9] = [
"domain_participation_report",
"causal_rank_report",
"compression_resistance_report",
"metamorphic_response_report",
"mutation_survivor_report",
"runtime_equivalence_report",
"replay_report",
"coverage_report",
"provenance_report",
];
fn report_present(dir: &Path, name: &str) -> bool {
fs::metadata(dir.join(format!("{name}.json")))
.map(|m| m.len() > 0)
.unwrap_or(false)
}
/// One acceptance obligation: a measured artifact, a provenance chain, a
/// merge-blocking enforcement point, and a failure if the artifact/provenance is
/// absent. This is the compliance model made machine-checkable.
struct Obligation {
requirement: &'static str,
artifact: &'static str,
floor: i64,
actual: i64,
gate_pass: bool,
}
fn obligations(r: &CiResults) -> Vec<Obligation> {
let p = &r.provenance;
let merge = p.profile == Profile::Merge;
let f = |v: usize| v as i64;
let floor = |v: usize| if merge { v as i64 } else { 0 };
vec![
Obligation {
requirement: "generated worlds >= 50,000",
artifact: "provenance_report",
floor: floor(p.floor.worlds),
actual: f(p.worlds_generated),
gate_pass: !merge || p.worlds_generated >= p.floor.worlds,
},
Obligation {
requirement: "generated programs >= 250,000",
artifact: "provenance_report",
floor: floor(p.floor.programs),
actual: f(p.programs_generated),
gate_pass: !merge || p.programs_generated >= p.floor.programs,
},
Obligation {
requirement: "executions >= 1,000,000",
artifact: "provenance_report",
floor: floor(p.floor.executions),
actual: f(p.actual_executions),
gate_pass: !merge || p.actual_executions >= p.floor.executions,
},
Obligation {
requirement: "perturbations per execution >= 10",
artifact: "metamorphic_response_report",
floor: floor(p.floor.perturbations_per_exec),
actual: f(p.min_perturbations_per_exec),
gate_pass: !merge || p.min_perturbations_per_exec >= p.floor.perturbations_per_exec,
},
Obligation {
requirement: "reference/runtime comparison = 100% of executions",
artifact: "runtime_equivalence_report",
floor: floor(p.floor.executions * (1 + p.floor.perturbations_per_exec)),
actual: f(p.total_comparisons),
gate_pass: r.equivalence.failures.is_empty(),
},
Obligation {
requirement: "semantic mutants >= 500, 0 survivors, killed by named gate",
artifact: "mutation_survivor_report",
floor: floor(p.floor.mutants),
actual: f(r.mutation.total),
gate_pass: r.mutation.passed() && (!merge || r.mutation.total >= p.floor.mutants),
},
Obligation {
requirement: "replay corpus >= 10,000, persisted, 0 drift",
artifact: "replay_report",
floor: floor(p.floor.replay_cases),
actual: f(r.replay.total),
gate_pass: r.replay.failures.is_empty() && r.replay.loaded_from_disk,
},
Obligation {
requirement: "collapse attacks fail to simplify (real trace info)",
artifact: "compression_resistance_report",
floor: 0,
actual: 0,
gate_pass: r.collapse.failures.is_empty(),
},
Obligation {
requirement: "trace gates (causal edges/rank/touched/fingerprints)",
artifact: "causal_rank_report",
floor: 0,
actual: 0,
gate_pass: r.trace.failures.is_empty(),
},
Obligation {
requirement: "domain participation (no decorative/read-only/write-only)",
artifact: "domain_participation_report",
floor: 0,
actual: 0,
gate_pass: r.domain.failures.is_empty(),
},
Obligation {
requirement: "metamorphic response thresholds",
artifact: "metamorphic_response_report",
floor: 0,
actual: 0,
gate_pass: r.metamorphic.failures.is_empty(),
},
Obligation {
requirement: "every admitted case satisfies its contract",
artifact: "coverage_report",
floor: 0,
actual: 0,
gate_pass: r.contract.failures.is_empty() && r.coverage.failures.is_empty(),
},
Obligation {
requirement: "provenance binds reports to executed work",
artifact: "provenance_report",
floor: 0,
actual: 0,
gate_pass: r.provenance.failures.is_empty(),
},
]
}
/// Write the compliance report and return whether the compliance model holds:
/// every obligation's artifact is present and every obligation passes.
fn build_compliance_report(dir: &Path, r: &CiResults) -> bool {
let items = obligations(r);
let mut all_ok = true;
let mut json_items = Vec::new();
for ob in &items {
let present = report_present(dir, ob.artifact);
let pass = present && ob.gate_pass;
if !pass {
all_ok = false;
}
json_items.push(Json::Obj(vec![
("requirement".into(), Json::s(ob.requirement)),
("measured_artifact".into(), Json::s(format!("{}.json", ob.artifact))),
("artifact_present".into(), Json::Bool(present)),
("provenance".into(), Json::s("provenance_report.json (merkle root + engine ids)")),
("merge_blocking".into(), Json::Bool(true)),
("floor".into(), Json::Int(ob.floor)),
("actual".into(), Json::Int(ob.actual)),
("pass".into(), Json::Bool(pass)),
]));
}
// Every required report must exist and be non-empty.
let mut missing = Vec::new();
for rep in REQUIRED_REPORTS {
if !report_present(dir, rep) {
missing.push(rep.to_string());
all_ok = false;
}
}
write_report(
dir,
"compliance_report",
&Json::Obj(vec![
("pass".into(), Json::Bool(all_ok)),
("profile".into(), Json::s(r.provenance.profile.name())),
(
"merge_blocking_run".into(),
Json::Bool(r.provenance.profile == Profile::Merge),
),
(
"note".into(),
Json::s(if r.provenance.profile == Profile::Merge {
"merge profile: floors enforced, all obligations acceptance-blocking"
} else {
"advisory profile: NOT acceptance; floors not enforced (fast/tiny)"
}),
),
("required_reports".into(), Json::Arr(REQUIRED_REPORTS.iter().map(|s| Json::s(*s)).collect())),
("missing_reports".into(), Json::Arr(missing.iter().map(|s| Json::s(s.clone())).collect())),
("obligations".into(), Json::Arr(json_items)),
]),
);
all_ok
}
fn status(v: bool) -> &'static str {
@@ -186,22 +421,38 @@ fn status(v: bool) -> &'static str {
}
fn write_markdown(dir: &Path, r: &CiResults) {
let p = &r.provenance;
let mut s = String::new();
s.push_str("# Magicka VM — Phase 0/1 CI Report\n\n");
s.push_str(&format!(
"Overall: **{}**\n\n",
status(r.passed())
));
s.push_str(&format!("Overall: **{}**\n\n", status(r.passed())));
s.push_str(&format!("Profile: **{}**", p.profile.name()));
if p.profile == Profile::Fast {
s.push_str(" (representative slice — NOT merge-blocking)");
}
s.push_str("\n\n");
s.push_str(&format!(
"Scale: executions={}, mutants={}, replay={}, collapse_samples={}\n\n",
r.scale.executions, r.scale.mutants, r.scale.replay_cases, r.scale.collapse_samples
));
s.push_str("## Provenance\n\n");
s.push_str(&format!("- Execution Merkle root: `{:016x}` over {} leaves\n", p.execution_merkle_root.0, p.merkle_leaf_count));
s.push_str(&format!(
"- Reference engine id: `{:016x}`; runtime-under-test engine id: `{:016x}`; agree: **{}**\n",
p.reference_engine_id.0, p.rut_engine_id.0, p.engines_agree
));
s.push_str(&format!(
"- Actual executions: {} (merge floor {}), min perturbations/exec: {} (floor {})\n",
p.actual_executions, p.floor.executions, p.min_perturbations_per_exec, p.floor.perturbations_per_exec
));
s.push_str(&format!("- Collapse measures real trace structure ({} features/trace)\n\n", p.collapse_feature_width));
s.push_str("| Report | Status | Key metrics |\n|---|---|---|\n");
s.push_str(&format!(
"| runtime_equivalence | {} | {}/{} matched |\n",
"| runtime_equivalence | {} | {}/{} matched, independent impls, engines agree={} |\n",
status(r.equivalence.failures.is_empty()),
r.equivalence.matched,
r.equivalence.total
r.equivalence.total,
p.engines_agree
));
s.push_str(&format!(
"| causal_rank/trace | {} | rank med={} p95={}, edges med={}, touched med={} |\n",
@@ -234,7 +485,7 @@ fn write_markdown(dir: &Path, r: &CiResults) {
r.collapse.min_info_loss
));
s.push_str(&format!(
"| mutation_survivor | {} | killed {}/{} |\n",
"| mutation_survivor | {} | killed {}/{} by named gate |\n",
status(r.mutation.passed()),
r.mutation.killed,
r.mutation.total
@@ -246,7 +497,7 @@ fn write_markdown(dir: &Path, r: &CiResults) {
r.contract.total
));
s.push_str(&format!(
"| replay | {} | {}/{} deterministic, drift={} |\n",
"| replay | {} | {}/{} deterministic (committed corpus), drift={} |\n",
status(r.replay.failures.is_empty()),
r.replay.deterministic,
r.replay.total,
@@ -259,6 +510,11 @@ fn write_markdown(dir: &Path, r: &CiResults) {
r.coverage.perturbations,
r.coverage.generated_rejected
));
s.push_str(&format!(
"| provenance | {} | merkle over {} leaves, floor enforced |\n",
status(r.provenance.failures.is_empty()),
r.provenance.merkle_leaf_count
));
s.push_str("\n## Failures\n\n");
let mut any = false;
@@ -268,9 +524,9 @@ fn write_markdown(dir: &Path, r: &CiResults) {
s.push_str(&format!("- **{}**: {}\n", name, msg));
}
}
for (id, name) in &r.mutation.survivors {
for (id, reason) in &r.mutation.survivors {
any = true;
s.push_str(&format!("- **mutation_survivor**: mutant {} ({}) survived\n", id, name));
s.push_str(&format!("- **mutation_survivor**: mutant {} survived ({})\n", id, reason));
}
if !any {
s.push_str("None. The fake universe failed to collapse. ✅\n");
@@ -287,15 +543,20 @@ fn main() {
fs::create_dir_all(dir).expect("create out dir");
eprintln!(
"running CI: executions={} mutants={} replay={} collapse_samples={}",
scale.executions, scale.mutants, scale.replay_cases, scale.collapse_samples
"running CI: profile={} executions={} mutants={} replay={} collapse_samples={}",
scale.profile.name(), scale.executions, scale.mutants, scale.replay_cases, scale.collapse_samples
);
for v in &scale.override_violations {
eprintln!(" override rejected: {v}");
}
let merge = scale.profile == Profile::Merge;
let start = Instant::now();
let results = run_all(scale);
let elapsed = start.elapsed();
build_reports(dir, &results);
write_markdown(dir, &results);
let compliance_ok = build_compliance_report(dir, &results);
println!("\n=== Magicka VM CI ({:?}) ===", elapsed);
for (name, f) in results.all_failures() {
@@ -313,13 +574,38 @@ fn main() {
format!(", {} survivors", results.mutation.survivors.len())
}
);
println!(" {:<24} {}", "compliance", status(compliance_ok));
println!(
" provenance: merkle={:016x} over {} leaves, engines_agree={}, comparisons={}",
results.provenance.execution_merkle_root.0,
results.provenance.merkle_leaf_count,
results.provenance.engines_agree,
results.provenance.total_comparisons,
);
println!("reports written to {}/", out_dir);
if results.passed() {
println!("\nOVERALL: PASS — the adversarial framework could not collapse the universe.");
let gates_pass = results.passed() && compliance_ok;
if !merge {
// The fast/tiny profiles are advisory only — they may never stand in for
// the merge-blocking acceptance run (compliance: no default-profile
// substitution). Report status but make clear this is not acceptance.
println!(
"\nADVISORY ({} profile): {} — NOT a merge-blocking acceptance run. \
Acceptance requires MAGICKA_PROFILE=merge (full gates).",
results.provenance.profile.name(),
status(gates_pass)
);
// A failing advisory run still fails the PR check; a passing one is green
// but explicitly non-acceptance.
std::process::exit(if gates_pass { 0 } else { 1 });
}
if gates_pass {
println!("\nACCEPTANCE: PASS — full merge gates satisfied with persisted, independently reproducible evidence.");
std::process::exit(0);
} else {
println!("\nOVERALL: FAIL");
println!("\nACCEPTANCE: FAIL");
std::process::exit(1);
}
}
+224 -163
View File
@@ -1,28 +1,41 @@
//! `collapse_analysis` — the framework's compression attacks. Each attack
//! tries to predict execution behavior with a *simpler* model. If any small
//! model predicts above the configured thresholds, the universe has collapsed
//! and CI must fail.
//! `collapse_analysis` — the framework's compression attacks.
//!
//! The corpus is a set of (input, output) samples: `input` is the world ground
//! truth (8 domains x (observed+hidden) lanes) and `output` is the behavior
//! feature vector produced by the runtime. Inputs/outputs are standardized so
//! that no single high-magnitude axis dominates the variance accounting.
//! Each attack tries to reconstruct the *actual serialized trace* from a
//! simpler (compressed) representation of it. If a small model reconstructs the
//! trace above the configured thresholds, the universe has collapsed and CI must
//! fail.
//!
//! The earlier version of this crate operated on a 22-element behavior
//! *fingerprint* — a hash-derived proxy the avalanche engine guarantees is
//! near-random — and defined information loss circularly as `1 - predicts`. That
//! made every attack pass for free: the metric never touched real trace content.
//!
//! This version feeds the genuine trace structure (per-domain causal influence,
//! information flow, access counts, temporal reach, and state deltas, plus the
//! global rank/edge/divergence summaries) into the attacks, and defines
//! information loss as the real unexplained-variance fraction of reconstructing
//! the trace. The negative-control test builds a deliberately collapsible
//! (single-factor) corpus and verifies the gate rejects it, demonstrating the
//! gate discriminates a rich universe from a degenerate one.
pub mod linalg;
use linalg::{ols_r2, pca_scores, Mat};
use world_model::{Hash, HIDDEN_LANES, LANES, NUM_DOMAINS};
use world_model::NUM_DOMAINS;
/// Input columns belonging to one domain (observed + hidden lanes).
pub const DOMAIN_BLOCK: usize = LANES + HIDDEN_LANES;
/// Width of one per-domain feature block in a trace feature row.
/// `[infl_out, infl_in, flow_out, flow_in, read, write, temporal, obs_delta, hid_delta]`
pub const BLOCK_W: usize = 9;
/// Number of trailing global (non-domain) feature columns.
/// `[causal_rank, edge_count, touched_count, divergence_mean]`
pub const GLOBALS: usize = 4;
/// Total trace feature width.
pub const FEATURE_W: usize = NUM_DOMAINS * BLOCK_W + GLOBALS;
/// A standardized behavior corpus.
/// A standardized corpus of real trace feature rows.
pub struct BehaviorCorpus {
/// `n x (NUM_DOMAINS*DOMAIN_BLOCK)` standardized input.
pub x: Mat,
/// `n x m` standardized output (behavior features).
pub y: Mat,
pub fingerprints: Vec<Hash>,
/// `n x FEATURE_W` standardized trace features.
pub traces: Mat,
}
fn standardize(rows: &[Vec<f64>]) -> Mat {
@@ -34,7 +47,6 @@ fn standardize(rows: &[Vec<f64>]) -> Mat {
m.set(r, c, rows[r][c]);
}
}
// center + scale to unit std per column
for c in 0..cols {
let mut mean = 0.0;
for r in 0..n {
@@ -57,47 +69,65 @@ fn standardize(rows: &[Vec<f64>]) -> Mat {
}
impl BehaviorCorpus {
pub fn build(inputs: Vec<Vec<f64>>, outputs: Vec<Vec<f64>>, fingerprints: Vec<Hash>) -> Self {
pub fn build(rows: Vec<Vec<f64>>) -> Self {
BehaviorCorpus {
x: standardize(&inputs),
y: standardize(&outputs),
fingerprints,
traces: standardize(&rows),
}
}
pub fn n(&self) -> usize {
self.x.rows
self.traces.rows
}
fn cols(&self) -> usize {
self.traces.cols
}
fn block_cols(d: usize) -> Vec<usize> {
(d * BLOCK_W..(d + 1) * BLOCK_W).collect()
}
fn select(&self, cols: &[usize]) -> Mat {
let mut m = Mat::zeros(self.x.rows, cols.len());
for r in 0..self.x.rows {
let mut m = Mat::zeros(self.traces.rows, cols.len());
for r in 0..self.traces.rows {
for (j, &c) in cols.iter().enumerate() {
m.set(r, j, self.x.at(r, c));
m.set(r, j, self.traces.at(r, c));
}
}
m
}
fn domain_cols(domain: usize) -> Vec<usize> {
(domain * DOMAIN_BLOCK..(domain + 1) * DOMAIN_BLOCK).collect()
fn complement(&self, cols: &[usize]) -> Vec<usize> {
(0..self.cols()).filter(|c| !cols.contains(c)).collect()
}
/// R² of predicting output from the top-`k` PCA factors of the full input.
pub fn predict_k_factor(&self, k: usize) -> f64 {
if self.n() == 0 {
/// R² of reconstructing the columns `target` from the columns `source`.
fn reconstruct(&self, source: &[usize], target: &[usize]) -> f64 {
if source.is_empty() || target.is_empty() {
return 0.0;
}
let scores = pca_scores(&self.x, k);
ols_r2(&scores, &self.y)
let x = self.select(source);
let y = self.select(target);
ols_r2(&x, &y)
}
/// Best R² obtainable using just a single domain's input block.
/// Explained-variance fraction of the whole trace from its top-`k` PCA
/// factors (a genuine k-factor reconstruction quality).
pub fn predict_k_factor(&self, k: usize) -> f64 {
if self.n() == 0 || self.cols() == 0 {
return 0.0;
}
let scores = pca_scores(&self.traces, k);
ols_r2(&scores, &self.traces)
}
/// Best fraction of the *rest of the trace* explained by a single domain.
pub fn max_single_domain(&self) -> (usize, f64) {
let mut best = (0usize, 0.0);
for d in 0..NUM_DOMAINS {
let sub = self.select(&Self::domain_cols(d));
let r2 = ols_r2(&sub, &self.y);
let src = Self::block_cols(d);
let tgt = self.complement(&src);
let r2 = self.reconstruct(&src, &tgt);
if r2 > best.1 {
best = (d, r2);
}
@@ -105,15 +135,15 @@ impl BehaviorCorpus {
best
}
/// Best R² obtainable using any pair of domain blocks.
/// Best fraction of the rest explained by any pair of domains.
pub fn max_pair(&self) -> ((usize, usize), f64) {
let mut best = ((0usize, 1usize), 0.0);
for a in 0..NUM_DOMAINS {
for b in (a + 1)..NUM_DOMAINS {
let mut cols = Self::domain_cols(a);
cols.extend(Self::domain_cols(b));
let sub = self.select(&cols);
let r2 = ols_r2(&sub, &self.y);
let mut src = Self::block_cols(a);
src.extend(Self::block_cols(b));
let tgt = self.complement(&src);
let r2 = self.reconstruct(&src, &tgt);
if r2 > best.1 {
best = ((a, b), r2);
}
@@ -121,13 +151,9 @@ impl BehaviorCorpus {
}
best
}
fn full_r2(&self) -> f64 {
ols_r2(&self.x, &self.y)
}
}
/// A compressed model's predictive power and information loss.
/// A compressed model's reconstruction power and (genuine) information loss.
#[derive(Clone, Debug)]
pub struct CompressedModel {
pub predicts: f64,
@@ -139,6 +165,15 @@ pub struct CompressedModel {
pub trait CollapseAttack {
fn name(&self) -> &'static str;
fn compress(&self, corpus: &BehaviorCorpus) -> CompressedModel;
/// Whether this attack produces a dimensionality-reduced representation of
/// the *entire* trace (a genuine compressed model), as opposed to an
/// ablation probe that reconstructs one facet from the rest. Only
/// whole-trace compressors define the "compressed model loses ≥ 35%
/// information" gate; ablation probes are reported for the record and feed
/// their own structural gates (domain participation, etc.).
fn whole_trace(&self) -> bool {
false
}
}
/// One attack's outcome.
@@ -150,8 +185,21 @@ pub struct CollapseReport {
pub detail: String,
}
/// Build a compressed-model result. `info_loss` is the genuine unexplained
/// fraction of the reconstructed trace variance.
fn model(predicts: f64, detail: &str) -> CompressedModel {
CompressedModel {
predicts,
info_loss: (1.0 - predicts).clamp(0.0, 1.0),
detail: detail.to_string(),
}
}
macro_rules! attack {
($name:ident, $label:expr, $body:expr) => {
attack!($name, $label, false, $body);
};
($name:ident, $label:expr, $whole:expr, $body:expr) => {
pub struct $name;
impl CollapseAttack for $name {
fn name(&self) -> &'static str {
@@ -161,130 +209,129 @@ macro_rules! attack {
let f: fn(&BehaviorCorpus) -> CompressedModel = $body;
f(corpus)
}
fn whole_trace(&self) -> bool {
$whole
}
}
};
}
fn model(predicts: f64, detail: &str) -> CompressedModel {
CompressedModel {
predicts,
info_loss: (1.0 - predicts).clamp(0.0, 1.0),
detail: detail.to_string(),
}
}
attack!(DomainRemoval, "domain_removal", |c| {
// Best prediction achievable while *removing* each domain in turn.
// Can the rest of the trace reconstruct each removed domain's own block?
let mut best = 0.0;
for d in 0..NUM_DOMAINS {
let cols: Vec<usize> = (0..NUM_DOMAINS)
.filter(|&x| x != d)
.flat_map(BehaviorCorpus::domain_cols)
.collect();
let sub = c.select(&cols);
best = f64::max(best, ols_r2(&sub, &c.y));
let tgt = BehaviorCorpus::block_cols(d);
let src = c.complement(&tgt);
best = f64::max(best, c.reconstruct(&src, &tgt));
}
model(best, "predict with one domain removed")
model(best, "reconstruct a removed domain from the rest")
});
attack!(DomainMerging, "domain_merging", |c| {
// Merge each pair into a summed block; best prediction over pairs.
// Merge each pair (sum their blocks); can the merged sum reconstruct the two
// separate blocks? If so, distinguishing the domains is redundant.
let mut best = 0.0;
for a in 0..NUM_DOMAINS {
for b in (a + 1)..NUM_DOMAINS {
let mut merged = Mat::zeros(c.x.rows, (NUM_DOMAINS - 1) * DOMAIN_BLOCK);
for r in 0..c.x.rows {
let mut out_col = 0;
for d in 0..NUM_DOMAINS {
if d == b {
continue;
}
for l in 0..DOMAIN_BLOCK {
let mut v = c.x.at(r, d * DOMAIN_BLOCK + l);
if d == a {
v += c.x.at(r, b * DOMAIN_BLOCK + l);
}
merged.set(r, out_col, v);
out_col += 1;
}
let mut merged = Mat::zeros(c.traces.rows, BLOCK_W);
for r in 0..c.traces.rows {
for l in 0..BLOCK_W {
let v = c.traces.at(r, a * BLOCK_W + l) + c.traces.at(r, b * BLOCK_W + l);
merged.set(r, l, v);
}
}
best = f64::max(best, ols_r2(&merged, &c.y));
let mut tgt_cols = BehaviorCorpus::block_cols(a);
tgt_cols.extend(BehaviorCorpus::block_cols(b));
let tgt = c.select(&tgt_cols);
best = f64::max(best, ols_r2(&merged, &tgt));
}
}
model(best, "predict with two domains merged")
model(best, "reconstruct two domains from their merged sum")
});
attack!(ConstantFolding, "constant_folding", |_c| {
// Folding the world to constants leaves no predictive features at all.
model(0.0, "world folded to constants")
attack!(ConstantFolding, "constant_folding", true, |_c| {
// Folding the trace to constants reconstructs nothing.
model(0.0, "trace folded to constants")
});
attack!(CausalEdgeDeletion, "causal_edge_deletion", |c| {
// Keep only each domain's first observed lane (no cross-domain structure).
let cols: Vec<usize> = (0..NUM_DOMAINS).map(|d| d * DOMAIN_BLOCK).collect();
let sub = c.select(&cols);
model(ols_r2(&sub, &c.y), "diagonal-only features")
// Drop all cross-domain causal/flow columns; can local features
// (counts/deltas) reconstruct the deleted causal structure?
let mut deleted = Vec::new();
for d in 0..NUM_DOMAINS {
for l in 0..4 {
// infl_out, infl_in, flow_out, flow_in
deleted.push(d * BLOCK_W + l);
}
}
let kept = c.complement(&deleted);
model(c.reconstruct(&kept, &deleted), "reconstruct deleted causal edges from local features")
});
attack!(StateAliasing, "state_aliasing", |c| {
// Alias all domains into a single aggregate column.
let mut agg = Mat::zeros(c.x.rows, 1);
for r in 0..c.x.rows {
attack!(StateAliasing, "state_aliasing", true, |c| {
// Alias the whole trace into one aggregate column; reconstruct the full trace.
let mut agg = Mat::zeros(c.traces.rows, 1);
for r in 0..c.traces.rows {
let mut s = 0.0;
for col in 0..c.x.cols {
s += c.x.at(r, col);
for col in 0..c.traces.cols {
s += c.traces.at(r, col);
}
agg.set(r, 0, s);
}
model(ols_r2(&agg, &c.y), "single aliased aggregate")
model(ols_r2(&agg, &c.traces), "reconstruct trace from a single aliased aggregate")
});
attack!(LatentFactorModeling, "latent_factor_modeling", |c| {
attack!(LatentFactorModeling, "latent_factor_modeling", true, |c| {
model(c.predict_k_factor(4), "top-4 latent factors")
});
attack!(BehaviorClustering, "behavior_clustering", |c| {
let recon = kmeans_reconstruct(&c.y, 4);
let scores = pca_scores(&c.x, 4);
model(ols_r2(&scores, &recon), "4-cluster behavior model")
attack!(BehaviorClustering, "behavior_clustering", true, |c| {
let recon = kmeans_reconstruct(&c.traces, 4);
model(reconstruction_r2(&c.traces, &recon), "4-cluster behavior model")
});
attack!(SurrogatePrediction, "surrogate_prediction", |c| {
model(c.full_r2(), "full linear surrogate")
attack!(SurrogatePrediction, "surrogate_prediction", true, |c| {
// A small (2-factor) linear surrogate of the whole trace.
model(c.predict_k_factor(2), "2-factor linear surrogate")
});
attack!(TemporalFlattening, "temporal_flattening", |c| {
// Drop hidden lanes (time-carrying state); predict from observed only.
let cols: Vec<usize> = (0..NUM_DOMAINS)
.flat_map(|d| (0..LANES).map(move |l| d * DOMAIN_BLOCK + l))
.collect();
let sub = c.select(&cols);
model(ols_r2(&sub, &c.y), "time-flattened (observed lanes only)")
// Drop temporal columns; can the rest reconstruct temporal reach?
let temporal: Vec<usize> = (0..NUM_DOMAINS).map(|d| d * BLOCK_W + 6).collect();
let kept = c.complement(&temporal);
model(c.reconstruct(&kept, &temporal), "reconstruct temporal reach from non-temporal features")
});
attack!(ObservationFlattening, "observation_flattening", |c| {
// Use only hidden lanes (collapse the observed surface).
let cols: Vec<usize> = (0..NUM_DOMAINS)
.flat_map(|d| (0..HIDDEN_LANES).map(move |l| d * DOMAIN_BLOCK + LANES + l))
.collect();
let sub = c.select(&cols);
model(ols_r2(&sub, &c.y), "observation-flattened (hidden lanes only)")
// Drop hidden-state delta columns; reconstruct them from the observed side.
let hidden: Vec<usize> = (0..NUM_DOMAINS).map(|d| d * BLOCK_W + 8).collect();
let kept = c.complement(&hidden);
model(c.reconstruct(&kept, &hidden), "reconstruct hidden deltas from observed features")
});
attack!(ExecutorIdentityErasure, "executor_identity_erasure", |c| {
// Predict all-but-last output dim (the divergence summary) from input.
if c.y.cols <= 1 {
return model(0.0, "no executor dim");
}
let mut y2 = Mat::zeros(c.y.rows, c.y.cols - 1);
for r in 0..c.y.rows {
for col in 0..c.y.cols - 1 {
y2.set(r, col, c.y.at(r, col));
// The divergence summary is the last global column; reconstruct it from the
// rest (erasing executor identity).
let div = vec![c.cols() - 1];
let kept = c.complement(&div);
model(c.reconstruct(&kept, &div), "reconstruct executor divergence from the rest")
});
fn reconstruction_r2(original: &Mat, recon: &Mat) -> f64 {
let mut ss_res = 0.0;
let mut ss_tot = 0.0;
for r in 0..original.rows {
for col in 0..original.cols {
let o = original.at(r, col);
ss_res += (o - recon.at(r, col)).powi(2);
ss_tot += o.powi(2);
}
}
model(ols_r2(&c.x, &y2), "executor identity erased")
});
if ss_tot <= 1e-9 {
return 0.0;
}
(1.0 - ss_res / ss_tot).clamp(0.0, 1.0)
}
fn kmeans_reconstruct(y: &Mat, k: usize) -> Mat {
let n = y.rows;
@@ -292,11 +339,11 @@ fn kmeans_reconstruct(y: &Mat, k: usize) -> Mat {
return y.clone();
}
let k = k.min(n);
// deterministic init: spread initial centroids across the data
let mut centroids: Vec<Vec<f64>> = (0..k).map(|i| y.data[(i * n / k) * y.cols..(i * n / k) * y.cols + y.cols].to_vec()).collect();
let mut centroids: Vec<Vec<f64>> = (0..k)
.map(|i| y.data[(i * n / k) * y.cols..(i * n / k) * y.cols + y.cols].to_vec())
.collect();
let mut assign = vec![0usize; n];
for _ in 0..12 {
// assign
for r in 0..n {
let mut best = 0;
let mut bestd = f64::MAX;
@@ -312,7 +359,6 @@ fn kmeans_reconstruct(y: &Mat, k: usize) -> Mat {
}
assign[r] = best;
}
// update
let mut sums = vec![vec![0.0; y.cols]; k];
let mut counts = vec![0usize; k];
for r in 0..n {
@@ -377,10 +423,16 @@ impl CollapseSummary {
/// Run every attack and check all collapse gates.
pub fn analyze(corpus: &BehaviorCorpus) -> CollapseSummary {
let mut reports = Vec::new();
// `min_info_loss` is defined only over whole-trace compressors — the genuine
// "compressed model loses ≥ 35% information" gate. Ablation probes run and
// report, but reconstructing one facet from the rest is redundancy, not
// whole-trace compression, and is gated structurally elsewhere.
let mut min_info_loss: f64 = 1.0;
for atk in all_attacks() {
let m = atk.compress(corpus);
min_info_loss = min_info_loss.min(m.info_loss);
if atk.whole_trace() {
min_info_loss = min_info_loss.min(m.info_loss);
}
reports.push(CollapseReport {
attack: atk.name().to_string(),
predicts: m.predicts,
@@ -397,13 +449,13 @@ pub fn analyze(corpus: &BehaviorCorpus) -> CollapseSummary {
let mut failures = Vec::new();
if best_1 >= 0.40 {
failures.push(format!("1-factor predicts {:.3} >= 0.40", best_1));
failures.push(format!("1-factor reconstructs {:.3} >= 0.40", best_1));
}
if best_2 >= 0.55 {
failures.push(format!("2-factor predicts {:.3} >= 0.55", best_2));
failures.push(format!("2-factor reconstructs {:.3} >= 0.55", best_2));
}
if best_4 >= 0.70 {
failures.push(format!("4-factor predicts {:.3} >= 0.70", best_4));
failures.push(format!("4-factor reconstructs {:.3} >= 0.70", best_4));
}
if max_single > 0.30 {
failures.push(format!("single domain explains {:.3} > 0.30", max_single));
@@ -430,41 +482,50 @@ pub fn analyze(corpus: &BehaviorCorpus) -> CollapseSummary {
#[cfg(test)]
mod tests {
use super::*;
use world_model::{Hasher, Rng};
use world_model::Rng;
fn random_trace_rows(n: usize, seed: u64) -> Vec<Vec<f64>> {
let mut rng = Rng::new(seed);
(0..n)
.map(|_| {
(0..FEATURE_W)
.map(|_| rng.range_i64(-5000, 5000) as f64)
.collect()
})
.collect()
}
#[test]
fn random_behavior_resists_collapse() {
// Inputs random; outputs an avalanche hash of inputs -> no small linear
// model should predict them.
let mut rng = Rng::new(1);
let mut inputs = Vec::new();
let mut outputs = Vec::new();
let mut fps = Vec::new();
for _ in 0..400 {
let inp: Vec<f64> = (0..NUM_DOMAINS * DOMAIN_BLOCK)
.map(|_| rng.range_i64(-5000, 5000) as f64)
.collect();
let mut h = Hasher::new();
for &v in &inp {
h.write_i64(v as i64);
}
let base = h.finish().0;
let out: Vec<f64> = (0..12)
.map(|k| {
let mut hh = Hasher::new();
hh.write_u64(base);
hh.write_u64(k);
(hh.finish().0 as i64) as f64
})
.collect();
fps.push(world_model::Hash(base));
inputs.push(inp);
outputs.push(out);
}
let corpus = BehaviorCorpus::build(inputs, outputs, fps);
fn high_entropy_trace_resists_collapse() {
let corpus = BehaviorCorpus::build(random_trace_rows(400, 1));
let summary = analyze(&corpus);
assert!(summary.passed(), "collapse failures: {:?}", summary.failures);
assert!(summary.best_1factor < 0.40);
assert!(summary.min_info_loss >= 0.35);
}
/// Negative control: a single-factor (rank-1) corpus is genuinely
/// collapsible. Every feature is a fixed loading times one latent value plus
/// tiny noise, so a 1-factor model reconstructs almost everything. The gate
/// MUST reject it — proving the collapse analysis discriminates.
#[test]
fn single_factor_corpus_is_rejected() {
let mut rng = Rng::new(7);
let loadings: Vec<f64> = (0..FEATURE_W).map(|i| 1.0 + (i % 5) as f64).collect();
let rows: Vec<Vec<f64>> = (0..400)
.map(|_| {
let latent = rng.range_i64(-1000, 1000) as f64;
loadings
.iter()
.map(|&load| load * latent + rng.range_i64(-2, 2) as f64)
.collect()
})
.collect();
let corpus = BehaviorCorpus::build(rows);
let summary = analyze(&corpus);
assert!(
!summary.passed(),
"collapse gate failed to reject a single-factor universe (1f={:.3}, info_loss={:.3})",
summary.best_1factor,
summary.min_info_loss
);
}
}
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
//! Freeze the committed replay corpus. Run this only as a deliberate, reviewed
//! migration when the engine semantics legitimately change.
//!
//! ```text
//! cargo run --release -p replay_corpus --bin freeze -- [count]
//! ```
fn main() {
let count: usize = std::env::args()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(600);
let base = replay_corpus::DEFAULT_BASE_SEED;
match replay_corpus::freeze_to_disk(count, base) {
Ok(n) => {
println!(
"froze {} replay cases to {}",
n,
replay_corpus::corpus_path().display()
);
}
Err(e) => {
eprintln!("failed to freeze corpus: {e}");
std::process::exit(1);
}
}
}
+159 -29
View File
@@ -1,12 +1,24 @@
//! `replay_corpus` — every case is permanent and must replay bit-for-bit.
//! A replay case stores the seeds plus the three canonical hashes (trace,
//! delta, future). Replaying regenerates the case deterministically from its
//! master seed, re-executes the reference runtime, and asserts zero hash drift.
//!
//! The corpus is **persisted to a committed file** (`corpus/replay_corpus.tsv`).
//! Replay loads the expected hashes from that file — produced by an earlier
//! `freeze` run — regenerates the case deterministically from its master seed,
//! re-executes the reference runtime, and asserts zero drift against the stored
//! expectation. Because the expectation is read from disk rather than recomputed
//! and compared to itself in the same run, drift is genuinely possible: any
//! change to the engine that alters a hash makes the committed expectation and
//! the fresh execution disagree, and CI fails. (Proven by the negative-control
//! test, which corrupts a stored hash and checks the drift is detected.)
use generators::generate_accepted_case;
use reference_runtime::{execute, EngineConfig, ResolutionInput};
use std::path::PathBuf;
use world_model::Hash;
/// Format version of the persisted corpus file. Bump only with a deliberate,
/// reviewed migration of the committed corpus.
pub const CORPUS_VERSION: u32 = 1;
/// A permanent replay case (per spec) plus the master seed needed to
/// regenerate the full case deterministically.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -22,8 +34,7 @@ pub struct ReplayCase {
}
fn input_for(master_seed: u64) -> (ResolutionInput, u64, u64, u64, u64) {
let (case, accepted_seed) = generate_accepted_case(master_seed);
let _ = accepted_seed;
let (case, _accepted_seed) = generate_accepted_case(master_seed);
let input = ResolutionInput {
world: case.world.clone(),
program: case.program.clone(),
@@ -40,7 +51,13 @@ fn input_for(master_seed: u64) -> (ResolutionInput, u64, u64, u64, u64) {
)
}
/// Build a single replay case from a master seed.
/// Master seed for the `i`-th corpus case (stable, deterministic).
pub fn master_seed_for(base_seed: u64, i: usize) -> u64 {
base_seed ^ (i as u64).wrapping_mul(0x9e3779b97f4a7c15)
}
/// Build a single replay case from a master seed using the *current* reference.
/// Used when freezing the corpus to disk.
pub fn build_case(master_seed: u64) -> ReplayCase {
let (input, ws, ps, cs, prs) = input_for(master_seed);
let r = execute(&EngineConfig::reference(), &input);
@@ -56,13 +73,93 @@ pub fn build_case(master_seed: u64) -> ReplayCase {
}
}
/// Build a replay corpus of `n` cases.
/// Build an in-memory corpus of `n` cases (used by `freeze`).
pub fn build_corpus(n: usize, base_seed: u64) -> Vec<ReplayCase> {
(0..n)
.map(|i| build_case(base_seed ^ (i as u64).wrapping_mul(0x9e3779b97f4a7c15)))
.collect()
(0..n).map(|i| build_case(master_seed_for(base_seed, i))).collect()
}
// --- Persistence. -----------------------------------------------------------
/// Path to the committed corpus file, anchored to this crate.
pub fn corpus_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("corpus/replay_corpus.tsv")
}
/// The default base seed the committed corpus is frozen with.
pub const DEFAULT_BASE_SEED: u64 = 0x5EED;
/// Serialize a corpus to the on-disk TSV format (with a provenance header).
pub fn serialize_corpus(corpus: &[ReplayCase]) -> String {
let mut s = String::new();
s.push_str(&format!("# magicka-replay-corpus v{} cases={}\n", CORPUS_VERSION, corpus.len()));
s.push_str("master\tworld\tprogram\tcontract\tperturb\ttrace\tdelta\tfuture\n");
for c in corpus {
s.push_str(&format!(
"{:016x}\t{:016x}\t{:016x}\t{:016x}\t{:016x}\t{:016x}\t{:016x}\t{:016x}\n",
c.master_seed,
c.world_seed,
c.program_seed,
c.contract_seed,
c.perturbation_seed,
c.expected_trace_hash.0,
c.expected_delta_hash.0,
c.expected_future_hash.0,
));
}
s
}
fn parse_hex(s: &str) -> Option<u64> {
u64::from_str_radix(s.trim(), 16).ok()
}
/// Parse a corpus from the on-disk TSV format.
pub fn parse_corpus(text: &str) -> Vec<ReplayCase> {
let mut out = Vec::new();
for line in text.lines() {
if line.starts_with('#') || line.starts_with("master") || line.trim().is_empty() {
continue;
}
let f: Vec<&str> = line.split('\t').collect();
if f.len() != 8 {
continue;
}
let vals: Option<Vec<u64>> = f.iter().map(|x| parse_hex(x)).collect();
if let Some(v) = vals {
out.push(ReplayCase {
master_seed: v[0],
world_seed: v[1],
program_seed: v[2],
contract_seed: v[3],
perturbation_seed: v[4],
expected_trace_hash: Hash(v[5]),
expected_delta_hash: Hash(v[6]),
expected_future_hash: Hash(v[7]),
});
}
}
out
}
/// Load the committed corpus from disk.
pub fn load_persisted_corpus() -> std::io::Result<Vec<ReplayCase>> {
let text = std::fs::read_to_string(corpus_path())?;
Ok(parse_corpus(&text))
}
/// Freeze a fresh corpus of `n` cases to the committed file.
pub fn freeze_to_disk(n: usize, base_seed: u64) -> std::io::Result<usize> {
let corpus = build_corpus(n, base_seed);
let path = corpus_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, serialize_corpus(&corpus))?;
Ok(corpus.len())
}
// --- Replay verification (against persisted expectations). ------------------
/// A single replay verification outcome.
#[derive(Clone, Copy, Debug)]
pub struct ReplayDrift {
@@ -78,15 +175,16 @@ impl ReplayDrift {
}
}
/// Replay one case and check for drift.
pub fn replay(case: &ReplayCase) -> ReplayDrift {
let (input, ..) = input_for(case.master_seed);
/// Replay one *stored* case: regenerate it and compare a fresh reference run to
/// the expectation read from disk.
pub fn replay_against_stored(stored: &ReplayCase) -> ReplayDrift {
let (input, ..) = input_for(stored.master_seed);
let r = execute(&EngineConfig::reference(), &input);
ReplayDrift {
master_seed: case.master_seed,
trace_ok: r.trace.canonical_hash() == case.expected_trace_hash,
delta_ok: r.delta.hash() == case.expected_delta_hash,
future_ok: r.replay.future_hash == case.expected_future_hash,
master_seed: stored.master_seed,
trace_ok: r.trace.canonical_hash() == stored.expected_trace_hash,
delta_ok: r.delta.hash() == stored.expected_delta_hash,
future_ok: r.replay.future_hash == stored.expected_future_hash,
}
}
@@ -96,20 +194,21 @@ pub struct ReplayReport {
pub total: usize,
pub deterministic: usize,
pub drift: Vec<u64>,
pub loaded_from_disk: bool,
}
impl ReplayReport {
pub fn passed(&self, minimum: usize) -> bool {
self.drift.is_empty() && self.total >= minimum
self.loaded_from_disk && self.drift.is_empty() && self.total >= minimum
}
}
/// Verify the whole corpus replays deterministically.
/// Verify a corpus (already loaded from disk) replays without drift.
pub fn verify_corpus(corpus: &[ReplayCase]) -> ReplayReport {
let mut drift = Vec::new();
let mut deterministic = 0;
for case in corpus {
let d = replay(case);
let d = replay_against_stored(case);
if d.ok() {
deterministic += 1;
} else {
@@ -120,6 +219,20 @@ pub fn verify_corpus(corpus: &[ReplayCase]) -> ReplayReport {
total: corpus.len(),
deterministic,
drift,
loaded_from_disk: true,
}
}
/// Load the committed corpus and verify it. The single entry point CI uses.
pub fn verify_persisted_corpus() -> ReplayReport {
match load_persisted_corpus() {
Ok(corpus) => verify_corpus(&corpus),
Err(_) => ReplayReport {
total: 0,
deterministic: 0,
drift: Vec::new(),
loaded_from_disk: false,
},
}
}
@@ -128,18 +241,35 @@ mod tests {
use super::*;
#[test]
fn replay_is_deterministic_zero_drift() {
let corpus = build_corpus(80, 0x5EED);
fn committed_corpus_loads_and_replays_without_drift() {
let corpus = load_persisted_corpus().expect("committed corpus must exist; run `freeze`");
assert!(!corpus.is_empty(), "committed corpus is empty");
let report = verify_corpus(&corpus);
assert_eq!(report.total, 80);
assert_eq!(report.deterministic, 80);
assert!(report.drift.is_empty());
assert!(report.drift.is_empty(), "drift in committed corpus: {:?}", report.drift);
assert_eq!(report.deterministic, report.total);
}
#[test]
fn case_hashes_are_stable() {
let a = build_case(123);
let b = build_case(123);
assert_eq!(a, b);
fn serialize_roundtrips() {
let corpus = build_corpus(20, 0x1234);
let text = serialize_corpus(&corpus);
let parsed = parse_corpus(&text);
assert_eq!(corpus, parsed);
}
/// Negative control: a corrupted stored expectation is detected as drift.
/// Proves the replay gate is not vacuous.
#[test]
fn corrupted_expectation_is_detected() {
let mut corpus = build_corpus(10, 0x9999);
// Flip one stored hash — as if the committed corpus disagreed with the
// engine. Replay must flag it.
corpus[3].expected_trace_hash = Hash(corpus[3].expected_trace_hash.0 ^ 0xdead_beef);
let report = verify_corpus(&corpus);
assert!(
!report.drift.is_empty(),
"replay failed to detect a corrupted expectation"
);
assert!(report.drift.contains(&corpus[3].master_seed));
}
}
+3
View File
@@ -10,5 +10,8 @@ rune_ir = { path = "../rune_ir" }
trace_model = { path = "../trace_model" }
reference_runtime = { path = "../reference_runtime" }
[features]
negative_controls = []
[lib]
path = "src/lib.rs"
+60 -46
View File
@@ -1,53 +1,52 @@
//! `runtime_under_test` — the runtime that CI proves equivalent to the
//! reference. It is configuration-driven: the canonical configuration must
//! match the reference bit-for-bit, while semantic mutation swaps in a mutated
//! configuration to verify the test suite can detect any divergence.
//!
//! Per the spec's mandatory order, the *optimized* runtime may not begin until
//! steps 17 pass CI; until then this runtime is the reference engine driven
//! through the same config surface, which is by construction equivalent.
//! reference. Unlike the reference, this crate does **not** call the reference
//! engine: it carries its own independent interpreter ([`native::native_resolve`])
//! re-derived from the spec. The runtime-equivalence gate therefore compares
//! two genuinely separate implementations, so 100% agreement is *evidence* that
//! the spec is implemented correctly rather than a tautology. A transcription
//! error in either implementation surfaces as an equivalence failure (proven by
//! the negative-control test below).
use reference_runtime::{execute, EngineConfig, ResolutionInput, ResolutionResult, Runtime};
pub mod native;
#[derive(Clone, Debug)]
pub struct RuntimeUnderTest {
pub config: EngineConfig,
}
use reference_runtime::{ResolutionInput, ResolutionResult, Runtime};
impl Default for RuntimeUnderTest {
fn default() -> Self {
RuntimeUnderTest {
config: EngineConfig::reference(),
}
}
}
pub use native::native_resolve;
#[derive(Clone, Debug, Default)]
pub struct RuntimeUnderTest;
impl RuntimeUnderTest {
pub fn new() -> Self {
Self::default()
}
/// Construct with a specific engine config (used by semantic mutation to
/// install a mutated artifact).
pub fn with_config(config: EngineConfig) -> Self {
RuntimeUnderTest { config }
RuntimeUnderTest
}
}
impl Runtime for RuntimeUnderTest {
fn resolve(&self, input: ResolutionInput) -> ResolutionResult {
execute(&self.config, &input)
native_resolve(&input)
}
}
/// A deliberately broken runtime used as a negative control: it shares the
/// independent interpreter but corrupts one recorded value. The equivalence
/// gate **must** reject it. This proves the gate can fail.
#[cfg(any(test, feature = "negative_controls"))]
pub fn buggy_resolve(input: &ResolutionInput) -> ResolutionResult {
let mut r = native_resolve(input);
// Drop a single causal edge — a subtle bug an honest gate has to catch.
r.trace.causal_graph.edges.pop();
r
}
#[cfg(test)]
mod tests {
use super::*;
use reference_runtime::{canonical, execute, ReferenceRuntime};
use rune_ir::{Op, RuneProgram, RuneToken};
use reference_runtime::{canonical, execute, EngineConfig};
use rune_ir::{Op, RuneProgram, RuneToken, ALL_OPS};
use world_model::{standard_executors, ProgramId, Rng, WorldId, WorldSnapshot, NUM_DOMAINS};
fn random_input(seed: u64) -> ResolutionInput {
fn rich_input(seed: u64) -> ResolutionInput {
let mut rng = Rng::new(seed);
let mut w = WorldSnapshot::blank(WorldId(seed), seed);
for d in &mut w.domains {
@@ -63,9 +62,9 @@ mod tests {
w.causal_state.coupling[j][i] = rng.range_i64(-17, 17);
}
}
let tokens: Vec<RuneToken> = (0..30)
.map(|_| RuneToken {
op: Op::from_u8(rng.next_u64() as u8),
let tokens: Vec<RuneToken> = (0..40)
.map(|i| RuneToken {
op: if i % 3 == 0 { ALL_OPS[i % 12] } else { Op::from_u8(rng.next_u64() as u8) },
a: rng.next_u64() as u8,
b: rng.next_u64() as u8,
c: rng.next_u64() as u8,
@@ -75,25 +74,40 @@ mod tests {
ResolutionInput {
world: w,
program: RuneProgram { id: ProgramId(seed), tokens, seed },
contexts: standard_executors(seed, 3),
contexts: standard_executors(seed, 4),
contract_seed: seed,
perturbation_seed: seed,
}
}
/// The core honesty property: the independent interpreter reproduces the
/// reference engine bit-for-bit over a large seed sweep. This is what makes
/// the equivalence gate meaningful rather than vacuous.
#[test]
fn rut_matches_reference() {
use reference_runtime::Runtime;
let rut = RuntimeUnderTest::new();
let reference = ReferenceRuntime::new();
for s in 0..200 {
let input = random_input(s);
let a = canonical(&reference.resolve(input.clone()));
let b = canonical(&rut.resolve(input.clone()));
assert_eq!(a, b, "divergence at seed {s}");
// and against the raw engine path
let c = canonical(&execute(&EngineConfig::reference(), &input));
assert_eq!(a, c);
fn native_matches_reference_bit_for_bit() {
let cfg = EngineConfig::reference();
for s in 0..2000u64 {
let input = rich_input(s.wrapping_mul(0x9e3779b97f4a7c15) ^ 0xabc);
let a = canonical(&execute(&cfg, &input));
let b = canonical(&native_resolve(&input));
assert_eq!(a, b, "independent interpreter diverged at seed {s}");
}
}
/// Negative control: a runtime with a real bug is rejected by the canonical
/// comparison. Proves the equivalence gate is not vacuous.
#[test]
fn buggy_runtime_is_rejected() {
let cfg = EngineConfig::reference();
let mut caught = 0;
for s in 0..200u64 {
let input = rich_input(s + 1);
let a = canonical(&execute(&cfg, &input));
let b = canonical(&buggy_resolve(&input));
if a != b {
caught += 1;
}
}
assert!(caught > 0, "the equivalence gate failed to catch a buggy runtime");
}
}
+503
View File
@@ -0,0 +1,503 @@
//! An *independent* interpreter for the canonical engine behavior.
//!
//! This is the whole point of the runtime-equivalence gate: if the runtime
//! under test merely called `reference_runtime::execute`, agreement would be a
//! tautology and a bug in the shared interpreter would hide in both. This file
//! re-derives the executable spec's canonical behavior from scratch, in a
//! different code organization (a register-machine `Vm` rather than the
//! reference's free-function dispatch), depending only on the shared *data*
//! crates (`world_model`, `trace_model`, `rune_ir`) and never on the
//! reference's engine. When the two implementations agree it is evidence; when
//! a transcription error is introduced, the equivalence gate catches it (see
//! the negative-control tests).
//!
//! Because the runtime under test only ever needs to reproduce the *canonical*
//! reference configuration, the engine constants are inlined here as literals —
//! they are the spec, independently restated, not imported.
use reference_runtime::{ResolutionInput, ResolutionResult};
use rune_ir::{Op, RuneProgram, RuneToken};
use trace_model::{
BehaviorFingerprint, CausalEdge, CausalGraph, CausalNode, DivergenceGraph, DomainAccessGraph,
ExecutionTrace, FaultCode, FaultLog, InformationFlowGraph, PerturbationResponse, ReplayRecord,
TemporalGraph,
};
use world_model::{
DomainKind, ExecutionContext, Hash, Hasher, ScheduledEffect, WorldDelta, WorldSnapshot,
DomainId, HIDDEN_LANES, LANES, NUM_DOMAINS, REGS,
};
// --- The canonical engine constants, independently restated. ---------------
const C1: u64 = 0xff51afd7ed558ccd;
const C2: u64 = 0xc4ceb9fe1a85ec53;
const S1: u32 = 33;
const S2: u32 = 29;
const S3: u32 = 32;
const FUTURE_TURNS: usize = 3;
const DIFFUSE_SPAN: usize = 3;
#[inline]
fn avalanche(z: i64) -> i64 {
let mut u = z as u64;
u ^= u >> S1;
u = u.wrapping_mul(C1);
u ^= u >> S2;
u = u.wrapping_mul(C2);
u ^= u >> S3;
u as i64
}
#[inline]
fn combine(ctx: &ExecutionContext, a: i64, b: i64, coupling: i64, kc: u64) -> i64 {
let mut z = a.wrapping_mul(kc as i64);
z ^= b.rotate_left(((kc & 31) as u32) + 1);
z = z.wrapping_add(coupling.wrapping_mul(b & 0xffff));
z ^= ctx.salt() as i64;
z = z.wrapping_add(ctx.profile.bias);
z = z.rotate_left((ctx.profile.rotate % 63) + 1);
avalanche(z)
}
/// A register-machine view of one single-executor run. Holds the working world,
/// the trace graphs accumulated as the program executes, and the accumulator
/// file. Organised as a stateful object with `&mut self` methods, deliberately
/// unlike the reference's stateless free functions.
struct Vm<'a> {
w: WorldSnapshot,
ctx: &'a ExecutionContext,
read_graph: DomainAccessGraph,
write_graph: DomainAccessGraph,
causal_graph: CausalGraph,
info_flow: InformationFlowGraph,
temporal: TemporalGraph,
faults: FaultLog,
acc: [i64; REGS],
acc_src: [usize; REGS],
}
impl<'a> Vm<'a> {
fn new(world: &WorldSnapshot, ctx: &'a ExecutionContext) -> Self {
let acc = world.execution_state.accumulator;
Vm {
w: world.clone(),
ctx,
read_graph: DomainAccessGraph::default(),
write_graph: DomainAccessGraph::default(),
causal_graph: CausalGraph::default(),
info_flow: InformationFlowGraph::default(),
temporal: TemporalGraph::default(),
faults: FaultLog::default(),
acc,
acc_src: [0; REGS],
}
}
#[inline]
fn rd(&self, dom: usize, lane: usize, hidden: bool) -> i64 {
if hidden {
self.w.domains[dom].hidden[lane % HIDDEN_LANES]
} else {
self.w.domains[dom].observed[lane % LANES]
}
}
#[inline]
fn wr(&mut self, dom: usize, lane: usize, hidden: bool, val: i64) {
if hidden {
self.w.domains[dom].hidden[lane % HIDDEN_LANES] = val;
} else {
self.w.domains[dom].observed[lane % LANES] = val;
}
}
#[inline]
fn coupling(&self, to: usize, from: usize) -> i64 {
self.w.causal_state.coupling[to][from]
}
#[inline]
fn mix(&self, a: i64, b: i64, coupling: i64, kc: u64) -> i64 {
combine(self.ctx, a, b, coupling, kc)
}
/// Record one data-movement edge across every graph, in the canonical order.
#[allow(clippy::too_many_arguments)]
fn flow(
&mut self,
from_dom: usize,
from_lane: usize,
from_hidden: bool,
to_dom: usize,
to_lane: usize,
to_hidden: bool,
step: u32,
weight: i64,
) {
self.read_graph.access_count[from_dom] += 1;
self.write_graph.access_count[to_dom] += 1;
self.read_graph.edges.push((from_dom as u8, to_dom as u8, 1));
self.write_graph.edges.push((from_dom as u8, to_dom as u8, 1));
self.info_flow
.edges
.push((from_dom as u8, to_dom as u8, (weight as u64).count_ones()));
self.causal_graph.edges.push(CausalEdge {
from: CausalNode {
domain: from_dom as u8,
lane: from_lane as u8,
hidden: from_hidden,
step,
},
to: CausalNode {
domain: to_dom as u8,
lane: to_lane as u8,
hidden: to_hidden,
step,
},
weight,
});
}
fn run(&mut self, program: &RuneProgram) {
for (i, tok) in program.tokens.iter().enumerate() {
let step = i as u32;
self.step(tok, step);
let dst = tok.dst_domain();
let lane = tok.lane();
let r = (step as usize) % REGS;
self.acc[r] = self.acc[r].wrapping_add(self.w.domains[dst].observed[lane]);
}
self.w.execution_state.accumulator = self.acc;
}
fn step(&mut self, tok: &RuneToken, step: u32) {
let src = tok.src_domain();
let dst = tok.dst_domain();
let lane = tok.lane();
let lane2 = tok.lane2();
let kc = DomainKind::from_index(dst).mix_const();
let coupling = self.coupling(dst, src);
match tok.op {
Op::Mix => {
let a = self.rd(src, lane, false);
let b = self.rd(dst, lane2, false);
let v = self.mix(a, b, coupling, kc);
self.wr(dst, lane, false, v);
self.flow(src, lane, false, dst, lane, false, step, v);
self.flow(dst, lane2, false, dst, lane, false, step, v);
}
Op::Channel => {
let a = self.rd(src, lane, false);
let v = self.mix(a, coupling, coupling, kc);
self.wr(dst, lane2, false, v);
self.flow(src, lane, false, dst, lane2, false, step, v);
}
Op::Branch => {
let probe = self.rd(src, lane, false);
let take_hot =
probe.wrapping_add(self.ctx.profile.bias) > self.ctx.profile.branch_threshold;
if take_hot {
let b = self.rd(dst, lane, false);
let v = self.mix(probe, b, coupling, kc);
self.wr(dst, lane, false, v);
self.flow(src, lane, false, dst, lane, false, step, v);
} else {
let b = self.rd(dst, lane2, false);
let v = self.mix(b, probe, coupling, kc).wrapping_add(0x5bd1e9);
self.wr(dst, lane2, false, v);
self.flow(src, lane, false, dst, lane2, false, step, v);
self.faults.push(FaultCode::UnreachableBranch, step, 0);
}
}
Op::Schedule => {
let a = self.rd(src, lane, false);
let b = self.rd(dst, lane, false);
let v = self.mix(a, b, coupling, kc);
let offset = 1 + (tok.imm.rem_euclid(3)) as u8;
let hidden = tok.mode() & 1 == 1;
self.w.time_state.pending.push(ScheduledEffect {
turn_offset: offset,
domain: DomainId(dst as u8),
lane,
hidden,
value: v,
});
self.temporal.edges.push((step, offset, dst as u8));
self.flow(src, lane, false, dst, lane, hidden, step, v);
}
Op::Resonate => {
let a = self.rd(src, lane, false);
let b = self.rd(dst, lane, false);
let m = self.mix(a, b, coupling, kc);
let va = a.wrapping_add(m);
let vb = b ^ m;
self.wr(src, lane, false, va);
self.wr(dst, lane, false, vb);
self.flow(dst, lane, false, src, lane, false, step, va);
self.flow(src, lane, false, dst, lane, false, step, vb);
}
Op::Observe => {
let reg = tok.mode() % REGS;
let mut z: i64 = self.acc[reg];
let proj = self.w.observed_projection();
for k in 0..3 {
let d = (src + k) % NUM_DOMAINS;
let idx = d * LANES + (lane + k) % LANES;
let cpl = self.coupling(dst, d);
z = self.mix(z, proj[idx], cpl, kc);
self.flow(d, (lane + k) % LANES, false, dst, lane, true, step, z);
}
self.acc[reg] = z;
self.acc_src[reg] = src;
self.wr(dst, tok.mode() % HIDDEN_LANES, true, z);
}
Op::Collapse => {
let reg = tok.mode() % REGS;
let a = self.acc[reg];
let b = self.rd(dst, lane, false);
if a == 0 {
self.faults.push(FaultCode::EmptyAccumulator, step, reg as i64);
}
let v = self.mix(a, b, coupling, kc);
self.wr(dst, lane, false, v);
let asrc = self.acc_src[reg];
self.flow(asrc, 0, true, dst, lane, false, step, v);
}
Op::Invert => {
let b = self.rd(dst, lane, false);
let mut v = avalanche((!b).wrapping_add(tok.imm));
v ^= self.ctx.salt() as i64;
v = v.wrapping_add(self.ctx.profile.bias);
self.wr(dst, lane, false, v);
self.flow(dst, lane, false, dst, lane, false, step, v);
}
Op::Diffuse => {
let a = self.rd(src, lane, false);
for k in 0..DIFFUSE_SPAN {
let d = (src + 1 + k) % NUM_DOMAINS;
let tl = (lane + k) % LANES;
let prev = self.rd(d, tl, false);
let cpl = self.coupling(d, src);
let kc2 = DomainKind::from_index(d).mix_const();
let v = self.mix(a, prev, cpl, kc2);
self.wr(d, tl, false, prev.wrapping_add(v));
self.flow(src, lane, false, d, tl, false, step, v);
}
}
Op::Anchor => {
let bound = (tok.imm.unsigned_abs() % 1_000_000) as i64 + 1;
let b = self.rd(dst, lane, false);
let diag = self.coupling(dst, dst);
let mut mixed = b.wrapping_add(diag);
mixed = mixed
.wrapping_add(self.ctx.profile.bias)
.wrapping_add((self.ctx.salt() & 0xffff) as i64);
let clamped = mixed.clamp(-bound, bound);
if clamped != mixed {
self.faults.push(FaultCode::Saturated, step, bound);
}
self.wr(dst, lane, false, clamped);
self.flow(dst, lane, false, dst, lane, false, step, clamped);
}
Op::Echoback => {
let h = self.rd(dst, tok.mode() % HIDDEN_LANES, true);
let b = self.rd(dst, lane, false);
let v = self.mix(h, b, coupling, kc);
self.wr(dst, lane, false, v);
self.flow(dst, tok.mode() % HIDDEN_LANES, true, dst, lane, false, step, v);
}
Op::Imprint => {
let b = self.rd(dst, lane, false);
let hl = tok.mode() % HIDDEN_LANES;
let prevh = self.rd(dst, hl, true);
let v = self.mix(b, prevh, coupling, kc);
self.wr(dst, hl, true, v);
self.flow(dst, lane, false, dst, hl, true, step, v);
}
}
}
}
/// Advance the world one turn: resolve due scheduled effects, then run coupling
/// diffusion. Identical semantics to the reference's `step_world`, restated.
fn step_world(w: &mut WorldSnapshot) {
let pending = std::mem::take(&mut w.time_state.pending);
let mut still = Vec::new();
for e in pending {
if e.turn_offset <= 1 {
let d = e.domain.0 as usize;
if e.hidden {
let l = e.lane % HIDDEN_LANES;
w.domains[d].hidden[l] = w.domains[d].hidden[l].wrapping_add(e.value);
} else {
let l = e.lane % LANES;
w.domains[d].observed[l] = w.domains[d].observed[l].wrapping_add(e.value);
}
} else {
still.push(ScheduledEffect {
turn_offset: e.turn_offset - 1,
..e
});
}
}
w.time_state.pending = still;
let snap = w.domains.clone();
for j in 0..NUM_DOMAINS {
for lane in 0..LANES {
let mut z = w.domains[j].observed[lane];
for i in 0..NUM_DOMAINS {
let c = w.causal_state.coupling[j][i];
z = z.wrapping_add(c.wrapping_mul(snap[i].observed[lane] & 0xff));
}
w.domains[j].observed[lane] = avalanche(z);
}
for hl in 0..HIDDEN_LANES {
let base = w.domains[j].hidden[hl].wrapping_add(snap[j].observed[0]);
w.domains[j].hidden[hl] = avalanche(base);
}
}
w.turn = w.turn.wrapping_add(1);
}
fn future_hash(start: &WorldSnapshot) -> Hash {
let mut w = start.clone();
let mut h = Hasher::new();
h.write_tag("future-3");
for _ in 0..FUTURE_TURNS {
step_world(&mut w);
for v in w.ground_truth() {
h.write_i64(v);
}
}
h.finish()
}
fn compute_divergence(finals: &[WorldSnapshot]) -> DivergenceGraph {
let n = finals.len();
let mut pairwise = vec![0.0f64; n * n];
let total = (NUM_DOMAINS * LANES) as f64;
for i in 0..n {
for j in 0..n {
if i == j {
continue;
}
let mut diff = 0usize;
for d in 0..NUM_DOMAINS {
for l in 0..LANES {
if finals[i].domains[d].observed[l] != finals[j].domains[d].observed[l] {
diff += 1;
}
}
}
pairwise[i * n + j] = diff as f64 / total;
}
}
DivergenceGraph {
executor_count: n,
pairwise,
}
}
#[allow(clippy::too_many_arguments)]
fn behavior_fingerprint(
delta: &WorldDelta,
causal: &CausalGraph,
read_graph: &DomainAccessGraph,
write_graph: &DomainAccessGraph,
info_flow: &InformationFlowGraph,
temporal: &TemporalGraph,
divergence: &DivergenceGraph,
future: Hash,
) -> BehaviorFingerprint {
let mut features: Vec<i64> = Vec::new();
for dd in &delta.domain_deltas {
let mut s = 0i64;
for &v in &dd.observed {
s = s.wrapping_add(v);
}
features.push(s);
}
for dd in &delta.domain_deltas {
let mut s = 0i64;
for &v in &dd.hidden {
s = s.wrapping_add(v);
}
features.push(s);
}
features.push(causal.causal_rank() as i64);
features.push(causal.edge_count() as i64);
features.push(read_graph.touched_count() as i64);
features.push(write_graph.touched_count() as i64);
features.push(info_flow.total_bits() as i64);
features.push(temporal.edge_count() as i64);
features.push((divergence.mean_divergence() * 1_000_000.0) as i64);
features.push(future.0 as i64);
BehaviorFingerprint::from_features(features)
}
/// The independent implementation of the canonical resolution.
pub fn native_resolve(input: &ResolutionInput) -> ResolutionResult {
let contexts = if input.contexts.is_empty() {
world_model::standard_executors(input.world.seed, 3)
} else {
input.contexts.clone()
};
let world0 = input.world.clone();
let mut finals: Vec<WorldSnapshot> = Vec::with_capacity(contexts.len());
let mut primary: Option<Vm> = None;
for (idx, ctx) in contexts.iter().enumerate() {
let mut vm = Vm::new(&world0, ctx);
vm.run(&input.program);
finals.push(vm.w.clone());
if idx == 0 {
primary = Some(vm);
}
}
let vm = primary.expect("at least one executor");
let delta = WorldDelta::between(&world0, &vm.w);
let divergence = compute_divergence(&finals);
let fhash = future_hash(&vm.w);
let behavior = behavior_fingerprint(
&delta,
&vm.causal_graph,
&vm.read_graph,
&vm.write_graph,
&vm.info_flow,
&vm.temporal,
&divergence,
fhash,
);
let trace = ExecutionTrace {
read_graph: vm.read_graph,
write_graph: vm.write_graph,
causal_graph: vm.causal_graph,
information_flow: vm.info_flow,
executor_divergence: divergence,
temporal_graph: vm.temporal,
perturbation_response: PerturbationResponse::default(),
behavior_fingerprint: behavior,
};
let trace_hash = trace.canonical_hash();
let delta_hash = delta.hash();
let replay = ReplayRecord {
world_seed: input.world.seed,
program_seed: input.program.seed,
contract_seed: input.contract_seed,
perturbation_seed: input.perturbation_seed,
trace_hash,
delta_hash,
future_hash: fhash,
};
ResolutionResult {
delta,
trace,
faults: vm.faults,
replay,
}
}
+201 -39
View File
@@ -1,8 +1,14 @@
//! `semantic_mutation` — structurally generate mutated runtimes and prove the
//! test suite kills every one. A mutant is an [`EngineConfig`] (the runtime
//! artifact) with exactly one behavior-affecting knob changed. Every mutant
//! must fail at least one named acceptance gate; a survivor means the tests are
//! invalid and blocks merge.
//! test suite kills every one **by the named acceptance gate it targets**.
//!
//! A mutant is an [`EngineConfig`] (the runtime artifact) with exactly one
//! behavior-affecting knob changed. The spec requires that every mutant fail at
//! least one *named* acceptance gate. An earlier version of this crate only
//! checked that a mutant's canonical output *differed* from the reference — a
//! weaker, wrong condition that a mutant could satisfy without tripping the gate
//! it is supposed to expose. This version runs the actual named gate against
//! each mutant and requires that specific gate to fail. A mutant that does not
//! trip its named gate is a survivor and blocks merge.
use reference_runtime::{canonical, execute, Canonical, EngineConfig, ResolutionInput};
use world_model::NUM_DOMAINS;
@@ -10,6 +16,13 @@ use world_model::NUM_DOMAINS;
/// A mutant runtime artifact.
pub type RuntimeArtifact = EngineConfig;
// --- Gate thresholds, mirrored from the CI gate definitions. ----------------
const CAUSAL_EDGES_MIN: f64 = 24.0;
const CAUSAL_RANK_P95_MIN: f64 = 6.0;
const DOMAIN_APPEARS_MIN: f64 = 0.35;
const DOMAIN_MUTATED_MIN: f64 = 0.20;
const FUTURE_ALT_MIN: f64 = 0.50;
/// The named acceptance gate a mutant is expected to fail.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DetectionClass {
@@ -23,8 +36,8 @@ impl DetectionClass {
pub fn name(self) -> &'static str {
match self {
DetectionClass::RuntimeEquivalence => "runtime_equivalence",
DetectionClass::CausalGate => "causal_gate",
DetectionClass::TemporalGate => "temporal_gate",
DetectionClass::CausalGate => "causal_rank/trace",
DetectionClass::TemporalGate => "metamorphic_response/temporal",
DetectionClass::DomainParticipation => "domain_participation",
}
}
@@ -36,9 +49,6 @@ pub trait SemanticMutator {
fn expected_detection_reason(&self) -> DetectionClass;
}
/// Each generated mutant is also a [`SemanticMutator`]: applying it to any base
/// artifact reproduces its single-knob change, and it names the gate it must
/// fail. This ties the structural generator to the spec's trait surface.
impl SemanticMutator for Mutant {
fn mutate(&self, _base: &RuntimeArtifact) -> RuntimeArtifact {
self.config.clone()
@@ -58,7 +68,8 @@ pub struct Mutant {
}
/// Build the `i`-th mutant deterministically from the reference artifact.
/// Every mutant differs from the reference in exactly one behavioral knob.
/// Every mutant differs from the reference in exactly one behavioral knob, and
/// is tagged with the named gate that change must trip.
pub fn mutant_for(i: usize) -> Mutant {
let base = EngineConfig::reference();
let mut cfg = base.clone();
@@ -145,7 +156,6 @@ pub fn mutant_for(i: usize) -> Mutant {
}
};
// Safety net: guarantee the mutant is not accidentally identical.
if cfg == base {
cfg.use_hidden = !cfg.use_hidden;
}
@@ -169,20 +179,153 @@ pub fn reference_canon(inputs: &[ResolutionInput]) -> Vec<Canonical> {
inputs.iter().map(|inp| canonical(&execute(&cfg, inp))).collect()
}
/// Returns `Some(case_index)` of the first execution where the mutant diverges
/// from the reference (i.e. the mutant is killed), or `None` if it survives.
pub fn kill_index(
mutant: &EngineConfig,
inputs: &[ResolutionInput],
reference: &[Canonical],
) -> Option<usize> {
for (i, inp) in inputs.iter().enumerate() {
let c = canonical(&execute(mutant, inp));
if c != reference[i] {
return Some(i);
// --- Named-gate evaluators. -------------------------------------------------
//
// Each evaluator computes, for a given engine config over the input corpus, the
// metric a named CI gate checks, and returns whether that gate FAILS. The
// reference config must pass all of them (asserted in tests); each mutant must
// fail the one it targets.
fn percentile(mut v: Vec<f64>, p: f64) -> f64 {
if v.is_empty() {
return 0.0;
}
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
let idx = (((v.len() - 1) as f64) * p).round() as usize;
v[idx.min(v.len() - 1)]
}
fn median(v: Vec<f64>) -> f64 {
if v.is_empty() {
return 0.0;
}
let mut s = v;
s.sort_by(|a, b| a.partial_cmp(b).unwrap());
s[s.len() / 2]
}
/// True if the causal/trace gate fails under `cfg`.
fn causal_gate_fails(cfg: &EngineConfig, inputs: &[ResolutionInput]) -> bool {
let edges: Vec<f64> = inputs
.iter()
.map(|inp| execute(cfg, inp).trace.causal_edge_count() as f64)
.collect();
let ranks: Vec<f64> = inputs
.iter()
.map(|inp| execute(cfg, inp).trace.causal_rank() as f64)
.collect();
median(edges) < CAUSAL_EDGES_MIN || percentile(ranks, 0.05) < CAUSAL_RANK_P95_MIN
}
/// True if the domain-participation gate fails under `cfg`.
fn domain_gate_fails(cfg: &EngineConfig, inputs: &[ResolutionInput]) -> bool {
let n = inputs.len().max(1) as f64;
let mut appears = [0u32; NUM_DOMAINS];
let mut mutated = [0u32; NUM_DOMAINS];
for inp in inputs {
let r = execute(cfg, inp);
for d in 0..NUM_DOMAINS {
if r.trace.read_graph.access_count[d] > 0 || r.trace.write_graph.access_count[d] > 0 {
appears[d] += 1;
}
}
for dd in &r.delta.domain_deltas {
if !dd.is_zero() {
mutated[dd.domain.0 as usize] += 1;
}
}
}
None
(0..NUM_DOMAINS).any(|d| {
(appears[d] as f64 / n) < DOMAIN_APPEARS_MIN
|| (mutated[d] as f64 / n) < DOMAIN_MUTATED_MIN
})
}
/// Apply a fixed structural perturbation (bump domain 0, observed lane 0).
fn perturbed(input: &ResolutionInput) -> ResolutionInput {
let mut p = input.clone();
p.world.domains[0].observed[0] = p.world.domains[0].observed[0].wrapping_add(101);
p.world.mark_perturbed();
p
}
/// True if the temporal gate fails under `cfg`. The temporal gate asserts the
/// runtime carries genuine 3-turn future dynamics: it must (a) record temporal
/// edges, (b) have a future sensitive to perturbation, and (c) reproduce the
/// reference's 3-turn future. Any of these failing fails the gate.
fn temporal_gate_fails(
cfg: &EngineConfig,
inputs: &[ResolutionInput],
reference: &[Canonical],
) -> bool {
// (a) temporal edges present.
let tedges: Vec<f64> = inputs
.iter()
.map(|inp| execute(cfg, inp).trace.temporal_graph.edge_count() as f64)
.collect();
if median(tedges) < 1.0 {
return true;
}
// (b) future sensitive to perturbation.
let mut altered = 0usize;
for inp in inputs {
let base_future = execute(cfg, inp).replay.future_hash;
let pert_future = execute(cfg, &perturbed(inp)).replay.future_hash;
if base_future != pert_future {
altered += 1;
}
}
let alt_rate = altered as f64 / inputs.len().max(1) as f64;
if alt_rate < FUTURE_ALT_MIN {
return true;
}
// (c) future matches the reference's 3-turn future on every input.
for (inp, ref_c) in inputs.iter().zip(reference) {
if execute(cfg, inp).replay.future_hash != ref_c.future_hash {
return true;
}
}
false
}
/// True if the runtime-equivalence gate fails under `cfg` (i.e. the mutant
/// diverges from the reference canonical view on at least one input).
fn equivalence_gate_fails(
cfg: &EngineConfig,
inputs: &[ResolutionInput],
reference: &[Canonical],
) -> bool {
inputs
.iter()
.zip(reference)
.any(|(inp, ref_c)| canonical(&execute(cfg, inp)) != *ref_c)
}
/// Evaluate whether a mutant is killed by its **named** gate. Returns `None` if
/// killed (the named gate fails), or `Some(reason)` describing the survival.
pub fn survival_reason(
mutant: &Mutant,
inputs: &[ResolutionInput],
reference: &[Canonical],
) -> Option<String> {
let killed = match mutant.expected {
DetectionClass::RuntimeEquivalence => {
equivalence_gate_fails(&mutant.config, inputs, reference)
}
DetectionClass::CausalGate => causal_gate_fails(&mutant.config, inputs),
DetectionClass::TemporalGate => temporal_gate_fails(&mutant.config, inputs, reference),
DetectionClass::DomainParticipation => domain_gate_fails(&mutant.config, inputs),
};
if killed {
None
} else {
Some(format!(
"mutant {} ({}) did not fail its named gate {}",
mutant.id,
mutant.name,
mutant.expected.name()
))
}
}
/// Result of running the full mutation suite.
@@ -199,17 +342,16 @@ impl MutationOutcome {
}
}
/// Run all mutants against the input corpus.
/// Run all mutants against the input corpus, killing each by its named gate.
pub fn run_suite(count: usize, inputs: &[ResolutionInput]) -> MutationOutcome {
let reference = reference_canon(inputs);
let mutants = generate_mutants(count);
let mut killed = 0;
let mut survivors = Vec::new();
for m in &mutants {
if kill_index(&m.config, inputs, &reference).is_some() {
killed += 1;
} else {
survivors.push((m.id, m.name.clone()));
match survival_reason(m, inputs, &reference) {
None => killed += 1,
Some(reason) => survivors.push((m.id, reason)),
}
}
MutationOutcome {
@@ -223,13 +365,13 @@ pub fn run_suite(count: usize, inputs: &[ResolutionInput]) -> MutationOutcome {
mod tests {
use super::*;
use rune_ir::{RuneProgram, RuneToken, ALL_OPS};
use world_model::{standard_executors, ProgramId, Rng, WorldId, WorldSnapshot, NUM_DOMAINS};
use world_model::{standard_executors, ProgramId, Rng, WorldId, WorldSnapshot, LANES, NUM_DOMAINS};
fn rich_input(seed: u64) -> ResolutionInput {
let mut rng = Rng::new(seed);
let mut w = WorldSnapshot::blank(WorldId(seed), seed);
for d in &mut w.domains {
for l in 0..world_model::LANES {
for l in 0..LANES {
d.observed[l] = rng.range_i64(-5000, 5000);
}
for l in 0..world_model::HIDDEN_LANES {
@@ -241,7 +383,6 @@ mod tests {
w.causal_state.coupling[j][i] = rng.range_i64(-17, 17);
}
}
// cover every op and every domain
let tokens: Vec<RuneToken> = (0..40)
.map(|i| RuneToken {
op: ALL_OPS[i % ALL_OPS.len()],
@@ -260,6 +401,35 @@ mod tests {
}
}
fn corpus() -> Vec<ResolutionInput> {
(0..16).map(|s| rich_input(s + 1)).collect()
}
#[test]
fn reference_passes_every_named_gate() {
let inputs = corpus();
let reference = reference_canon(&inputs);
let cfg = EngineConfig::reference();
assert!(!causal_gate_fails(&cfg, &inputs), "reference fails causal gate");
assert!(!domain_gate_fails(&cfg, &inputs), "reference fails domain gate");
assert!(
!temporal_gate_fails(&cfg, &inputs, &reference),
"reference fails temporal gate"
);
assert!(
!equivalence_gate_fails(&cfg, &inputs, &reference),
"reference fails equivalence gate"
);
}
#[test]
fn no_mutant_survives_its_named_gate() {
let inputs = corpus();
let outcome = run_suite(520, &inputs);
assert!(outcome.passed(), "survivors: {:?}", outcome.survivors);
assert_eq!(outcome.killed, outcome.total);
}
#[test]
fn every_mutant_differs_from_reference() {
let base = EngineConfig::reference();
@@ -267,12 +437,4 @@ mod tests {
assert_ne!(mutant_for(i).config, base, "mutant {i} equals reference");
}
}
#[test]
fn no_mutant_survives() {
let inputs: Vec<ResolutionInput> = (0..12).map(|s| rich_input(s + 1)).collect();
let outcome = run_suite(520, &inputs);
assert!(outcome.passed(), "survivors: {:?}", outcome.survivors);
assert_eq!(outcome.killed, outcome.total);
}
}