changes claude never committed

This commit is contained in:
2026-06-21 18:00:52 -07:00
parent 39386a81c9
commit 2fe989bcb3
33 changed files with 5528 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "ci_reports"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
world_model = { path = "../world_model" }
rune_ir = { path = "../rune_ir" }
trace_model = { path = "../trace_model" }
generators = { path = "../generators" }
reference_runtime = { path = "../reference_runtime" }
runtime_under_test = { path = "../runtime_under_test" }
collapse_analysis = { path = "../collapse_analysis" }
semantic_mutation = { path = "../semantic_mutation" }
replay_corpus = { path = "../replay_corpus" }
[lib]
path = "src/lib.rs"
[[bin]]
name = "ci"
path = "src/main.rs"
+94
View File
@@ -0,0 +1,94 @@
//! Minimal hand-rolled JSON value + pretty printer (no external crates).
pub enum Json {
Null,
Bool(bool),
Int(i64),
Num(f64),
Str(String),
Arr(Vec<Json>),
Obj(Vec<(String, Json)>),
}
impl Json {
pub fn s(v: impl Into<String>) -> Json {
Json::Str(v.into())
}
pub fn to_pretty(&self) -> String {
let mut out = String::new();
self.write(&mut out, 0);
out.push('\n');
out
}
fn write(&self, out: &mut String, indent: usize) {
match self {
Json::Null => out.push_str("null"),
Json::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Json::Int(i) => out.push_str(&i.to_string()),
Json::Num(f) => {
if f.is_finite() {
out.push_str(&format!("{:.6}", f));
} else {
out.push_str("null");
}
}
Json::Str(s) => {
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
_ => out.push(c),
}
}
out.push('"');
}
Json::Arr(items) => {
if items.is_empty() {
out.push_str("[]");
return;
}
out.push_str("[\n");
for (i, it) in items.iter().enumerate() {
pad(out, indent + 1);
it.write(out, indent + 1);
if i + 1 < items.len() {
out.push(',');
}
out.push('\n');
}
pad(out, indent);
out.push(']');
}
Json::Obj(fields) => {
if fields.is_empty() {
out.push_str("{}");
return;
}
out.push_str("{\n");
for (i, (k, v)) in fields.iter().enumerate() {
pad(out, indent + 1);
out.push('"');
out.push_str(k);
out.push_str("\": ");
v.write(out, indent + 1);
if i + 1 < fields.len() {
out.push(',');
}
out.push('\n');
}
pad(out, indent);
out.push('}');
}
}
}
}
fn pad(out: &mut String, indent: usize) {
for _ in 0..indent {
out.push_str(" ");
}
}
+784
View File
@@ -0,0 +1,784 @@
//! `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.
pub mod json;
use collapse_analysis::{analyze, BehaviorCorpus, CollapseSummary};
use generators::{generate_accepted_case, generated_gate_failures, GeneratedCase};
use reference_runtime::{canonical, execute, EngineConfig, ResolutionInput, ResolutionResult, Runtime};
use runtime_under_test::RuntimeUnderTest;
use semantic_mutation::{run_suite, MutationOutcome};
use std::collections::HashMap;
use world_model::{Hash, WorldSnapshot, HIDDEN_LANES, LANES, NUM_DOMAINS};
// ---------------------------------------------------------------------------
// Scale configuration.
// ---------------------------------------------------------------------------
#[derive(Clone, Copy, Debug)]
pub struct Scale {
pub executions: usize,
pub mutants: usize,
pub replay_cases: usize,
pub collapse_samples: usize,
pub domain_probe_cases: usize,
pub domain_probe_variations: usize,
}
impl Scale {
/// The full, merge-blocking gates mandated by the spec.
pub fn full() -> Self {
Scale {
executions: 1_000_000,
mutants: 600,
replay_cases: 10_000,
collapse_samples: 5_000,
domain_probe_cases: 2_000,
domain_probe_variations: 64,
}
}
/// Fast CI (~10% spirit): small but exercises every gate.
pub fn fast() -> Self {
Scale {
executions: 600,
mutants: 520,
replay_cases: 600,
collapse_samples: 600,
domain_probe_cases: 200,
domain_probe_variations: 48,
}
}
pub fn tiny() -> Self {
Scale {
executions: 120,
mutants: 520,
replay_cases: 120,
collapse_samples: 120,
domain_probe_cases: 60,
domain_probe_variations: 32,
}
}
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(),
};
if let Some(v) = env_usize("MAGICKA_EXECUTIONS") {
s.executions = v;
}
if let Some(v) = env_usize("MAGICKA_MUTANTS") {
s.mutants = v;
}
if let Some(v) = env_usize("MAGICKA_REPLAY") {
s.replay_cases = v;
}
if let Some(v) = env_usize("MAGICKA_COLLAPSE") {
s.collapse_samples = v;
}
s
}
}
fn env_usize(key: &str) -> Option<usize> {
std::env::var(key).ok().and_then(|v| v.parse().ok())
}
// ---------------------------------------------------------------------------
// Helpers.
// ---------------------------------------------------------------------------
fn case_seed(i: usize) -> u64 {
0xC0FFEE_u64 ^ (i as u64).wrapping_mul(0x9E3779B97F4A7C15)
}
fn input_from_case(case: &GeneratedCase) -> ResolutionInput {
ResolutionInput {
world: case.world.clone(),
program: case.program.clone(),
contexts: case.contexts.clone(),
contract_seed: case.contract_seed,
perturbation_seed: case.perturbation_seed,
}
}
fn input_with_world(case: &GeneratedCase, world: WorldSnapshot) -> ResolutionInput {
ResolutionInput {
world,
program: case.program.clone(),
contexts: case.contexts.clone(),
contract_seed: case.contract_seed,
perturbation_seed: case.perturbation_seed,
}
}
fn masked_config(d: usize) -> EngineConfig {
let mut c = EngineConfig::reference();
c.domain_mask[d] = false;
c
}
fn median(v: &[f64]) -> f64 {
if v.is_empty() {
return 0.0;
}
let mut s = v.to_vec();
s.sort_by(|a, b| a.partial_cmp(b).unwrap());
s[s.len() / 2]
}
fn percentile(v: &[f64], p: f64) -> f64 {
if v.is_empty() {
return 0.0;
}
let mut s = v.to_vec();
s.sort_by(|a, b| a.partial_cmp(b).unwrap());
let idx = ((s.len() as f64 - 1.0) * p).round() as usize;
s[idx.min(s.len() - 1)]
}
/// Order-0 entropy-based compressibility of a behavior feature vector, in
/// `[0,1]`; high-entropy (incompressible) behavior tends toward 0.
fn compressibility(features: &[i64]) -> f64 {
let mut bytes = Vec::with_capacity(features.len() * 8);
for &f in features {
bytes.extend_from_slice(&f.to_le_bytes());
}
if bytes.is_empty() {
return 1.0;
}
let mut counts = [0u32; 256];
for &b in &bytes {
counts[b as usize] += 1;
}
let n = bytes.len() as f64;
let mut h = 0.0;
for &c in &counts {
if c > 0 {
let p = c as f64 / n;
h -= p * p.log2();
}
}
(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);
}
}
out
}
fn behavior_output_features(r: &ResolutionResult) -> Vec<f64> {
r.trace
.behavior_fingerprint
.features
.iter()
.map(|&v| v as f64)
.collect()
}
// ---------------------------------------------------------------------------
// Result aggregates.
// ---------------------------------------------------------------------------
#[derive(Clone, Debug)]
pub struct TraceGates {
pub median_causal_edges: f64,
pub p95_causal_rank: f64,
pub median_touched: f64,
pub p95_touched: f64,
pub median_rank: f64,
pub fp_collision_rate: f64,
pub largest_cluster: f64,
pub failures: Vec<String>,
}
#[derive(Clone, Debug)]
pub struct EquivalenceGates {
pub total: usize,
pub matched: usize,
pub failures: Vec<String>,
}
#[derive(Clone, Debug)]
pub struct DomainGates {
pub appears: [f64; NUM_DOMAINS],
pub influences: [f64; NUM_DOMAINS],
pub mutated: [f64; NUM_DOMAINS],
pub removal_loss: [f64; NUM_DOMAINS],
pub min_merge_loss: f64,
pub read_only: Vec<usize>,
pub write_only: Vec<usize>,
pub failures: Vec<String>,
}
#[derive(Clone, Debug)]
pub struct MetamorphicGates {
pub total: usize,
pub altered_trace: f64,
pub altered_delta: f64,
pub altered_future: f64,
pub neutral_unexplained: f64,
pub failures: Vec<String>,
}
#[derive(Clone, Debug)]
pub struct ContractGates {
pub total: usize,
pub passed: usize,
pub failures: Vec<String>,
}
#[derive(Clone, Debug)]
pub struct ReplayGates {
pub total: usize,
pub deterministic: usize,
pub drift: usize,
pub failures: Vec<String>,
}
#[derive(Clone, Debug)]
pub struct CoverageGates {
pub generated_cases: usize,
pub generated_rejected: usize,
pub contract_rejected: usize,
pub executions: usize,
pub perturbations: usize,
pub failures: Vec<String>,
}
pub struct CiResults {
pub scale: Scale,
pub trace: TraceGates,
pub equivalence: EquivalenceGates,
pub domain: DomainGates,
pub metamorphic: MetamorphicGates,
pub collapse: CollapseSummary,
pub mutation: MutationOutcome,
pub contract: ContractGates,
pub replay: ReplayGates,
pub coverage: CoverageGates,
}
impl CiResults {
pub fn all_failures(&self) -> Vec<(&'static str, &Vec<String>)> {
vec![
("causal_rank/trace", &self.trace.failures),
("runtime_equivalence", &self.equivalence.failures),
("domain_participation", &self.domain.failures),
("metamorphic_response", &self.metamorphic.failures),
("compression_resistance", &self.collapse.failures),
("contract", &self.contract.failures),
("replay", &self.replay.failures),
("coverage", &self.coverage.failures),
]
}
pub fn passed(&self) -> bool {
self.all_failures().iter().all(|(_, f)| f.is_empty()) && self.mutation.passed()
}
}
// ---------------------------------------------------------------------------
// The pipeline.
// ---------------------------------------------------------------------------
pub fn run_all(scale: Scale) -> CiResults {
let cfg = EngineConfig::reference();
let rut = RuntimeUnderTest::new();
// Main execution + metamorphic + contract pass.
let mut ranks = Vec::with_capacity(scale.executions);
let mut edges = Vec::with_capacity(scale.executions);
let mut touched = Vec::with_capacity(scale.executions);
let mut fp_counts: HashMap<Hash, u32> = HashMap::new();
let mut equiv_matched = 0usize;
let mut equiv_failures = Vec::new();
let mut domain_appear = [0u64; NUM_DOMAINS];
let mut domain_mutated = [0u64; NUM_DOMAINS];
let mut domain_read_any = [false; NUM_DOMAINS];
let mut domain_write_any = [false; NUM_DOMAINS];
let mut meta_total = 0usize;
let mut meta_alt_trace = 0usize;
let mut meta_alt_delta = 0usize;
let mut meta_alt_future = 0usize;
let mut meta_neutral_unexpl = 0usize;
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 mutation_inputs: Vec<ResolutionInput> = Vec::new();
let mut generated_rejected = 0usize;
let mut perturbation_runs = 0usize;
let mut contract_rejected = 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.
let mut seed = case_seed(i);
for attempt in 0..48 {
let last_attempt = attempt == 47;
let (case, _) = generate_accepted_case(seed);
if !generated_gate_failures(&case.estimate()).is_empty() {
generated_rejected += 1;
}
let input = input_from_case(&case);
let r = execute(&cfg, &input);
let rank = r.trace.causal_rank();
let touched_cnt = r.trace.touched_domain_count();
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;
let mut c_alt_trace = 0usize;
let mut c_alt_delta = 0usize;
let mut c_alt_future = 0usize;
let mut c_neutral = 0usize;
let mut c_pert = 0usize;
for pc in &case.perturbations {
let pr = execute(&cfg, &input_with_world(&case, pc.world.clone()));
c_pert += 1;
let at = pr.trace.canonical_hash() != base_trace_h;
let ad = pr.delta.hash() != base_delta_h;
let af = pr.replay.future_hash != base_future_h;
if at {
c_alt_trace += 1;
}
if ad {
c_alt_delta += 1;
}
if af {
c_alt_future += 1;
}
if !at && pc.expectation.neutral_explanation.is_none() {
c_neutral += 1;
}
}
let future_sensitivity = if c_pert > 0 {
c_alt_future as f64 / c_pert as f64
} else {
0.0
};
let mut creasons = Vec::new();
if rank < case.contract.min_causal_rank {
creasons.push("causal_rank");
}
if touched_cnt < case.contract.min_domain_participation {
creasons.push("domain_participation");
}
if future_sensitivity < case.contract.min_future_sensitivity {
creasons.push("future_sensitivity");
}
if div < case.contract.min_context_divergence {
creasons.push("context_divergence");
}
if comp > case.contract.max_compressibility {
creasons.push("compressibility");
}
if !creasons.is_empty() && !last_attempt {
contract_rejected += 1;
seed = generators::derive(seed, "contract-retry");
continue;
}
// ---- Commit the admitted case ----
let r2 = rut.resolve(input.clone());
if canonical(&r) == canonical(&r2) {
equiv_matched += 1;
} else if equiv_failures.len() < 16 {
equiv_failures.push(format!("case {} reference != runtime_under_test", i));
}
ranks.push(rank as f64);
edges.push(r.trace.causal_edge_count() as f64);
touched.push(touched_cnt as f64);
*fp_counts.entry(r.trace.behavior_fingerprint.hash).or_insert(0) += 1;
for d in 0..NUM_DOMAINS {
let read = r.trace.read_graph.access_count[d] > 0;
let write = r.trace.write_graph.access_count[d] > 0;
if read || write {
domain_appear[d] += 1;
}
if read {
domain_read_any[d] = true;
}
if write {
domain_write_any[d] = true;
}
}
for dd in &r.delta.domain_deltas {
if !dd.is_zero() {
domain_mutated[dd.domain.0 as usize] += 1;
}
}
meta_total += c_pert;
meta_alt_trace += c_alt_trace;
meta_alt_delta += c_alt_delta;
meta_alt_future += c_alt_future;
meta_neutral_unexpl += c_neutral;
perturbation_runs += c_pert;
if creasons.is_empty() {
contract_pass += 1;
} else if contract_failures.len() < 16 {
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 mutation_inputs.len() < 64 {
mutation_inputs.push(input.clone());
}
break;
}
}
let n = scale.executions.max(1) as f64;
// ---- Trace gates ----
let collisions = scale.executions - fp_counts.len();
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 med_touched = median(&touched);
let p95_touched = percentile(&touched, 0.05);
let med_rank = median(&ranks);
let fp_collision_rate = collisions as f64 / n;
if med_edges < 24.0 {
trace_failures.push(format!("median causal edges {} < 24", med_edges));
}
if p95_rank < 6.0 {
trace_failures.push(format!("95% causal rank {} < 6", p95_rank));
}
if med_touched < 4.0 {
trace_failures.push(format!("median touched {} < 4", med_touched));
}
if p95_touched < 3.0 {
trace_failures.push(format!("95% touched {} < 3", p95_touched));
}
if fp_collision_rate >= 0.05 {
trace_failures.push(format!("fp collision rate {:.4} >= 0.05", fp_collision_rate));
}
if largest_cluster >= 0.02 {
trace_failures.push(format!("largest cluster {:.4} >= 0.02", largest_cluster));
}
let trace = TraceGates {
median_causal_edges: med_edges,
p95_causal_rank: p95_rank,
median_touched: med_touched,
p95_touched,
median_rank: med_rank,
fp_collision_rate,
largest_cluster,
failures: trace_failures,
};
// ---- Equivalence gates ----
if equiv_matched != scale.executions {
equiv_failures.push(format!(
"{}/{} executions matched (require 100%)",
equiv_matched, scale.executions
));
}
let equivalence = EquivalenceGates {
total: scale.executions,
matched: equiv_matched,
failures: equiv_failures,
};
// ---- Domain participation gates ----
let domain = domain_gates(
scale,
&domain_appear,
&domain_mutated,
&domain_read_any,
&domain_write_any,
n,
);
// ---- Metamorphic gates ----
let mt = meta_total.max(1) as f64;
let mut meta_failures = Vec::new();
let r_trace = meta_alt_trace as f64 / mt;
let r_delta = meta_alt_delta as f64 / mt;
let r_future = meta_alt_future as f64 / mt;
let r_neutral = meta_neutral_unexpl as f64 / mt;
if r_trace < 0.90 {
meta_failures.push(format!("altered trace {:.3} < 0.90", r_trace));
}
if r_delta < 0.75 {
meta_failures.push(format!("altered delta {:.3} < 0.75", r_delta));
}
if r_future < 0.50 {
meta_failures.push(format!("altered future {:.3} < 0.50", r_future));
}
if r_neutral > 0.05 {
meta_failures.push(format!("unexplained neutral {:.3} > 0.05", r_neutral));
}
let metamorphic = MetamorphicGates {
total: meta_total,
altered_trace: r_trace,
altered_delta: r_delta,
altered_future: r_future,
neutral_unexplained: r_neutral,
failures: meta_failures,
};
// ---- Collapse gates ----
let corpus = BehaviorCorpus::build(collapse_inputs, collapse_outputs, collapse_fps);
let collapse = analyze(&corpus);
// ---- Mutation gates ----
let mutation = run_suite(scale.mutants, &mutation_inputs);
// ---- Contract gates ----
let mut contract_gate_failures = Vec::new();
if contract_pass != scale.executions {
contract_gate_failures.push(format!(
"{}/{} cases satisfy their contract",
contract_pass, scale.executions
));
contract_gate_failures.extend(contract_failures);
}
let contract = ContractGates {
total: scale.executions,
passed: contract_pass,
failures: contract_gate_failures,
};
// ---- Replay gates ----
let replay = replay_gates(scale);
// ---- Coverage gates ----
let mut cov_failures = Vec::new();
if generated_rejected != 0 {
cov_failures.push(format!(
"{} admitted cases failed generated gates",
generated_rejected
));
}
if scale.executions == 0 {
cov_failures.push("no executions".into());
}
let coverage = CoverageGates {
generated_cases: scale.executions,
generated_rejected,
contract_rejected,
executions: scale.executions,
perturbations: perturbation_runs,
failures: cov_failures,
};
CiResults {
scale,
trace,
equivalence,
domain,
metamorphic,
collapse,
mutation,
contract,
replay,
coverage,
}
}
fn domain_gates(
scale: Scale,
appear: &[u64; NUM_DOMAINS],
mutated: &[u64; NUM_DOMAINS],
read_any: &[bool; NUM_DOMAINS],
write_any: &[bool; NUM_DOMAINS],
n: f64,
) -> DomainGates {
let cfg = EngineConfig::reference();
let mut appears = [0.0; NUM_DOMAINS];
let mut mutated_f = [0.0; NUM_DOMAINS];
for d in 0..NUM_DOMAINS {
appears[d] = appear[d] as f64 / n;
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 {
let masked = masked_config(d);
let mut changed = 0usize;
for i in 0..probe_cases {
let (case, _) = generate_accepted_case(case_seed(i));
let input = input_from_case(&case);
let base = execute(&cfg, &input);
let alt = execute(&masked, &input);
if base.trace.canonical_hash() != alt.trace.canonical_hash()
|| base.delta.hash() != alt.delta.hash()
{
changed += 1;
}
}
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 {
let base_world = generators::generate_world(0xBEEF ^ d as u64);
let case = {
let (c, _) = generate_accepted_case(case_seed(d + 1));
c
};
let mut full = std::collections::HashSet::new();
let mut masked = std::collections::HashSet::new();
let mcfg = masked_config(d);
for v in 0..k {
let mut w = base_world.clone();
for l in 0..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);
let input = input_with_world(&case, w);
full.insert(execute(&cfg, &input).trace.behavior_fingerprint.hash);
masked.insert(execute(&mcfg, &input).trace.behavior_fingerprint.hash);
}
let df = full.len().max(1) as f64;
let dm = masked.len() as f64;
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 {
for b in (a + 1)..NUM_DOMAINS {
let mut changed = 0usize;
for i in 0..merge_cases {
let (case, _) = generate_accepted_case(case_seed(i));
let base = execute(&cfg, &input_from_case(&case));
let mut w = case.world.clone();
w.domains[b].observed = w.domains[a].observed;
w.domains[b].hidden = w.domains[a].hidden;
let merged = execute(&cfg, &input_with_world(&case, w));
if merged.trace.behavior_fingerprint.hash != base.trace.behavior_fingerprint.hash {
changed += 1;
}
}
let loss = changed as f64 / merge_cases as f64;
min_merge_loss = min_merge_loss.min(loss);
}
}
let read_only: Vec<usize> = (0..NUM_DOMAINS)
.filter(|&d| read_any[d] && !write_any[d])
.collect();
let write_only: Vec<usize> = (0..NUM_DOMAINS)
.filter(|&d| write_any[d] && !read_any[d])
.collect();
let mut failures = Vec::new();
for d in 0..NUM_DOMAINS {
if appears[d] < 0.35 {
failures.push(format!("domain {} appears {:.3} < 0.35", d, appears[d]));
}
if influences[d] < 0.20 {
failures.push(format!("domain {} influences {:.3} < 0.20", d, influences[d]));
}
if mutated_f[d] < 0.20 {
failures.push(format!("domain {} mutated {:.3} < 0.20", d, mutated_f[d]));
}
if removal_loss[d] < 0.10 {
failures.push(format!(
"domain {} removal diversity loss {:.3} < 0.10",
d, removal_loss[d]
));
}
}
if min_merge_loss < 0.08 {
failures.push(format!("min merge loss {:.3} < 0.08", min_merge_loss));
}
if !read_only.is_empty() {
failures.push(format!("read-only domains: {:?}", read_only));
}
if !write_only.is_empty() {
failures.push(format!("write-only domains: {:?}", write_only));
}
DomainGates {
appears,
influences,
mutated: mutated_f,
removal_loss,
min_merge_loss,
read_only,
write_only,
failures,
}
}
fn replay_gates(scale: Scale) -> ReplayGates {
let corpus = replay_corpus::build_corpus(scale.replay_cases, 0x5EED);
let report = replay_corpus::verify_corpus(&corpus);
let mut failures = Vec::new();
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));
}
ReplayGates {
total: report.total,
deterministic: report.deterministic,
drift: report.drift.len(),
failures,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tiny_ci_passes_all_gates() {
let r = run_all(Scale::tiny());
for (name, f) in r.all_failures() {
assert!(f.is_empty(), "{name} failed: {:?}", f);
}
assert!(r.mutation.passed(), "mutants survived: {:?}", r.mutation.survivors);
assert!(r.passed());
}
}
+325
View File
@@ -0,0 +1,325 @@
//! 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.
use ci_reports::json::Json;
use ci_reports::{run_all, CiResults, Scale};
use std::fs;
use std::io::Write;
use std::path::Path;
use std::time::Instant;
fn arr_f(vals: &[f64]) -> Json {
Json::Arr(vals.iter().map(|&v| Json::Num(v)).collect())
}
fn fails(v: &[String]) -> Json {
Json::Arr(v.iter().map(|s| Json::s(s.clone())).collect())
}
fn pass_field(v: &[String]) -> Json {
Json::Bool(v.is_empty())
}
fn write_report(dir: &Path, name: &str, j: &Json) {
let path = dir.join(format!("{name}.json"));
let mut f = fs::File::create(&path).expect("create report");
f.write_all(j.to_pretty().as_bytes()).expect("write report");
}
fn build_reports(dir: &Path, r: &CiResults) {
// 1. domain_participation_report
write_report(
dir,
"domain_participation_report",
&Json::Obj(vec![
("pass".into(), pass_field(&r.domain.failures)),
("appears_fraction".into(), arr_f(&r.domain.appears)),
("influences_fraction".into(), arr_f(&r.domain.influences)),
("mutated_fraction".into(), arr_f(&r.domain.mutated)),
("removal_diversity_loss".into(), arr_f(&r.domain.removal_loss)),
("min_merge_loss".into(), Json::Num(r.domain.min_merge_loss)),
(
"read_only_domains".into(),
Json::Arr(r.domain.read_only.iter().map(|&d| Json::Int(d as i64)).collect()),
),
(
"write_only_domains".into(),
Json::Arr(r.domain.write_only.iter().map(|&d| Json::Int(d as i64)).collect()),
),
("failures".into(), fails(&r.domain.failures)),
]),
);
// 2. causal_rank_report (trace gates)
write_report(
dir,
"causal_rank_report",
&Json::Obj(vec![
("pass".into(), pass_field(&r.trace.failures)),
("median_causal_rank".into(), Json::Num(r.trace.median_rank)),
("p95_causal_rank".into(), Json::Num(r.trace.p95_causal_rank)),
("median_causal_edges".into(), Json::Num(r.trace.median_causal_edges)),
("median_touched_domains".into(), Json::Num(r.trace.median_touched)),
("p95_touched_domains".into(), Json::Num(r.trace.p95_touched)),
("fp_collision_rate".into(), Json::Num(r.trace.fp_collision_rate)),
("largest_cluster".into(), Json::Num(r.trace.largest_cluster)),
("failures".into(), fails(&r.trace.failures)),
]),
);
// 3. compression_resistance_report
let attack_json: Vec<Json> = r
.collapse
.reports
.iter()
.map(|rep| {
Json::Obj(vec![
("attack".into(), Json::s(rep.attack.clone())),
("predicts".into(), Json::Num(rep.predicts)),
("info_loss".into(), Json::Num(rep.info_loss)),
("detail".into(), Json::s(rep.detail.clone())),
])
})
.collect();
write_report(
dir,
"compression_resistance_report",
&Json::Obj(vec![
("pass".into(), pass_field(&r.collapse.failures)),
("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)),
("max_single_domain".into(), Json::Num(r.collapse.max_single_domain)),
("max_pair".into(), Json::Num(r.collapse.max_pair)),
("min_info_loss".into(), Json::Num(r.collapse.min_info_loss)),
("attacks".into(), Json::Arr(attack_json)),
("failures".into(), fails(&r.collapse.failures)),
]),
);
// 4. metamorphic_response_report
write_report(
dir,
"metamorphic_response_report",
&Json::Obj(vec![
("pass".into(), pass_field(&r.metamorphic.failures)),
("perturbations".into(), Json::Int(r.metamorphic.total as i64)),
("altered_trace".into(), Json::Num(r.metamorphic.altered_trace)),
("altered_delta".into(), Json::Num(r.metamorphic.altered_delta)),
("altered_future".into(), Json::Num(r.metamorphic.altered_future)),
("neutral_unexplained".into(), Json::Num(r.metamorphic.neutral_unexplained)),
("failures".into(), fails(&r.metamorphic.failures)),
]),
);
// 5. mutation_survivor_report
let survivors: Vec<Json> = r
.mutation
.survivors
.iter()
.map(|(id, name)| {
Json::Obj(vec![
("id".into(), Json::Int(*id as i64)),
("name".into(), Json::s(name.clone())),
])
})
.collect();
write_report(
dir,
"mutation_survivor_report",
&Json::Obj(vec![
("pass".into(), Json::Bool(r.mutation.passed())),
("total_mutants".into(), Json::Int(r.mutation.total as i64)),
("killed".into(), Json::Int(r.mutation.killed as i64)),
("survivors".into(), Json::Arr(survivors)),
]),
);
// 6. runtime_equivalence_report
write_report(
dir,
"runtime_equivalence_report",
&Json::Obj(vec![
("pass".into(), pass_field(&r.equivalence.failures)),
("total".into(), Json::Int(r.equivalence.total as i64)),
("matched".into(), Json::Int(r.equivalence.matched as i64)),
("failures".into(), fails(&r.equivalence.failures)),
]),
);
// 7. replay_report
write_report(
dir,
"replay_report",
&Json::Obj(vec![
("pass".into(), pass_field(&r.replay.failures)),
("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)),
("failures".into(), fails(&r.replay.failures)),
]),
);
// 8. coverage_report
write_report(
dir,
"coverage_report",
&Json::Obj(vec![
("pass".into(), pass_field(&r.coverage.failures)),
("generated_cases".into(), Json::Int(r.coverage.generated_cases as i64)),
("generated_rejected".into(), Json::Int(r.coverage.generated_rejected as i64)),
("contract_rejected".into(), Json::Int(r.coverage.contract_rejected as i64)),
("executions".into(), Json::Int(r.coverage.executions as i64)),
("perturbations".into(), Json::Int(r.coverage.perturbations as i64)),
("contracts_passed".into(), Json::Int(r.contract.passed as i64)),
("contracts_total".into(), Json::Int(r.contract.total as i64)),
("failures".into(), fails(&r.coverage.failures)),
]),
);
}
fn status(v: bool) -> &'static str {
if v {
"PASS"
} else {
"FAIL"
}
}
fn write_markdown(dir: &Path, r: &CiResults) {
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!(
"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("| Report | Status | Key metrics |\n|---|---|---|\n");
s.push_str(&format!(
"| runtime_equivalence | {} | {}/{} matched |\n",
status(r.equivalence.failures.is_empty()),
r.equivalence.matched,
r.equivalence.total
));
s.push_str(&format!(
"| causal_rank/trace | {} | rank med={} p95={}, edges med={}, touched med={} |\n",
status(r.trace.failures.is_empty()),
r.trace.median_rank,
r.trace.p95_causal_rank,
r.trace.median_causal_edges,
r.trace.median_touched
));
s.push_str(&format!(
"| domain_participation | {} | min_merge_loss={:.3} |\n",
status(r.domain.failures.is_empty()),
r.domain.min_merge_loss
));
s.push_str(&format!(
"| metamorphic_response | {} | trace={:.3} delta={:.3} future={:.3} |\n",
status(r.metamorphic.failures.is_empty()),
r.metamorphic.altered_trace,
r.metamorphic.altered_delta,
r.metamorphic.altered_future
));
s.push_str(&format!(
"| compression_resistance | {} | 1f={:.3} 2f={:.3} 4f={:.3} single={:.3} pair={:.3} info_loss={:.3} |\n",
status(r.collapse.failures.is_empty()),
r.collapse.best_1factor,
r.collapse.best_2factor,
r.collapse.best_4factor,
r.collapse.max_single_domain,
r.collapse.max_pair,
r.collapse.min_info_loss
));
s.push_str(&format!(
"| mutation_survivor | {} | killed {}/{} |\n",
status(r.mutation.passed()),
r.mutation.killed,
r.mutation.total
));
s.push_str(&format!(
"| contract | {} | {}/{} cases |\n",
status(r.contract.failures.is_empty()),
r.contract.passed,
r.contract.total
));
s.push_str(&format!(
"| replay | {} | {}/{} deterministic, drift={} |\n",
status(r.replay.failures.is_empty()),
r.replay.deterministic,
r.replay.total,
r.replay.drift
));
s.push_str(&format!(
"| coverage | {} | exec={}, perturb={}, rejected={} |\n",
status(r.coverage.failures.is_empty()),
r.coverage.executions,
r.coverage.perturbations,
r.coverage.generated_rejected
));
s.push_str("\n## Failures\n\n");
let mut any = false;
for (name, f) in r.all_failures() {
for msg in f {
any = true;
s.push_str(&format!("- **{}**: {}\n", name, msg));
}
}
for (id, name) in &r.mutation.survivors {
any = true;
s.push_str(&format!("- **mutation_survivor**: mutant {} ({}) survived\n", id, name));
}
if !any {
s.push_str("None. The fake universe failed to collapse. ✅\n");
}
let path = dir.join("ci_summary.md");
fs::write(path, s).expect("write markdown");
}
fn main() {
let scale = Scale::from_env();
let out_dir = std::env::var("MAGICKA_OUT").unwrap_or_else(|_| "ci_out".to_string());
let dir = Path::new(&out_dir);
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
);
let start = Instant::now();
let results = run_all(scale);
let elapsed = start.elapsed();
build_reports(dir, &results);
write_markdown(dir, &results);
println!("\n=== Magicka VM CI ({:?}) ===", elapsed);
for (name, f) in results.all_failures() {
println!(" {:<24} {}", name, status(f.is_empty()));
}
println!(
" {:<24} {} ({} killed / {} mutants{})",
"mutation_survivor",
status(results.mutation.passed()),
results.mutation.killed,
results.mutation.total,
if results.mutation.survivors.is_empty() {
String::new()
} else {
format!(", {} survivors", results.mutation.survivors.len())
}
);
println!("reports written to {}/", out_dir);
if results.passed() {
println!("\nOVERALL: PASS — the adversarial framework could not collapse the universe.");
std::process::exit(0);
} else {
println!("\nOVERALL: FAIL");
std::process::exit(1);
}
}
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "collapse_analysis"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
world_model = { path = "../world_model" }
trace_model = { path = "../trace_model" }
[lib]
path = "src/lib.rs"
+470
View File
@@ -0,0 +1,470 @@
//! `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.
//!
//! 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.
pub mod linalg;
use linalg::{ols_r2, pca_scores, Mat};
use world_model::{Hash, HIDDEN_LANES, LANES, NUM_DOMAINS};
/// Input columns belonging to one domain (observed + hidden lanes).
pub const DOMAIN_BLOCK: usize = LANES + HIDDEN_LANES;
/// A standardized behavior corpus.
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>,
}
fn standardize(rows: &[Vec<f64>]) -> Mat {
let n = rows.len();
let cols = if n == 0 { 0 } else { rows[0].len() };
let mut m = Mat::zeros(n, cols);
for r in 0..n {
for c in 0..cols {
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 {
mean += m.at(r, c);
}
mean /= n.max(1) as f64;
let mut var = 0.0;
for r in 0..n {
var += (m.at(r, c) - mean).powi(2);
}
var /= n.max(1) as f64;
let sd = var.sqrt();
let inv = if sd > 1e-9 { 1.0 / sd } else { 0.0 };
for r in 0..n {
let v = (m.at(r, c) - mean) * inv;
m.set(r, c, v);
}
}
m
}
impl BehaviorCorpus {
pub fn build(inputs: Vec<Vec<f64>>, outputs: Vec<Vec<f64>>, fingerprints: Vec<Hash>) -> Self {
BehaviorCorpus {
x: standardize(&inputs),
y: standardize(&outputs),
fingerprints,
}
}
pub fn n(&self) -> usize {
self.x.rows
}
fn select(&self, cols: &[usize]) -> Mat {
let mut m = Mat::zeros(self.x.rows, cols.len());
for r in 0..self.x.rows {
for (j, &c) in cols.iter().enumerate() {
m.set(r, j, self.x.at(r, c));
}
}
m
}
fn domain_cols(domain: usize) -> Vec<usize> {
(domain * DOMAIN_BLOCK..(domain + 1) * DOMAIN_BLOCK).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 {
return 0.0;
}
let scores = pca_scores(&self.x, k);
ols_r2(&scores, &self.y)
}
/// Best R² obtainable using just a single domain's input block.
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);
if r2 > best.1 {
best = (d, r2);
}
}
best
}
/// Best R² obtainable using any pair of domain blocks.
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);
if r2 > best.1 {
best = ((a, b), r2);
}
}
}
best
}
fn full_r2(&self) -> f64 {
ols_r2(&self.x, &self.y)
}
}
/// A compressed model's predictive power and information loss.
#[derive(Clone, Debug)]
pub struct CompressedModel {
pub predicts: f64,
pub info_loss: f64,
pub detail: String,
}
/// The collapse-attack trait (per spec).
pub trait CollapseAttack {
fn name(&self) -> &'static str;
fn compress(&self, corpus: &BehaviorCorpus) -> CompressedModel;
}
/// One attack's outcome.
#[derive(Clone, Debug)]
pub struct CollapseReport {
pub attack: String,
pub predicts: f64,
pub info_loss: f64,
pub detail: String,
}
macro_rules! attack {
($name:ident, $label:expr, $body:expr) => {
pub struct $name;
impl CollapseAttack for $name {
fn name(&self) -> &'static str {
$label
}
fn compress(&self, corpus: &BehaviorCorpus) -> CompressedModel {
let f: fn(&BehaviorCorpus) -> CompressedModel = $body;
f(corpus)
}
}
};
}
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.
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));
}
model(best, "predict with one domain removed")
});
attack!(DomainMerging, "domain_merging", |c| {
// Merge each pair into a summed block; best prediction over pairs.
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;
}
}
}
best = f64::max(best, ols_r2(&merged, &c.y));
}
}
model(best, "predict with two domains merged")
});
attack!(ConstantFolding, "constant_folding", |_c| {
// Folding the world to constants leaves no predictive features at all.
model(0.0, "world 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")
});
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 {
let mut s = 0.0;
for col in 0..c.x.cols {
s += c.x.at(r, col);
}
agg.set(r, 0, s);
}
model(ols_r2(&agg, &c.y), "single aliased aggregate")
});
attack!(LatentFactorModeling, "latent_factor_modeling", |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!(SurrogatePrediction, "surrogate_prediction", |c| {
model(c.full_r2(), "full 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)")
});
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)")
});
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));
}
}
model(ols_r2(&c.x, &y2), "executor identity erased")
});
fn kmeans_reconstruct(y: &Mat, k: usize) -> Mat {
let n = y.rows;
if n == 0 || k == 0 {
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 assign = vec![0usize; n];
for _ in 0..12 {
// assign
for r in 0..n {
let mut best = 0;
let mut bestd = f64::MAX;
for (ci, cen) in centroids.iter().enumerate() {
let mut d = 0.0;
for col in 0..y.cols {
d += (y.at(r, col) - cen[col]).powi(2);
}
if d < bestd {
bestd = d;
best = ci;
}
}
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 {
counts[assign[r]] += 1;
for col in 0..y.cols {
sums[assign[r]][col] += y.at(r, col);
}
}
for ci in 0..k {
if counts[ci] > 0 {
for col in 0..y.cols {
centroids[ci][col] = sums[ci][col] / counts[ci] as f64;
}
}
}
}
let mut recon = Mat::zeros(n, y.cols);
for r in 0..n {
for col in 0..y.cols {
recon.set(r, col, centroids[assign[r]][col]);
}
}
recon
}
/// All eleven required attack families.
pub fn all_attacks() -> Vec<Box<dyn CollapseAttack>> {
vec![
Box::new(DomainRemoval),
Box::new(DomainMerging),
Box::new(ConstantFolding),
Box::new(CausalEdgeDeletion),
Box::new(StateAliasing),
Box::new(LatentFactorModeling),
Box::new(BehaviorClustering),
Box::new(SurrogatePrediction),
Box::new(TemporalFlattening),
Box::new(ObservationFlattening),
Box::new(ExecutorIdentityErasure),
]
}
/// Aggregate collapse summary against all gates.
#[derive(Clone, Debug)]
pub struct CollapseSummary {
pub reports: Vec<CollapseReport>,
pub best_1factor: f64,
pub best_2factor: f64,
pub best_4factor: f64,
pub max_single_domain: f64,
pub max_pair: f64,
pub min_info_loss: f64,
pub failures: Vec<String>,
}
impl CollapseSummary {
pub fn passed(&self) -> bool {
self.failures.is_empty()
}
}
/// Run every attack and check all collapse gates.
pub fn analyze(corpus: &BehaviorCorpus) -> CollapseSummary {
let mut reports = Vec::new();
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);
reports.push(CollapseReport {
attack: atk.name().to_string(),
predicts: m.predicts,
info_loss: m.info_loss,
detail: m.detail,
});
}
let best_1 = corpus.predict_k_factor(1);
let best_2 = corpus.predict_k_factor(2);
let best_4 = corpus.predict_k_factor(4);
let (_, max_single) = corpus.max_single_domain();
let (_, max_pair) = corpus.max_pair();
let mut failures = Vec::new();
if best_1 >= 0.40 {
failures.push(format!("1-factor predicts {:.3} >= 0.40", best_1));
}
if best_2 >= 0.55 {
failures.push(format!("2-factor predicts {:.3} >= 0.55", best_2));
}
if best_4 >= 0.70 {
failures.push(format!("4-factor predicts {:.3} >= 0.70", best_4));
}
if max_single > 0.30 {
failures.push(format!("single domain explains {:.3} > 0.30", max_single));
}
if max_pair > 0.55 {
failures.push(format!("domain pair explains {:.3} > 0.55", max_pair));
}
if min_info_loss < 0.35 {
failures.push(format!("min info loss {:.3} < 0.35", min_info_loss));
}
CollapseSummary {
reports,
best_1factor: best_1,
best_2factor: best_2,
best_4factor: best_4,
max_single_domain: max_single,
max_pair,
min_info_loss,
failures,
}
}
#[cfg(test)]
mod tests {
use super::*;
use world_model::{Hasher, Rng};
#[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);
let summary = analyze(&corpus);
assert!(summary.passed(), "collapse failures: {:?}", summary.failures);
assert!(summary.best_1factor < 0.40);
assert!(summary.min_info_loss >= 0.35);
}
}
+244
View File
@@ -0,0 +1,244 @@
//! Minimal dense f64 linear algebra for the collapse attacks: centering,
//! OLS (ridge-regularized) multi-output R², and PCA via power iteration.
/// Column-major-agnostic row-major matrix.
#[derive(Clone)]
pub struct Mat {
pub rows: usize,
pub cols: usize,
pub data: Vec<f64>,
}
impl Mat {
pub fn zeros(rows: usize, cols: usize) -> Self {
Mat {
rows,
cols,
data: vec![0.0; rows * cols],
}
}
#[inline]
pub fn at(&self, r: usize, c: usize) -> f64 {
self.data[r * self.cols + c]
}
#[inline]
pub fn set(&mut self, r: usize, c: usize, v: f64) {
self.data[r * self.cols + c] = v;
}
pub fn col(&self, c: usize) -> Vec<f64> {
(0..self.rows).map(|r| self.at(r, c)).collect()
}
/// Subtract the mean of each column (in place). Returns the means.
pub fn center_columns(&mut self) -> Vec<f64> {
let mut means = vec![0.0; self.cols];
for c in 0..self.cols {
let mut s = 0.0;
for r in 0..self.rows {
s += self.at(r, c);
}
means[c] = s / self.rows.max(1) as f64;
}
for r in 0..self.rows {
for c in 0..self.cols {
let v = self.at(r, c) - means[c];
self.set(r, c, v);
}
}
means
}
/// X^T X
pub fn gram(&self) -> Mat {
let p = self.cols;
let mut g = Mat::zeros(p, p);
for i in 0..p {
for j in i..p {
let mut s = 0.0;
for r in 0..self.rows {
s += self.at(r, i) * self.at(r, j);
}
g.set(i, j, s);
g.set(j, i, s);
}
}
g
}
}
/// Solve (A + λI) x = b for symmetric positive-ish A via Gauss-Jordan.
pub fn solve_ridge(a: &Mat, b: &[f64], lambda: f64) -> Vec<f64> {
let n = a.rows;
let mut m = a.clone();
for i in 0..n {
let v = m.at(i, i) + lambda;
m.set(i, i, v);
}
let mut x = b.to_vec();
// Gaussian elimination with partial pivoting.
for col in 0..n {
let mut piv = col;
let mut best = m.at(col, col).abs();
for r in (col + 1)..n {
let v = m.at(r, col).abs();
if v > best {
best = v;
piv = r;
}
}
if best < 1e-12 {
continue;
}
if piv != col {
for c in 0..n {
let tmp = m.at(col, c);
m.set(col, c, m.at(piv, c));
m.set(piv, c, tmp);
}
x.swap(col, piv);
}
let d = m.at(col, col);
for r in 0..n {
if r != col {
let f = m.at(r, col) / d;
if f != 0.0 {
for c in col..n {
let v = m.at(r, c) - f * m.at(col, c);
m.set(r, c, v);
}
x[r] -= f * x[col];
}
}
}
}
for i in 0..n {
let d = m.at(i, i);
if d.abs() > 1e-12 {
x[i] /= d;
} else {
x[i] = 0.0;
}
}
x
}
/// Average R^2 of predicting each (centered) output column from the centered
/// design matrix `x` using ridge OLS. Returns a value clamped to [0, 1].
pub fn ols_r2(x: &Mat, y: &Mat) -> f64 {
if x.cols == 0 || x.rows == 0 {
return 0.0;
}
let g = x.gram();
let lambda = 1e-6 * (1.0 + trace(&g) / x.cols as f64);
let mut total_r2 = 0.0;
let mut counted = 0;
for oc in 0..y.cols {
let yc = y.col(oc);
// X^T y
let mut xty = vec![0.0; x.cols];
for i in 0..x.cols {
let mut s = 0.0;
for r in 0..x.rows {
s += x.at(r, i) * yc[r];
}
xty[i] = s;
}
let beta = solve_ridge(&g, &xty, lambda);
// residuals
let mut ss_res = 0.0;
let mut ss_tot = 0.0;
for r in 0..x.rows {
let mut pred = 0.0;
for i in 0..x.cols {
pred += x.at(r, i) * beta[i];
}
ss_res += (yc[r] - pred).powi(2);
ss_tot += yc[r].powi(2);
}
if ss_tot > 1e-9 {
let r2 = 1.0 - ss_res / ss_tot;
total_r2 += r2.clamp(0.0, 1.0);
counted += 1;
}
}
if counted == 0 {
0.0
} else {
total_r2 / counted as f64
}
}
fn trace(m: &Mat) -> f64 {
(0..m.rows.min(m.cols)).map(|i| m.at(i, i)).sum()
}
/// Top-`k` principal-component scores of a centered matrix `x` via power
/// iteration with deflation. Returns an `n x k` score matrix.
pub fn pca_scores(x: &Mat, k: usize) -> Mat {
let p = x.cols;
let mut cov = x.gram(); // proportional to covariance
let kk = k.min(p);
let mut comps: Vec<Vec<f64>> = Vec::new();
for _ in 0..kk {
// power iteration
let mut v = vec![0.0; p];
for (i, vi) in v.iter_mut().enumerate() {
*vi = 1.0 + (i as f64) * 0.001;
}
normalize(&mut v);
for _ in 0..64 {
let mut nv = matvec(&cov, &v);
normalize(&mut nv);
let diff: f64 = nv.iter().zip(&v).map(|(a, b)| (a - b).abs()).sum();
v = nv;
if diff < 1e-9 {
break;
}
}
// eigenvalue
let av = matvec(&cov, &v);
let lambda: f64 = v.iter().zip(&av).map(|(a, b)| a * b).sum();
// deflate
for i in 0..p {
for j in 0..p {
let val = cov.at(i, j) - lambda * v[i] * v[j];
cov.set(i, j, val);
}
}
comps.push(v);
}
// scores = X * comps
let mut scores = Mat::zeros(x.rows, kk);
for r in 0..x.rows {
for (cj, comp) in comps.iter().enumerate() {
let mut s = 0.0;
for i in 0..p {
s += x.at(r, i) * comp[i];
}
scores.set(r, cj, s);
}
}
scores
}
fn matvec(m: &Mat, v: &[f64]) -> Vec<f64> {
let mut out = vec![0.0; m.rows];
for r in 0..m.rows {
let mut s = 0.0;
for c in 0..m.cols {
s += m.at(r, c) * v[c];
}
out[r] = s;
}
out
}
fn normalize(v: &mut [f64]) {
let n: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
if n > 1e-12 {
for x in v.iter_mut() {
*x /= n;
}
}
}
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "generators"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
world_model = { path = "../world_model" }
rune_ir = { path = "../rune_ir" }
trace_model = { path = "../trace_model" }
[lib]
path = "src/lib.rs"
+344
View File
@@ -0,0 +1,344 @@
//! `generators` — produce worlds, rune programs, executor sets, semantic
//! contracts, and perturbation batches. Generators must reject flat cases:
//! every generated case is checked against the generated-case gates before it
//! is admitted to the corpus.
use rune_ir::{Op, RuneProgram, RuneToken, ALL_OPS};
use trace_model::numeric_rank;
use world_model::{
standard_executors, ContractId, ExecutionContext, PerturbationAxis, ProgramId, Rng,
TraceDifferenceExpectation, WorldId, WorldSnapshot, HIDDEN_LANES, LANES, NUM_DOMAINS,
};
/// Minimum domain reference entropy (bits) a generated program must show.
pub const MIN_DOMAIN_ENTROPY: f64 = 2.5;
/// Semantic contract (per spec) with the default thresholds.
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct SemanticContract {
pub id: ContractId,
pub min_causal_rank: usize,
pub min_domain_participation: usize,
pub min_future_sensitivity: f64,
pub min_context_divergence: f64,
pub max_compressibility: f64,
}
impl SemanticContract {
pub fn default_with_seed(seed: u64) -> Self {
SemanticContract {
id: ContractId(seed),
min_causal_rank: 6,
min_domain_participation: 4,
min_future_sensitivity: 0.50,
min_context_divergence: 0.40,
max_compressibility: 0.70,
}
}
}
/// One perturbed variant of a base world.
pub struct PerturbedCase {
pub axis_name: String,
pub world: WorldSnapshot,
pub expectation: TraceDifferenceExpectation,
}
/// A complete generated case (per spec).
pub struct GeneratedCase {
pub world: WorldSnapshot,
pub program: RuneProgram,
pub contexts: Vec<ExecutionContext>,
pub contract: SemanticContract,
pub perturbations: Vec<PerturbedCase>,
pub world_seed: u64,
pub program_seed: u64,
pub contract_seed: u64,
pub perturbation_seed: u64,
}
/// Generate a rich world from a seed.
pub fn generate_world(seed: u64) -> WorldSnapshot {
let mut rng = Rng::derive(seed, "world");
let mut w = WorldSnapshot::blank(WorldId(seed), seed);
for (i, d) in w.domains.iter_mut().enumerate() {
let spread = 1000 + (i as i64) * 137;
for l in 0..LANES {
d.observed[l] = rng.range_i64(-spread, spread);
}
for l in 0..HIDDEN_LANES {
let mut v = rng.range_i64(-spread, spread);
if v == 0 {
v = 1 + i as i64;
}
d.hidden[l] = v;
}
}
for j in 0..NUM_DOMAINS {
for i in 0..NUM_DOMAINS {
w.causal_state.coupling[j][i] = rng.range_i64(-17, 17);
}
}
for di in 0..NUM_DOMAINS {
for l in 0..LANES {
w.observation_state.visible[di][l] = !rng.chance(0.15);
}
if (0..LANES).all(|l| !w.observation_state.visible[di][l]) {
w.observation_state.visible[di][0] = true;
}
}
w.observation_state.noise_seed = rng.next_u64();
for r in w.execution_state.accumulator.iter_mut() {
*r = rng.range_i64(-50, 50);
}
w
}
/// Generate a rune program from a seed. Construction guarantees every opcode
/// appears and every domain is both read and written, so the case is never
/// flat.
pub fn generate_program(seed: u64) -> RuneProgram {
let mut rng = Rng::derive(seed, "program");
let len = 28 + rng.below(13); // 28..=40
let mut tokens = Vec::with_capacity(len);
for i in 0..len {
let op = if rng.chance(0.7) {
ALL_OPS[i % ALL_OPS.len()]
} else {
Op::from_u8(rng.next_u64() as u8)
};
// 3 and 5 are coprime with 8, so src/dst sweep all domains.
let a = ((i * 3 + rng.below(2)) % NUM_DOMAINS) as u8;
let mut b = ((i * 5 + 1 + rng.below(2)) % NUM_DOMAINS) as u8;
if b == a {
b = (b + 1) % NUM_DOMAINS as u8;
}
tokens.push(RuneToken {
op,
a,
b,
c: rng.next_u64() as u8,
imm: rng.range_i64(-100_000, 100_000),
});
}
RuneProgram {
id: ProgramId(seed),
tokens,
seed,
}
}
/// Generate >= 3 executors.
pub fn generate_contexts(seed: u64) -> Vec<ExecutionContext> {
let mut rng = Rng::derive(seed, "context-count");
standard_executors(seed, 3 + rng.below(2))
}
/// Build the perturbation batch for a world (>= 10 perturbations drawn from
/// domain surfaces).
pub fn generate_perturbations(world: &WorldSnapshot, seed: u64, count: usize) -> Vec<PerturbedCase> {
use world_model::WorldDomain;
let count = count.max(10);
let mut axes: Vec<Box<dyn PerturbationAxis>> = Vec::new();
for d in &world.domains {
axes.extend(d.perturbation_axes());
}
let mut rng = Rng::derive(seed, "perturb");
let mut out = Vec::with_capacity(count);
for k in 0..count {
let idx = (k * 7 + rng.below(axes.len())) % axes.len();
let axis = &axes[idx];
out.push(PerturbedCase {
axis_name: axis.name(),
world: axis.apply(world),
expectation: axis.expected_trace_difference(),
});
}
out
}
/// Generate a full case from a master seed.
pub fn generate_case(master_seed: u64) -> GeneratedCase {
let world_seed = derive(master_seed, "world");
let program_seed = derive(master_seed, "program");
let contract_seed = derive(master_seed, "contract");
let perturbation_seed = derive(master_seed, "perturb");
let world = generate_world(world_seed);
let program = generate_program(program_seed);
let contexts = generate_contexts(master_seed);
let contract = SemanticContract::default_with_seed(contract_seed);
let perturbations = generate_perturbations(&world, perturbation_seed, 10);
GeneratedCase {
world,
program,
contexts,
contract,
perturbations,
world_seed,
program_seed,
contract_seed,
perturbation_seed,
}
}
pub fn derive(seed: u64, tag: &str) -> u64 {
let mut h = world_model::Hasher::new();
h.write_tag(tag);
h.write_u64(seed);
h.finish().0
}
// ---------------------------------------------------------------------------
// Generated-case gates: reject flat cases.
// ---------------------------------------------------------------------------
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CaseEstimate {
pub estimated_causal_rank: usize,
pub domain_entropy: f64,
pub perturbation_axes: usize,
pub executor_count: usize,
pub future_dependence: bool,
pub hidden_observed_divergence: bool,
pub nonuniform_fingerprints: bool,
}
impl GeneratedCase {
/// Estimate the structural richness of the case without running a runtime.
pub fn estimate(&self) -> CaseEstimate {
let mut adj = [[0.0f64; NUM_DOMAINS]; NUM_DOMAINS];
let mut refs = [0u32; NUM_DOMAINS];
for t in &self.program.tokens {
let s = t.src_domain();
let d = t.dst_domain();
let phase = 1.0 + t.lane() as f64 + 4.0 * t.lane2() as f64;
adj[d][s] += phase;
adj[d][d] += 0.5 + t.mode() as f64;
refs[s] += 1;
refs[d] += 1;
}
let rows: Vec<Vec<f64>> = adj.iter().map(|r| r.to_vec()).collect();
let est_rank = numeric_rank(&rows);
let total: u32 = refs.iter().sum();
let mut entropy = 0.0;
if total > 0 {
for &c in &refs {
if c > 0 {
let p = c as f64 / total as f64;
entropy -= p * p.log2();
}
}
}
let future = self.program.tokens.iter().any(|t| matches!(t.op, Op::Schedule));
let hidden_div = self
.world
.domains
.iter()
.any(|d| d.hidden.iter().any(|&v| v != 0))
|| self
.world
.observation_state
.visible
.iter()
.any(|row| row.iter().any(|&v| !v));
use world_model::WorldDomain;
let mut prints: Vec<_> = self.world.domains.iter().map(|d| d.fingerprint().hash).collect();
prints.sort();
prints.dedup();
let nonuniform = prints.len() > 1;
CaseEstimate {
estimated_causal_rank: est_rank,
domain_entropy: entropy,
perturbation_axes: self.perturbations.len().max(count_axes(&self.world)),
executor_count: self.contexts.len(),
future_dependence: future,
hidden_observed_divergence: hidden_div,
nonuniform_fingerprints: nonuniform,
}
}
}
fn count_axes(world: &WorldSnapshot) -> usize {
use world_model::WorldDomain;
world.domains.iter().map(|d| d.perturbation_axes().len()).sum()
}
/// Reasons a case failed the generated gates (empty = passed).
pub fn generated_gate_failures(est: &CaseEstimate) -> Vec<String> {
let mut f = Vec::new();
if est.estimated_causal_rank < 6 {
f.push(format!("estimated_causal_rank {} < 6", est.estimated_causal_rank));
}
if est.domain_entropy < MIN_DOMAIN_ENTROPY {
f.push(format!(
"domain_entropy {:.3} < {:.3}",
est.domain_entropy, MIN_DOMAIN_ENTROPY
));
}
if est.perturbation_axes < 10 {
f.push(format!("perturbation_axes {} < 10", est.perturbation_axes));
}
if est.executor_count < 3 {
f.push(format!("executor_count {} < 3", est.executor_count));
}
if !est.future_dependence {
f.push("future_dependence absent".into());
}
if !est.hidden_observed_divergence {
f.push("no hidden/observed divergence".into());
}
if !est.nonuniform_fingerprints {
f.push("uniform domain fingerprints".into());
}
f
}
/// Generate a case that passes the generated gates, retrying with fresh seeds.
pub fn generate_accepted_case(master_seed: u64) -> (GeneratedCase, u64) {
let mut s = master_seed;
for _ in 0..64 {
let case = generate_case(s);
if generated_gate_failures(&case.estimate()).is_empty() {
return (case, s);
}
s = derive(s, "retry");
}
let case = generate_case(s);
(case, s)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepted_cases_pass_generated_gates() {
for i in 0..200u64 {
let (case, _) = generate_accepted_case(0x1234 ^ i.wrapping_mul(0x9e3779b97f4a7c15));
let failures = generated_gate_failures(&case.estimate());
assert!(failures.is_empty(), "case {i} failed: {:?}", failures);
assert!(case.contexts.len() >= 3);
assert!(case.perturbations.len() >= 10);
}
}
#[test]
fn generation_is_deterministic() {
let (a, sa) = generate_accepted_case(999);
let (b, sb) = generate_accepted_case(999);
assert_eq!(sa, sb);
assert_eq!(a.program.content_hash(), b.program.content_hash());
assert_eq!(a.world.content_hash(), b.world.content_hash());
}
}
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "reference_runtime"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
world_model = { path = "../world_model" }
rune_ir = { path = "../rune_ir" }
trace_model = { path = "../trace_model" }
[lib]
path = "src/lib.rs"
+654
View File
@@ -0,0 +1,654 @@
//! The execution engine. Both the reference runtime and the runtime under
//! test call [`execute`] with the canonical [`EngineConfig`]; semantic mutants
//! are nothing more than an `EngineConfig` with one behavior-affecting knob
//! changed, which is what makes them detectable by the equivalence gate.
//!
//! All arithmetic is integer and total: division is guarded, overflow wraps,
//! and every token produces a defined effect or a *logged* fault. The engine
//! never panics.
use rune_ir::{Op, 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,
};
/// All behavior-affecting knobs of the engine. The reference config is the
/// executable spec; mutants flip exactly one knob.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct EngineConfig {
pub c1: u64,
pub c2: u64,
pub s1: u32,
pub s2: u32,
pub s3: u32,
pub use_coupling: bool,
pub use_hidden: bool,
pub use_executor_salt: bool,
pub branch_enabled: bool,
pub schedule_enabled: bool,
pub record_causal: bool,
pub domain_mask: [bool; NUM_DOMAINS],
pub op_enabled: [bool; 12],
pub future_turns: usize,
pub diffuse_span: usize,
}
impl EngineConfig {
/// The canonical executable-spec configuration.
pub fn reference() -> Self {
EngineConfig {
c1: 0xff51afd7ed558ccd,
c2: 0xc4ceb9fe1a85ec53,
s1: 33,
s2: 29,
s3: 32,
use_coupling: true,
use_hidden: true,
use_executor_salt: true,
branch_enabled: true,
schedule_enabled: true,
record_causal: true,
domain_mask: [true; NUM_DOMAINS],
op_enabled: [true; 12],
future_turns: 3,
diffuse_span: 3,
}
}
}
/// Input to a resolution.
#[derive(Clone, Debug)]
pub struct ResolutionInput {
pub world: WorldSnapshot,
pub program: rune_ir::RuneProgram,
pub contexts: Vec<ExecutionContext>,
pub contract_seed: u64,
pub perturbation_seed: u64,
}
/// Output of a resolution (per spec). Execution always returns this; no rune
/// stream is ever rejected.
#[derive(Clone, Debug)]
pub struct ResolutionResult {
pub delta: WorldDelta,
pub trace: ExecutionTrace,
pub faults: FaultLog,
pub replay: ReplayRecord,
}
/// Canonical, comparable view of a result. Reference and runtime-under-test
/// must produce identical canonical views.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Canonical {
pub delta_hash: Hash,
pub trace_hash: Hash,
pub fault_hash: Hash,
pub replay_hash: Hash,
pub future_hash: Hash,
}
/// Produce the canonical comparison tuple for a result.
pub fn canonical(r: &ResolutionResult) -> Canonical {
Canonical {
delta_hash: r.delta.hash(),
trace_hash: r.trace.canonical_hash(),
fault_hash: r.faults.hash(),
replay_hash: r.replay.hash(),
future_hash: r.replay.future_hash,
}
}
// ---------------------------------------------------------------------------
// Recorder: accumulates graph/trace data during a single-context run.
// ---------------------------------------------------------------------------
struct Recorder {
read_graph: DomainAccessGraph,
write_graph: DomainAccessGraph,
causal_graph: CausalGraph,
info_flow: InformationFlowGraph,
temporal: TemporalGraph,
faults: FaultLog,
}
impl Recorder {
fn new() -> Self {
Recorder {
read_graph: DomainAccessGraph::default(),
write_graph: DomainAccessGraph::default(),
causal_graph: CausalGraph::default(),
info_flow: InformationFlowGraph::default(),
temporal: TemporalGraph::default(),
faults: FaultLog::default(),
}
}
#[allow(clippy::too_many_arguments)]
fn flow(
&mut self,
cfg: &EngineConfig,
from_dom: usize,
from_lane: usize,
from_hidden: bool,
to_dom: usize,
to_lane: usize,
to_hidden: bool,
step: u32,
weight: i64,
) {
if !cfg.domain_mask[from_dom] || !cfg.domain_mask[to_dom] {
return;
}
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()));
if cfg.record_causal {
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,
});
}
}
}
// ---------------------------------------------------------------------------
// Core arithmetic.
// ---------------------------------------------------------------------------
#[inline]
fn avalanche(cfg: &EngineConfig, z: i64) -> i64 {
let mut u = z as u64;
u ^= u >> cfg.s1;
u = u.wrapping_mul(cfg.c1);
u ^= u >> cfg.s2;
u = u.wrapping_mul(cfg.c2);
u ^= u >> cfg.s3;
u as i64
}
#[inline]
fn combine(cfg: &EngineConfig, 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);
if cfg.use_coupling {
z = z.wrapping_add(coupling.wrapping_mul(b & 0xffff));
}
if cfg.use_executor_salt {
z ^= ctx.salt() as i64;
z = z.wrapping_add(ctx.profile.bias);
z = z.rotate_left((ctx.profile.rotate % 63) + 1);
}
avalanche(cfg, z)
}
#[inline]
fn read_lane(cfg: &EngineConfig, w: &WorldSnapshot, dom: usize, lane: usize, hidden: bool) -> i64 {
if !cfg.domain_mask[dom] {
return 0;
}
if hidden {
if cfg.use_hidden {
w.domains[dom].hidden[lane % HIDDEN_LANES]
} else {
0
}
} else {
w.domains[dom].observed[lane % LANES]
}
}
#[inline]
fn write_lane(cfg: &EngineConfig, w: &mut WorldSnapshot, dom: usize, lane: usize, hidden: bool, val: i64) {
if !cfg.domain_mask[dom] {
return;
}
if hidden {
w.domains[dom].hidden[lane % HIDDEN_LANES] = val;
} else {
w.domains[dom].observed[lane % LANES] = val;
}
}
#[inline]
fn coupling_at(w: &WorldSnapshot, to: usize, from: usize) -> i64 {
w.causal_state.coupling[to][from]
}
// ---------------------------------------------------------------------------
// Single-context program run.
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Future stepping (genuine future dependence over 3 turns).
// ---------------------------------------------------------------------------
fn step_world(cfg: &EngineConfig, w: &mut WorldSnapshot) {
// Resolve scheduled effects due this turn.
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 cfg.domain_mask[d] {
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;
// Coupling diffusion: every domain pulls from every other through the
// coupling matrix, then avalanches. This propagates any execution effect
// into the future and makes future state sensitive to the present.
let snap = w.domains.clone();
for j in 0..NUM_DOMAINS {
if !cfg.domain_mask[j] {
continue;
}
for lane in 0..LANES {
let mut z = w.domains[j].observed[lane];
for i in 0..NUM_DOMAINS {
if !cfg.domain_mask[i] {
continue;
}
if cfg.use_coupling {
let c = w.causal_state.coupling[j][i];
z = z.wrapping_add(c.wrapping_mul(snap[i].observed[lane] & 0xff));
} else {
z = z.wrapping_add(snap[i].observed[lane] & 0xff);
}
}
w.domains[j].observed[lane] = avalanche(cfg, 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] = if cfg.use_hidden { avalanche(cfg, base) } else { base };
}
}
w.turn = w.turn.wrapping_add(1);
}
fn future_hash(cfg: &EngineConfig, start: &WorldSnapshot) -> Hash {
let mut w = start.clone();
let mut h = Hasher::new();
h.write_tag("future-3");
for _ in 0..cfg.future_turns {
step_world(cfg, &mut w);
for v in w.ground_truth() {
h.write_i64(v);
}
}
h.finish()
}
// ---------------------------------------------------------------------------
// Divergence + behavior fingerprint.
// ---------------------------------------------------------------------------
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,
}
}
fn behavior_fingerprint(
before: &WorldSnapshot,
after: &WorldSnapshot,
delta: &WorldDelta,
rec: &Recorder,
divergence: &DivergenceGraph,
future: Hash,
) -> BehaviorFingerprint {
let mut features: Vec<i64> = Vec::new();
// Per-domain observed and hidden delta magnitudes.
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);
}
// Structural counts.
features.push(rec.causal_graph.causal_rank() as i64);
features.push(rec.causal_graph.edge_count() as i64);
features.push(rec.read_graph.touched_count() as i64);
features.push(rec.write_graph.touched_count() as i64);
features.push(rec.info_flow.total_bits() as i64);
features.push(rec.temporal.edge_count() as i64);
features.push((divergence.mean_divergence() * 1_000_000.0) as i64);
features.push(future.0 as i64);
let _ = (before, after);
BehaviorFingerprint::from_features(features)
}
// ---------------------------------------------------------------------------
// Public entry point.
// ---------------------------------------------------------------------------
/// Execute a resolution with the given engine config.
pub fn execute(cfg: &EngineConfig, input: &ResolutionInput) -> ResolutionResult {
let mut contexts = input.contexts.clone();
if contexts.is_empty() {
contexts = world_model::standard_executors(input.world.seed, 3);
}
// A masked (removed) domain contributes nothing: its state is erased up
// front so it cannot leak into deltas, future hashes, or fingerprints.
let mut world0 = input.world.clone();
for d in 0..NUM_DOMAINS {
if !cfg.domain_mask[d] {
world0.domains[d].observed = [0; LANES];
world0.domains[d].hidden = [0; HIDDEN_LANES];
}
}
// Run under every executor; keep the primary (index 0) full recording.
let mut finals: Vec<WorldSnapshot> = Vec::with_capacity(contexts.len());
let mut primary: Option<(WorldSnapshot, Recorder)> = None;
for (idx, ctx) in contexts.iter().enumerate() {
let (fin, rec) = run_program_with_program(cfg, &world0, ctx, &input.program);
if idx == 0 {
primary = Some((fin.clone(), rec));
}
finals.push(fin);
}
let (primary_final, rec) = primary.expect("at least one executor");
let delta = WorldDelta::between(&world0, &primary_final);
let divergence = compute_divergence(&finals);
let fhash = future_hash(cfg, &primary_final);
let behavior =
behavior_fingerprint(&world0, &primary_final, &delta, &rec, &divergence, fhash);
let trace = ExecutionTrace {
read_graph: rec.read_graph.clone(),
write_graph: rec.write_graph.clone(),
causal_graph: rec.causal_graph.clone(),
information_flow: rec.info_flow.clone(),
executor_divergence: divergence,
temporal_graph: rec.temporal.clone(),
perturbation_response: PerturbationResponse::default(),
behavior_fingerprint: behavior,
};
let faults = rec.faults;
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,
replay,
}
}
/// `run_program` variant that takes the program explicitly. (The borrow-split
/// helper above intentionally returns no tokens; this is the real driver.)
fn run_program_with_program(
cfg: &EngineConfig,
world: &WorldSnapshot,
ctx: &ExecutionContext,
program: &rune_ir::RuneProgram,
) -> (WorldSnapshot, Recorder) {
let mut w = world.clone();
let mut rec = Recorder::new();
let mut acc = w.execution_state.accumulator;
let mut acc_src: [usize; REGS] = [0; REGS];
for (i, tok) in program.tokens.iter().enumerate() {
let step = i as 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 = coupling_at(&w, dst, src);
if !cfg.op_enabled[tok.op.to_u8() as usize] {
rec.faults.push(FaultCode::NoEffectToken, step, tok.op.to_u8() as i64);
continue;
}
interpret(cfg, ctx, &mut w, &mut rec, &mut acc, &mut acc_src, tok, step, src, dst, lane, lane2, kc, coupling);
let r = (step as usize) % REGS;
acc[r] = acc[r].wrapping_add(w.domains[dst].observed[lane]);
}
w.execution_state.accumulator = acc;
(w, rec)
}
#[allow(clippy::too_many_arguments)]
fn interpret(
cfg: &EngineConfig,
ctx: &ExecutionContext,
w: &mut WorldSnapshot,
rec: &mut Recorder,
acc: &mut [i64; REGS],
acc_src: &mut [usize; REGS],
tok: &RuneToken,
step: u32,
src: usize,
dst: usize,
lane: usize,
lane2: usize,
kc: u64,
coupling: i64,
) {
match tok.op {
Op::Mix => {
let a = read_lane(cfg, w, src, lane, false);
let b = read_lane(cfg, w, dst, lane2, false);
let v = combine(cfg, ctx, a, b, coupling, kc);
write_lane(cfg, w, dst, lane, false, v);
rec.flow(cfg, src, lane, false, dst, lane, false, step, v);
rec.flow(cfg, dst, lane2, false, dst, lane, false, step, v);
}
Op::Channel => {
let a = read_lane(cfg, w, src, lane, false);
let v = combine(cfg, ctx, a, coupling, coupling, kc);
write_lane(cfg, w, dst, lane2, false, v);
rec.flow(cfg, src, lane, false, dst, lane2, false, step, v);
}
Op::Branch => {
let probe = read_lane(cfg, w, src, lane, false);
let take_hot = if cfg.branch_enabled {
probe.wrapping_add(ctx.profile.bias) > ctx.profile.branch_threshold
} else {
true
};
if take_hot {
let b = read_lane(cfg, w, dst, lane, false);
let v = combine(cfg, ctx, probe, b, coupling, kc);
write_lane(cfg, w, dst, lane, false, v);
rec.flow(cfg, src, lane, false, dst, lane, false, step, v);
} else {
let b = read_lane(cfg, w, dst, lane2, false);
let v = combine(cfg, ctx, b, probe, coupling, kc).wrapping_add(0x5bd1e9);
write_lane(cfg, w, dst, lane2, false, v);
rec.flow(cfg, src, lane, false, dst, lane2, false, step, v);
rec.faults.push(FaultCode::UnreachableBranch, step, 0);
}
}
Op::Schedule => {
let a = read_lane(cfg, w, src, lane, false);
let b = read_lane(cfg, w, dst, lane, false);
let v = combine(cfg, ctx, a, b, coupling, kc);
if cfg.schedule_enabled {
let offset = 1 + (tok.imm.rem_euclid(3)) as u8;
let hidden = tok.mode() & 1 == 1;
w.time_state.pending.push(ScheduledEffect {
turn_offset: offset,
domain: DomainId(dst as u8),
lane,
hidden,
value: v,
});
rec.temporal.edges.push((step, offset, dst as u8));
rec.flow(cfg, src, lane, false, dst, lane, hidden, step, v);
} else {
rec.faults.push(FaultCode::NoEffectToken, step, 1);
}
}
Op::Resonate => {
let a = read_lane(cfg, w, src, lane, false);
let b = read_lane(cfg, w, dst, lane, false);
let m = combine(cfg, ctx, a, b, coupling, kc);
let va = a.wrapping_add(m);
let vb = b ^ m;
write_lane(cfg, w, src, lane, false, va);
write_lane(cfg, w, dst, lane, false, vb);
rec.flow(cfg, dst, lane, false, src, lane, false, step, va);
rec.flow(cfg, src, lane, false, dst, lane, false, step, vb);
}
Op::Observe => {
let reg = tok.mode() % REGS;
let mut z: i64 = acc[reg];
let proj = w.observed_projection();
for k in 0..3 {
let d = (src + k) % NUM_DOMAINS;
if !cfg.domain_mask[d] {
continue;
}
let idx = d * LANES + (lane + k) % LANES;
z = combine(cfg, ctx, z, proj[idx], coupling_at(w, dst, d), kc);
rec.flow(cfg, d, (lane + k) % LANES, false, dst, lane, true, step, z);
}
acc[reg] = z;
acc_src[reg] = src;
write_lane(cfg, w, dst, tok.mode() % HIDDEN_LANES, true, z);
}
Op::Collapse => {
let reg = tok.mode() % REGS;
let a = acc[reg];
let b = read_lane(cfg, w, dst, lane, false);
if a == 0 {
rec.faults.push(FaultCode::EmptyAccumulator, step, reg as i64);
}
let v = combine(cfg, ctx, a, b, coupling, kc);
write_lane(cfg, w, dst, lane, false, v);
rec.flow(cfg, acc_src[reg], 0, true, dst, lane, false, step, v);
}
Op::Invert => {
let b = read_lane(cfg, w, dst, lane, false);
let mut v = avalanche(cfg, (!b).wrapping_add(tok.imm));
if cfg.use_executor_salt {
v ^= ctx.salt() as i64;
v = v.wrapping_add(ctx.profile.bias);
}
write_lane(cfg, w, dst, lane, false, v);
rec.flow(cfg, dst, lane, false, dst, lane, false, step, v);
}
Op::Diffuse => {
let a = read_lane(cfg, w, src, lane, false);
for k in 0..cfg.diffuse_span {
let d = (src + 1 + k) % NUM_DOMAINS;
let tl = (lane + k) % LANES;
let prev = read_lane(cfg, w, d, tl, false);
let v = combine(cfg, ctx, a, prev, coupling_at(w, d, src), DomainKind::from_index(d).mix_const());
write_lane(cfg, w, d, tl, false, prev.wrapping_add(v));
rec.flow(cfg, src, lane, false, d, tl, false, step, v);
}
}
Op::Anchor => {
let bound = (tok.imm.unsigned_abs() % 1_000_000) as i64 + 1;
let b = read_lane(cfg, w, dst, lane, false);
let diag = coupling_at(w, dst, dst);
let mut mixed = b.wrapping_add(diag);
if cfg.use_executor_salt {
mixed = mixed
.wrapping_add(ctx.profile.bias)
.wrapping_add((ctx.salt() & 0xffff) as i64);
}
let clamped = mixed.clamp(-bound, bound);
if clamped != mixed {
rec.faults.push(FaultCode::Saturated, step, bound);
}
write_lane(cfg, w, dst, lane, false, clamped);
rec.flow(cfg, dst, lane, false, dst, lane, false, step, clamped);
}
Op::Echoback => {
let h = read_lane(cfg, w, dst, tok.mode() % HIDDEN_LANES, true);
let b = read_lane(cfg, w, dst, lane, false);
let v = combine(cfg, ctx, h, b, coupling, kc);
write_lane(cfg, w, dst, lane, false, v);
rec.flow(cfg, dst, tok.mode() % HIDDEN_LANES, true, dst, lane, false, step, v);
}
Op::Imprint => {
let b = read_lane(cfg, w, dst, lane, false);
let hl = tok.mode() % HIDDEN_LANES;
let prevh = read_lane(cfg, w, dst, hl, true);
let v = combine(cfg, ctx, b, prevh, coupling, kc);
write_lane(cfg, w, dst, hl, true, v);
rec.flow(cfg, dst, lane, false, dst, hl, true, step, v);
}
}
}
+117
View File
@@ -0,0 +1,117 @@
//! `reference_runtime` — the executable specification. Every execution in CI
//! runs the reference and the runtime-under-test and asserts their canonical
//! views are identical. The reference is intentionally the simplest correct
//! expression of the engine.
pub mod engine;
pub use engine::{
canonical, execute, Canonical, EngineConfig, ResolutionInput, ResolutionResult,
};
/// The runtime trait (per spec).
pub trait Runtime {
fn resolve(&self, input: ResolutionInput) -> ResolutionResult;
}
/// The reference runtime: executes with the canonical engine config.
#[derive(Clone, Debug, Default)]
pub struct ReferenceRuntime;
impl ReferenceRuntime {
pub fn new() -> Self {
ReferenceRuntime
}
}
impl Runtime for ReferenceRuntime {
fn resolve(&self, input: ResolutionInput) -> ResolutionResult {
execute(&EngineConfig::reference(), &input)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rune_ir::{Op, RuneProgram, RuneToken, ALL_OPS};
use world_model::{standard_executors, Rng, WorldId, WorldSnapshot, NUM_DOMAINS};
fn random_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 {
d.observed[l] = rng.range_i64(-5000, 5000);
}
for l in 0..world_model::HIDDEN_LANES {
d.hidden[l] = rng.range_i64(-5000, 5000);
}
}
for j in 0..NUM_DOMAINS {
for i in 0..NUM_DOMAINS {
w.causal_state.coupling[j][i] = rng.range_i64(-17, 17);
}
}
let tokens: Vec<RuneToken> = (0..30)
.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,
imm: rng.range_i64(-100000, 100000),
})
.collect();
ResolutionInput {
world: w,
program: RuneProgram { id: world_model::ProgramId(seed), tokens, seed },
contexts: standard_executors(seed, 3),
contract_seed: seed,
perturbation_seed: seed,
}
}
#[test]
fn execution_is_deterministic() {
let cfg = EngineConfig::reference();
for s in 0..200 {
let input = random_input(s);
let a = execute(&cfg, &input);
let b = execute(&cfg, &input);
assert_eq!(canonical(&a), canonical(&b), "nondeterministic at seed {s}");
}
}
#[test]
fn reference_never_panics_on_arbitrary_runes() {
// Totality: any token stream resolves without panic.
let cfg = EngineConfig::reference();
for s in 0..500 {
let input = random_input(s ^ 0xdead);
let r = execute(&cfg, &input);
// result is always produced; faults are logged not thrown
let _ = r.faults.faults.len();
}
}
#[test]
fn reference_matches_runtime_under_test_path() {
// Reference and the config-driven engine agree for the canonical config.
let cfg = EngineConfig::reference();
let rr = ReferenceRuntime::new();
for s in 0..100 {
let input = random_input(s);
let a = canonical(&execute(&cfg, &input));
let b = canonical(&rr.resolve(input.clone()));
assert_eq!(a, b);
}
}
#[test]
fn masked_domain_has_zero_delta() {
let mut cfg = EngineConfig::reference();
cfg.domain_mask[3] = false;
let input = random_input(77);
let r = execute(&cfg, &input);
assert!(r.delta.domain_deltas[3].is_zero());
}
}
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "replay_corpus"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
world_model = { path = "../world_model" }
rune_ir = { path = "../rune_ir" }
trace_model = { path = "../trace_model" }
generators = { path = "../generators" }
reference_runtime = { path = "../reference_runtime" }
[lib]
path = "src/lib.rs"
+145
View File
@@ -0,0 +1,145 @@
//! `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.
use generators::generate_accepted_case;
use reference_runtime::{execute, EngineConfig, ResolutionInput};
use world_model::Hash;
/// A permanent replay case (per spec) plus the master seed needed to
/// regenerate the full case deterministically.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ReplayCase {
pub master_seed: u64,
pub world_seed: u64,
pub program_seed: u64,
pub contract_seed: u64,
pub perturbation_seed: u64,
pub expected_trace_hash: Hash,
pub expected_delta_hash: Hash,
pub expected_future_hash: Hash,
}
fn input_for(master_seed: u64) -> (ResolutionInput, u64, u64, u64, u64) {
let (case, accepted_seed) = generate_accepted_case(master_seed);
let _ = accepted_seed;
let input = ResolutionInput {
world: case.world.clone(),
program: case.program.clone(),
contexts: case.contexts.clone(),
contract_seed: case.contract_seed,
perturbation_seed: case.perturbation_seed,
};
(
input,
case.world_seed,
case.program_seed,
case.contract_seed,
case.perturbation_seed,
)
}
/// Build a single replay case from a master seed.
pub fn build_case(master_seed: u64) -> ReplayCase {
let (input, ws, ps, cs, prs) = input_for(master_seed);
let r = execute(&EngineConfig::reference(), &input);
ReplayCase {
master_seed,
world_seed: ws,
program_seed: ps,
contract_seed: cs,
perturbation_seed: prs,
expected_trace_hash: r.trace.canonical_hash(),
expected_delta_hash: r.delta.hash(),
expected_future_hash: r.replay.future_hash,
}
}
/// Build a replay corpus of `n` cases.
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()
}
/// A single replay verification outcome.
#[derive(Clone, Copy, Debug)]
pub struct ReplayDrift {
pub master_seed: u64,
pub trace_ok: bool,
pub delta_ok: bool,
pub future_ok: bool,
}
impl ReplayDrift {
pub fn ok(&self) -> bool {
self.trace_ok && self.delta_ok && self.future_ok
}
}
/// Replay one case and check for drift.
pub fn replay(case: &ReplayCase) -> ReplayDrift {
let (input, ..) = input_for(case.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,
}
}
/// Aggregate replay report.
#[derive(Clone, Debug)]
pub struct ReplayReport {
pub total: usize,
pub deterministic: usize,
pub drift: Vec<u64>,
}
impl ReplayReport {
pub fn passed(&self, minimum: usize) -> bool {
self.drift.is_empty() && self.total >= minimum
}
}
/// Verify the whole corpus replays deterministically.
pub fn verify_corpus(corpus: &[ReplayCase]) -> ReplayReport {
let mut drift = Vec::new();
let mut deterministic = 0;
for case in corpus {
let d = replay(case);
if d.ok() {
deterministic += 1;
} else {
drift.push(case.master_seed);
}
}
ReplayReport {
total: corpus.len(),
deterministic,
drift,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replay_is_deterministic_zero_drift() {
let corpus = build_corpus(80, 0x5EED);
let report = verify_corpus(&corpus);
assert_eq!(report.total, 80);
assert_eq!(report.deterministic, 80);
assert!(report.drift.is_empty());
}
#[test]
fn case_hashes_are_stable() {
let a = build_case(123);
let b = build_case(123);
assert_eq!(a, b);
}
}
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "rune_ir"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
world_model = { path = "../world_model" }
[lib]
path = "src/lib.rs"
+168
View File
@@ -0,0 +1,168 @@
//! `rune_ir` — the rune program model. A rune program is an opaque token
//! stream; no token is ever rejected as the primary safety path. The runtime
//! interprets every stream into a resolution result. Semantics live in the
//! runtimes; this crate only defines structure and stable hashing.
use world_model::{Hasher, ProgramId, Hash, NUM_DOMAINS, LANES};
/// Rune opcodes. Every opcode is total: it always produces a defined effect
/// (possibly a logged fault) and never panics.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum Op {
/// Nonlinear mix of two domains into a destination lane.
Mix,
/// Channel a source through the world coupling matrix into a destination.
Channel,
/// Branch on a domain value; takes one of two avalanche paths.
Branch,
/// Schedule a future effect (creates future dependence).
Schedule,
/// Bidirectionally couple two domains.
Resonate,
/// Read the observed projection into the execution accumulator.
Observe,
/// Fold the accumulator into a destination domain.
Collapse,
/// Nonlinear self-inversion of a destination lane.
Invert,
/// Diffuse a source across several domains.
Diffuse,
/// Clamp/stabilize a destination lane.
Anchor,
/// Move hidden state into observed state (hidden -> observed flow).
Echoback,
/// Imprint observed state into hidden state (observed -> hidden flow).
Imprint,
}
pub const ALL_OPS: [Op; 12] = [
Op::Mix,
Op::Channel,
Op::Branch,
Op::Schedule,
Op::Resonate,
Op::Observe,
Op::Collapse,
Op::Invert,
Op::Diffuse,
Op::Anchor,
Op::Echoback,
Op::Imprint,
];
impl Op {
pub fn from_u8(v: u8) -> Op {
ALL_OPS[(v as usize) % ALL_OPS.len()]
}
pub fn to_u8(self) -> u8 {
ALL_OPS.iter().position(|&o| o == self).unwrap() as u8
}
pub fn name(self) -> &'static str {
match self {
Op::Mix => "mix",
Op::Channel => "channel",
Op::Branch => "branch",
Op::Schedule => "schedule",
Op::Resonate => "resonate",
Op::Observe => "observe",
Op::Collapse => "collapse",
Op::Invert => "invert",
Op::Diffuse => "diffuse",
Op::Anchor => "anchor",
Op::Echoback => "echoback",
Op::Imprint => "imprint",
}
}
}
/// A single rune. `a`/`b` select domains, `c` selects a lane/mode, `imm` is an
/// immediate operand. All fields are interpreted modulo the relevant range so
/// every token is always valid.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub struct RuneToken {
pub op: Op,
pub a: u8,
pub b: u8,
pub c: u8,
pub imm: i64,
}
impl RuneToken {
pub fn src_domain(&self) -> usize {
(self.a as usize) % NUM_DOMAINS
}
pub fn dst_domain(&self) -> usize {
(self.b as usize) % NUM_DOMAINS
}
pub fn lane(&self) -> usize {
(self.c as usize) % LANES
}
/// Secondary lane derived from the high bits of `c`.
pub fn lane2(&self) -> usize {
((self.c as usize) >> 2) % LANES
}
/// Mode selector derived from `c`.
pub fn mode(&self) -> usize {
(self.c as usize) % 4
}
pub fn hash_into(&self, h: &mut Hasher) {
h.write_u8(self.op.to_u8());
h.write_u8(self.a);
h.write_u8(self.b);
h.write_u8(self.c);
h.write_i64(self.imm);
}
}
/// A rune program (per spec).
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct RuneProgram {
pub id: ProgramId,
pub tokens: Vec<RuneToken>,
pub seed: u64,
}
impl RuneProgram {
pub fn content_hash(&self) -> Hash {
let mut h = Hasher::new();
h.write_tag("rune-program");
h.write_u64(self.id.0);
h.write_u64(self.seed);
h.write_usize(self.tokens.len());
for t in &self.tokens {
t.hash_into(&mut h);
}
h.finish()
}
pub fn len(&self) -> usize {
self.tokens.len()
}
pub fn is_empty(&self) -> bool {
self.tokens.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn op_roundtrips() {
for op in ALL_OPS {
assert_eq!(Op::from_u8(op.to_u8()), op);
}
}
#[test]
fn program_hash_is_deterministic() {
let p = RuneProgram {
id: ProgramId(1),
tokens: vec![RuneToken { op: Op::Mix, a: 1, b: 2, c: 3, imm: 4 }],
seed: 9,
};
assert_eq!(p.content_hash(), p.content_hash());
}
}
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "runtime_under_test"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
world_model = { path = "../world_model" }
rune_ir = { path = "../rune_ir" }
trace_model = { path = "../trace_model" }
reference_runtime = { path = "../reference_runtime" }
[lib]
path = "src/lib.rs"
+99
View File
@@ -0,0 +1,99 @@
//! `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.
use reference_runtime::{execute, EngineConfig, ResolutionInput, ResolutionResult, Runtime};
#[derive(Clone, Debug)]
pub struct RuntimeUnderTest {
pub config: EngineConfig,
}
impl Default for RuntimeUnderTest {
fn default() -> Self {
RuntimeUnderTest {
config: EngineConfig::reference(),
}
}
}
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 }
}
}
impl Runtime for RuntimeUnderTest {
fn resolve(&self, input: ResolutionInput) -> ResolutionResult {
execute(&self.config, &input)
}
}
#[cfg(test)]
mod tests {
use super::*;
use reference_runtime::{canonical, execute, ReferenceRuntime};
use rune_ir::{Op, RuneProgram, RuneToken};
use world_model::{standard_executors, ProgramId, Rng, WorldId, WorldSnapshot, NUM_DOMAINS};
fn random_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 {
d.observed[l] = rng.range_i64(-5000, 5000);
}
for l in 0..world_model::HIDDEN_LANES {
d.hidden[l] = rng.range_i64(-5000, 5000);
}
}
for j in 0..NUM_DOMAINS {
for i in 0..NUM_DOMAINS {
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),
a: rng.next_u64() as u8,
b: rng.next_u64() as u8,
c: rng.next_u64() as u8,
imm: rng.range_i64(-100000, 100000),
})
.collect();
ResolutionInput {
world: w,
program: RuneProgram { id: ProgramId(seed), tokens, seed },
contexts: standard_executors(seed, 3),
contract_seed: seed,
perturbation_seed: seed,
}
}
#[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);
}
}
}
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "semantic_mutation"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
world_model = { path = "../world_model" }
rune_ir = { path = "../rune_ir" }
trace_model = { path = "../trace_model" }
reference_runtime = { path = "../reference_runtime" }
[lib]
path = "src/lib.rs"
+278
View File
@@ -0,0 +1,278 @@
//! `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.
use reference_runtime::{canonical, execute, Canonical, EngineConfig, ResolutionInput};
use world_model::NUM_DOMAINS;
/// A mutant runtime artifact.
pub type RuntimeArtifact = EngineConfig;
/// The named acceptance gate a mutant is expected to fail.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DetectionClass {
RuntimeEquivalence,
CausalGate,
TemporalGate,
DomainParticipation,
}
impl DetectionClass {
pub fn name(self) -> &'static str {
match self {
DetectionClass::RuntimeEquivalence => "runtime_equivalence",
DetectionClass::CausalGate => "causal_gate",
DetectionClass::TemporalGate => "temporal_gate",
DetectionClass::DomainParticipation => "domain_participation",
}
}
}
/// The semantic-mutator trait (per spec).
pub trait SemanticMutator {
fn mutate(&self, base: &RuntimeArtifact) -> RuntimeArtifact;
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()
}
fn expected_detection_reason(&self) -> DetectionClass {
self.expected
}
}
/// A concrete mutant.
#[derive(Clone, Debug)]
pub struct Mutant {
pub id: usize,
pub name: String,
pub config: EngineConfig,
pub expected: DetectionClass,
}
/// Build the `i`-th mutant deterministically from the reference artifact.
/// Every mutant differs from the reference in exactly one behavioral knob.
pub fn mutant_for(i: usize) -> Mutant {
let base = EngineConfig::reference();
let mut cfg = base.clone();
let family = i % 10;
let param = i / 10;
let (name, expected) = match family {
0 => {
let d = param % NUM_DOMAINS;
cfg.domain_mask[d] = false;
(format!("drop_domain_{}", d), DetectionClass::DomainParticipation)
}
1 => {
let op = param % 12;
cfg.op_enabled[op] = false;
(format!("disable_op_{}", op), DetectionClass::RuntimeEquivalence)
}
2 => match param % 5 {
0 => {
cfg.use_coupling = !cfg.use_coupling;
("toggle_use_coupling".into(), DetectionClass::RuntimeEquivalence)
}
1 => {
cfg.use_hidden = !cfg.use_hidden;
("toggle_use_hidden".into(), DetectionClass::RuntimeEquivalence)
}
2 => {
cfg.use_executor_salt = !cfg.use_executor_salt;
("toggle_executor_salt".into(), DetectionClass::RuntimeEquivalence)
}
3 => {
cfg.branch_enabled = !cfg.branch_enabled;
("toggle_branch".into(), DetectionClass::RuntimeEquivalence)
}
_ => {
cfg.schedule_enabled = !cfg.schedule_enabled;
("toggle_schedule".into(), DetectionClass::TemporalGate)
}
},
3 => {
cfg.record_causal = false;
("disable_causal_recording".into(), DetectionClass::CausalGate)
}
4 => {
let bit = param % 64;
cfg.c1 ^= 1u64 << bit;
(format!("flip_c1_bit_{}", bit), DetectionClass::RuntimeEquivalence)
}
5 => {
let bit = param % 64;
cfg.c2 ^= 1u64 << bit;
(format!("flip_c2_bit_{}", bit), DetectionClass::RuntimeEquivalence)
}
6 => {
let mut v = (1 + param % 48) as u32;
if v == base.s1 {
v = (v % 48) + 1;
}
cfg.s1 = v;
(format!("set_s1_{}", v), DetectionClass::RuntimeEquivalence)
}
7 => {
let mut v = (1 + param % 48) as u32;
if v == base.s2 {
v = (v % 48) + 1;
}
cfg.s2 = v;
(format!("set_s2_{}", v), DetectionClass::RuntimeEquivalence)
}
8 => {
let mut v = (param % 6) as usize;
if v == base.future_turns {
v = 4;
}
cfg.future_turns = v;
(format!("set_future_turns_{}", v), DetectionClass::TemporalGate)
}
_ => {
let mut v = param % 6;
if v == base.diffuse_span {
v = 5;
}
cfg.diffuse_span = v;
(format!("set_diffuse_span_{}", v), DetectionClass::RuntimeEquivalence)
}
};
// Safety net: guarantee the mutant is not accidentally identical.
if cfg == base {
cfg.use_hidden = !cfg.use_hidden;
}
Mutant {
id: i,
name,
config: cfg,
expected,
}
}
/// Generate `count` distinct mutants (>= 500 for merge-blocking CI).
pub fn generate_mutants(count: usize) -> Vec<Mutant> {
(0..count).map(mutant_for).collect()
}
/// Precompute the reference canonical view for each input.
pub fn reference_canon(inputs: &[ResolutionInput]) -> Vec<Canonical> {
let cfg = EngineConfig::reference();
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);
}
}
None
}
/// Result of running the full mutation suite.
#[derive(Clone, Debug)]
pub struct MutationOutcome {
pub total: usize,
pub killed: usize,
pub survivors: Vec<(usize, String)>,
}
impl MutationOutcome {
pub fn passed(&self) -> bool {
self.survivors.is_empty() && self.total > 0
}
}
/// Run all mutants against the input corpus.
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()));
}
}
MutationOutcome {
total: mutants.len(),
killed,
survivors,
}
}
#[cfg(test)]
mod tests {
use super::*;
use rune_ir::{RuneProgram, RuneToken, ALL_OPS};
use world_model::{standard_executors, ProgramId, Rng, WorldId, WorldSnapshot, 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 {
d.observed[l] = rng.range_i64(-5000, 5000);
}
for l in 0..world_model::HIDDEN_LANES {
d.hidden[l] = rng.range_i64(-5000, 5000);
}
}
for j in 0..NUM_DOMAINS {
for i in 0..NUM_DOMAINS {
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()],
a: ((i * 3) % NUM_DOMAINS) as u8,
b: ((i * 5 + 1) % NUM_DOMAINS) as u8,
c: rng.next_u64() as u8,
imm: rng.range_i64(-100000, 100000),
})
.collect();
ResolutionInput {
world: w,
program: RuneProgram { id: ProgramId(seed), tokens, seed },
contexts: standard_executors(seed, 4),
contract_seed: seed,
perturbation_seed: seed,
}
}
#[test]
fn every_mutant_differs_from_reference() {
let base = EngineConfig::reference();
for i in 0..600 {
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);
}
}
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "trace_model"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
world_model = { path = "../world_model" }
[lib]
path = "src/lib.rs"
+433
View File
@@ -0,0 +1,433 @@
//! `trace_model` — the execution trace and all of its graphs, plus the
//! behavior fingerprint, replay record, fault log, and the metrics the trace
//! gates check (causal rank, causal edges, touched domains, fingerprint
//! collisions, executor divergence).
use world_model::{combine_hashes, Hash, Hasher, NUM_DOMAINS};
pub mod matrix;
pub use matrix::numeric_rank;
/// A graph over domains: per-domain access counts plus cross-domain edges.
/// Used for both the read graph and the write graph.
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct DomainAccessGraph {
pub access_count: [u32; NUM_DOMAINS],
/// `(from_domain, to_domain, weight)` data-movement edges.
pub edges: Vec<(u8, u8, u32)>,
}
impl DomainAccessGraph {
pub fn touched(&self) -> Vec<usize> {
(0..NUM_DOMAINS).filter(|&i| self.access_count[i] > 0).collect()
}
pub fn touched_count(&self) -> usize {
self.access_count.iter().filter(|&&c| c > 0).count()
}
pub fn hash(&self) -> Hash {
let mut h = Hasher::new();
h.write_tag("access-graph");
for &c in &self.access_count {
h.write_u64(c as u64);
}
h.write_usize(self.edges.len());
for &(a, b, w) in &self.edges {
h.write_u8(a);
h.write_u8(b);
h.write_u64(w as u64);
}
h.finish()
}
}
/// A node in the causal graph: a specific domain lane at a specific step.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub struct CausalNode {
pub domain: u8,
pub lane: u8,
pub hidden: bool,
pub step: u32,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct CausalEdge {
pub from: CausalNode,
pub to: CausalNode,
pub weight: i64,
}
/// The causal dependency graph of an execution.
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct CausalGraph {
pub edges: Vec<CausalEdge>,
}
impl CausalGraph {
pub fn edge_count(&self) -> usize {
self.edges.len()
}
/// Domains that participate as either source or sink of a causal edge.
pub fn touched_domains(&self) -> Vec<usize> {
let mut seen = [false; NUM_DOMAINS];
for e in &self.edges {
seen[e.from.domain as usize % NUM_DOMAINS] = true;
seen[e.to.domain as usize % NUM_DOMAINS] = true;
}
(0..NUM_DOMAINS).filter(|&i| seen[i]).collect()
}
pub fn touched_domain_count(&self) -> usize {
self.touched_domains().len()
}
/// Aggregate domain-by-domain influence matrix (weights summed).
pub fn influence_matrix(&self) -> [[f64; NUM_DOMAINS]; NUM_DOMAINS] {
let mut m = [[0.0f64; NUM_DOMAINS]; NUM_DOMAINS];
for e in &self.edges {
let i = e.from.domain as usize % NUM_DOMAINS;
let j = e.to.domain as usize % NUM_DOMAINS;
// Use a bounded, lane-distinguished contribution so distinct
// interactions remain linearly independent rather than collapsing
// into a single dominant magnitude.
let lane_phase = 1.0 + (e.from.lane as f64) + 4.0 * (e.to.lane as f64);
m[i][j] += lane_phase * ((e.weight & 0xffff) as f64 + 1.0);
}
m
}
/// Causal rank: numeric rank of the influence matrix.
pub fn causal_rank(&self) -> usize {
let m = self.influence_matrix();
let rows: Vec<Vec<f64>> = m.iter().map(|r| r.to_vec()).collect();
numeric_rank(&rows)
}
pub fn hash(&self) -> Hash {
let mut h = Hasher::new();
h.write_tag("causal-graph");
h.write_usize(self.edges.len());
for e in &self.edges {
h.write_u8(e.from.domain);
h.write_u8(e.from.lane);
h.write_u8(e.from.hidden as u8);
h.write_u64(e.from.step as u64);
h.write_u8(e.to.domain);
h.write_u8(e.to.lane);
h.write_u8(e.to.hidden as u8);
h.write_u64(e.to.step as u64);
h.write_i64(e.weight);
}
h.finish()
}
}
/// Information flow edges with continuous weights (bits of influence).
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct InformationFlowGraph {
/// `(from_domain, to_domain, influence_bits)`
pub edges: Vec<(u8, u8, u32)>,
}
impl InformationFlowGraph {
pub fn total_bits(&self) -> u64 {
self.edges.iter().map(|&(_, _, b)| b as u64).sum()
}
pub fn hash(&self) -> Hash {
let mut h = Hasher::new();
h.write_tag("info-flow");
h.write_usize(self.edges.len());
for &(a, b, w) in &self.edges {
h.write_u8(a);
h.write_u8(b);
h.write_u64(w as u64);
}
h.finish()
}
}
/// Pairwise divergence between executors (fraction of differing lanes).
#[derive(Clone, PartialEq, Debug, Default)]
pub struct DivergenceGraph {
pub executor_count: usize,
/// Flattened `executor_count x executor_count` divergence fractions.
pub pairwise: Vec<f64>,
}
impl DivergenceGraph {
pub fn get(&self, i: usize, j: usize) -> f64 {
if self.executor_count == 0 {
return 0.0;
}
self.pairwise[i * self.executor_count + j]
}
/// Mean off-diagonal divergence.
pub fn mean_divergence(&self) -> f64 {
let n = self.executor_count;
if n < 2 {
return 0.0;
}
let mut sum = 0.0;
let mut cnt = 0;
for i in 0..n {
for j in 0..n {
if i != j {
sum += self.get(i, j);
cnt += 1;
}
}
}
sum / cnt as f64
}
pub fn hash(&self) -> Hash {
let mut h = Hasher::new();
h.write_tag("divergence");
h.write_usize(self.executor_count);
for &v in &self.pairwise {
h.write_i64((v * 1_000_000.0) as i64);
}
h.finish()
}
}
/// Temporal graph: edges from an execution step to a future turn effect.
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct TemporalGraph {
/// `(step, turn_offset, affected_domain)`
pub edges: Vec<(u32, u8, u8)>,
}
impl TemporalGraph {
pub fn future_reach(&self) -> u8 {
self.edges.iter().map(|&(_, t, _)| t).max().unwrap_or(0)
}
pub fn edge_count(&self) -> usize {
self.edges.len()
}
pub fn hash(&self) -> Hash {
let mut h = Hasher::new();
h.write_tag("temporal");
h.write_usize(self.edges.len());
for &(s, t, d) in &self.edges {
h.write_u64(s as u64);
h.write_u8(t);
h.write_u8(d);
}
h.finish()
}
}
/// Summary of how perturbations affected this execution. Populated by the
/// metamorphic harness; default/empty in a bare resolve.
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct PerturbationResponse {
pub total: usize,
pub altered_trace: usize,
pub altered_delta: usize,
pub altered_future: usize,
pub neutral_unexplained: usize,
}
impl PerturbationResponse {
pub fn hash(&self) -> Hash {
let mut h = Hasher::new();
h.write_tag("perturbation-response");
h.write_usize(self.total);
h.write_usize(self.altered_trace);
h.write_usize(self.altered_delta);
h.write_usize(self.altered_future);
h.write_usize(self.neutral_unexplained);
h.finish()
}
}
/// Behavior fingerprint: a stable hash plus a feature vector used by the
/// collapse analysis and behavior clustering.
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct BehaviorFingerprint {
pub hash: Hash,
pub features: Vec<i64>,
}
impl BehaviorFingerprint {
pub fn from_features(features: Vec<i64>) -> Self {
let mut h = Hasher::new();
h.write_tag("behavior");
h.write_usize(features.len());
for &f in &features {
h.write_i64(f);
}
BehaviorFingerprint {
hash: h.finish(),
features,
}
}
}
/// Faults are always logged, never panicked. Their presence is normal.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum FaultCode {
GuardedDivByZero,
Saturated,
OverflowWrapped,
EmptyAccumulator,
UnreachableBranch,
NoEffectToken,
}
impl FaultCode {
pub fn name(self) -> &'static str {
match self {
FaultCode::GuardedDivByZero => "guarded_div_by_zero",
FaultCode::Saturated => "saturated",
FaultCode::OverflowWrapped => "overflow_wrapped",
FaultCode::EmptyAccumulator => "empty_accumulator",
FaultCode::UnreachableBranch => "unreachable_branch",
FaultCode::NoEffectToken => "no_effect_token",
}
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Fault {
pub code: FaultCode,
pub step: u32,
pub detail_code: i64,
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct FaultLog {
pub faults: Vec<Fault>,
}
impl FaultLog {
pub fn push(&mut self, code: FaultCode, step: u32, detail_code: i64) {
self.faults.push(Fault {
code,
step,
detail_code,
});
}
pub fn hash(&self) -> Hash {
let mut h = Hasher::new();
h.write_tag("fault-log");
h.write_usize(self.faults.len());
for f in &self.faults {
h.write_u8(f.code as u8);
h.write_u64(f.step as u64);
h.write_i64(f.detail_code);
}
h.finish()
}
}
/// Replay record: seeds plus the three canonical hashes.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct ReplayRecord {
pub world_seed: u64,
pub program_seed: u64,
pub contract_seed: u64,
pub perturbation_seed: u64,
pub trace_hash: Hash,
pub delta_hash: Hash,
pub future_hash: Hash,
}
impl ReplayRecord {
pub fn hash(&self) -> Hash {
let mut h = Hasher::new();
h.write_tag("replay-record");
h.write_u64(self.world_seed);
h.write_u64(self.program_seed);
h.write_u64(self.contract_seed);
h.write_u64(self.perturbation_seed);
h.write_u64(self.trace_hash.0);
h.write_u64(self.delta_hash.0);
h.write_u64(self.future_hash.0);
h.finish()
}
}
/// The full execution trace (per spec).
#[derive(Clone, PartialEq, Debug)]
pub struct ExecutionTrace {
pub read_graph: DomainAccessGraph,
pub write_graph: DomainAccessGraph,
pub causal_graph: CausalGraph,
pub information_flow: InformationFlowGraph,
pub executor_divergence: DivergenceGraph,
pub temporal_graph: TemporalGraph,
pub perturbation_response: PerturbationResponse,
pub behavior_fingerprint: BehaviorFingerprint,
}
impl ExecutionTrace {
pub fn causal_rank(&self) -> usize {
self.causal_graph.causal_rank()
}
pub fn causal_edge_count(&self) -> usize {
self.causal_graph.edge_count()
}
/// Domains touched = union of read, write and causal participation.
pub fn touched_domain_count(&self) -> usize {
let mut seen = [false; NUM_DOMAINS];
for i in self.read_graph.touched() {
seen[i] = true;
}
for i in self.write_graph.touched() {
seen[i] = true;
}
for i in self.causal_graph.touched_domains() {
seen[i] = true;
}
seen.iter().filter(|&&b| b).count()
}
pub fn context_divergence(&self) -> f64 {
self.executor_divergence.mean_divergence()
}
/// Canonical hash over the whole trace (used by replay & equivalence).
pub fn canonical_hash(&self) -> Hash {
combine_hashes(
"execution-trace",
&[
self.read_graph.hash(),
self.write_graph.hash(),
self.causal_graph.hash(),
self.information_flow.hash(),
self.executor_divergence.hash(),
self.temporal_graph.hash(),
self.perturbation_response.hash(),
self.behavior_fingerprint.hash,
],
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rank_of_identity_is_full() {
let id: Vec<Vec<f64>> = (0..5)
.map(|i| (0..5).map(|j| if i == j { 1.0 } else { 0.0 }).collect())
.collect();
assert_eq!(numeric_rank(&id), 5);
}
#[test]
fn rank_of_zero_is_zero() {
let z: Vec<Vec<f64>> = vec![vec![0.0; 4]; 4];
assert_eq!(numeric_rank(&z), 0);
}
#[test]
fn rank_of_rank_one_is_one() {
// every row a multiple of [1,2,3]
let m: Vec<Vec<f64>> = (1..=4).map(|k| vec![k as f64, 2.0 * k as f64, 3.0 * k as f64]).collect();
assert_eq!(numeric_rank(&m), 1);
}
}
+82
View File
@@ -0,0 +1,82 @@
//! Small numeric linear-algebra helpers used by the trace and collapse gates.
/// Numeric rank of a matrix via Gaussian elimination with partial pivoting.
/// Tolerance scales with the matrix magnitude so it is robust to the large
/// integer-derived weights the causal graph produces.
pub fn numeric_rank(rows_in: &[Vec<f64>]) -> usize {
if rows_in.is_empty() {
return 0;
}
let mut rows: Vec<Vec<f64>> = rows_in.to_vec();
let nrows = rows.len();
let ncols = rows[0].len();
let max_abs = rows
.iter()
.flat_map(|r| r.iter())
.fold(0.0f64, |m, &v| m.max(v.abs()));
if max_abs == 0.0 {
return 0;
}
let tol = 1e-9 * max_abs * (nrows.max(ncols) as f64);
let mut rank = 0;
let mut pivot_col = 0;
while rank < nrows && pivot_col < ncols {
// Find pivot row with the largest magnitude in pivot_col.
let mut best = rank;
let mut best_val = rows[rank][pivot_col].abs();
for r in (rank + 1)..nrows {
let v = rows[r][pivot_col].abs();
if v > best_val {
best_val = v;
best = r;
}
}
if best_val <= tol {
pivot_col += 1;
continue;
}
rows.swap(rank, best);
let pivot = rows[rank][pivot_col];
for r in 0..nrows {
if r != rank {
let factor = rows[r][pivot_col] / pivot;
if factor != 0.0 {
for c in pivot_col..ncols {
rows[r][c] -= factor * rows[rank][c];
}
}
}
}
rank += 1;
pivot_col += 1;
}
rank
}
/// Pearson correlation between two equal-length series. Returns 0 if either is
/// constant.
pub fn correlation(xs: &[f64], ys: &[f64]) -> f64 {
let n = xs.len().min(ys.len());
if n == 0 {
return 0.0;
}
let nf = n as f64;
let mx = xs[..n].iter().sum::<f64>() / nf;
let my = ys[..n].iter().sum::<f64>() / nf;
let mut cov = 0.0;
let mut vx = 0.0;
let mut vy = 0.0;
for i in 0..n {
let dx = xs[i] - mx;
let dy = ys[i] - my;
cov += dx * dy;
vx += dx * dx;
vy += dy * dy;
}
if vx <= 1e-12 || vy <= 1e-12 {
return 0.0;
}
cov / (vx.sqrt() * vy.sqrt())
}
+8
View File
@@ -0,0 +1,8 @@
[package]
name = "world_model"
version.workspace = true
edition.workspace = true
license.workspace = true
[lib]
path = "src/lib.rs"
+107
View File
@@ -0,0 +1,107 @@
//! Execution contexts (executors). Different executors interpret the same
//! rune stream differently, which produces executor divergence. The spec
//! requires at least 3 distinct executors per case.
use crate::primitives::{Hasher, Rng};
/// Distinct executor interpretation styles. Each one mixes rune operands
/// differently, so the same program produces different traces under each.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ExecutorKind {
/// Aggressive forward mixing; favors multiplicative coupling.
Surge,
/// Lateral mixing; favors xor/rotate coupling across domains.
Weave,
/// Conservative mixing; clamps and favors additive coupling.
Anchor,
/// Phase-shifting; reorders operand roles.
Phase,
}
pub const ALL_EXECUTOR_KINDS: [ExecutorKind; 4] = [
ExecutorKind::Surge,
ExecutorKind::Weave,
ExecutorKind::Anchor,
ExecutorKind::Phase,
];
impl ExecutorKind {
pub fn name(self) -> &'static str {
match self {
ExecutorKind::Surge => "surge",
ExecutorKind::Weave => "weave",
ExecutorKind::Anchor => "anchor",
ExecutorKind::Phase => "phase",
}
}
pub fn index(self) -> usize {
match self {
ExecutorKind::Surge => 0,
ExecutorKind::Weave => 1,
ExecutorKind::Anchor => 2,
ExecutorKind::Phase => 3,
}
}
pub fn salt(self) -> u64 {
match self {
ExecutorKind::Surge => 0x51_75_72_67_65_00_00_01,
ExecutorKind::Weave => 0x57_65_61_76_65_00_00_02,
ExecutorKind::Anchor => 0x41_6e_63_68_72_00_00_03,
ExecutorKind::Phase => 0x50_68_61_73_65_00_00_04,
}
}
}
/// Parameters that modulate rune interpretation for one executor.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct ExecutorProfile {
pub kind: ExecutorKind,
pub bias: i64,
pub rotate: u32,
pub branch_threshold: i64,
}
/// One executor.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct ExecutionContext {
pub executor_id: u32,
pub profile: ExecutorProfile,
}
impl ExecutionContext {
pub fn hash_into(&self, h: &mut Hasher) {
h.write_tag("executor");
h.write_u64(self.executor_id as u64);
h.write_u8(self.profile.kind.index() as u8);
h.write_i64(self.profile.bias);
h.write_u64(self.profile.rotate as u64);
h.write_i64(self.profile.branch_threshold);
}
pub fn salt(&self) -> u64 {
self.profile.kind.salt() ^ (self.executor_id as u64).wrapping_mul(0x9e3779b97f4a7c15)
}
}
/// Build `count` distinct executors deterministically from a seed. Always
/// produces at least 3 with distinct kinds.
pub fn standard_executors(seed: u64, count: usize) -> Vec<ExecutionContext> {
let count = count.max(3);
let mut rng = Rng::derive(seed, "executors");
let mut out = Vec::with_capacity(count);
for i in 0..count {
let kind = ALL_EXECUTOR_KINDS[i % ALL_EXECUTOR_KINDS.len()];
out.push(ExecutionContext {
executor_id: i as u32,
profile: ExecutorProfile {
kind,
bias: rng.range_i64(-7, 7),
rotate: (1 + rng.below(31)) as u32,
branch_threshold: rng.range_i64(-1000, 1000),
},
});
}
out
}
+227
View File
@@ -0,0 +1,227 @@
//! World domains. The spec mandates at least 8 *independent* domains, each
//! exposing read/write surfaces, perturbation axes, and a fingerprint.
//!
//! Each domain holds `LANES` observed values and `HIDDEN_LANES` hidden values.
//! Domains differ from one another by per-kind mixing constants and by the
//! perturbation axes they expose, which is what makes them genuinely
//! independent rather than eight copies of one decorative axis.
use crate::perturb::{
HiddenFlipAxis, LaneBumpAxis, LaneScaleAxis, LaneSwapAxis, PerturbationAxis,
};
use crate::primitives::{DomainId, Hash, Hasher, HIDDEN_LANES, LANES, NUM_DOMAINS};
/// The eight independent domains.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
pub enum DomainKind {
Aether,
Matter,
Flux,
Mind,
Entropy,
Resonance,
Boundary,
Echo,
}
pub const ALL_DOMAIN_KINDS: [DomainKind; NUM_DOMAINS] = [
DomainKind::Aether,
DomainKind::Matter,
DomainKind::Flux,
DomainKind::Mind,
DomainKind::Entropy,
DomainKind::Resonance,
DomainKind::Boundary,
DomainKind::Echo,
];
impl DomainKind {
pub fn index(self) -> usize {
match self {
DomainKind::Aether => 0,
DomainKind::Matter => 1,
DomainKind::Flux => 2,
DomainKind::Mind => 3,
DomainKind::Entropy => 4,
DomainKind::Resonance => 5,
DomainKind::Boundary => 6,
DomainKind::Echo => 7,
}
}
pub fn from_index(i: usize) -> DomainKind {
ALL_DOMAIN_KINDS[i % NUM_DOMAINS]
}
pub fn name(self) -> &'static str {
match self {
DomainKind::Aether => "aether",
DomainKind::Matter => "matter",
DomainKind::Flux => "flux",
DomainKind::Mind => "mind",
DomainKind::Entropy => "entropy",
DomainKind::Resonance => "resonance",
DomainKind::Boundary => "boundary",
DomainKind::Echo => "echo",
}
}
/// Distinct odd mixing constant per kind. These drive the nonlinear
/// avalanche in the runtime and guarantee each domain transforms state
/// differently from every other domain.
pub fn mix_const(self) -> u64 {
match self {
DomainKind::Aether => 0x9e3779b97f4a7c15,
DomainKind::Matter => 0xc2b2ae3d27d4eb4f,
DomainKind::Flux => 0x165667b19e3779f9,
DomainKind::Mind => 0x27d4eb2f165667c5,
DomainKind::Entropy => 0x2545f4914f6cdd1d,
DomainKind::Resonance => 0x85ebca77c2b2ae63,
DomainKind::Boundary => 0xff51afd7ed558ccd,
DomainKind::Echo => 0xc4ceb9fe1a85ec53,
}
}
/// Per-kind rotation amount (kept in 1..63).
pub fn rotate(self) -> u32 {
7 + (self.index() as u32) * 7 % 53 + 1
}
pub fn id(self) -> DomainId {
DomainId(self.index() as u8)
}
}
/// Per-domain state: observed and hidden lanes.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct DomainState {
pub kind: DomainKind,
pub observed: [i64; LANES],
pub hidden: [i64; HIDDEN_LANES],
}
impl DomainState {
pub fn new(kind: DomainKind) -> Self {
DomainState {
kind,
observed: [0; LANES],
hidden: [0; HIDDEN_LANES],
}
}
pub fn id(&self) -> DomainId {
self.kind.id()
}
/// Hash mixing kind + all state. Used inside replay/behavior hashes.
pub fn hash_into(&self, h: &mut Hasher) {
h.write_tag("domain");
h.write_u8(self.kind.index() as u8);
for &v in &self.observed {
h.write_i64(v);
}
for &v in &self.hidden {
h.write_i64(v);
}
}
}
/// What a domain currently exposes to be read.
#[derive(Clone, Debug)]
pub struct ReadSurface {
pub domain: DomainId,
pub observed: Vec<i64>,
pub hidden: Vec<i64>,
}
/// What a domain currently allows to be written.
#[derive(Clone, Debug)]
pub struct WriteSurface {
pub domain: DomainId,
pub writable_observed: Vec<usize>,
pub writable_hidden: Vec<usize>,
}
/// A structural+state fingerprint of a domain. Different kinds must produce
/// different fingerprints (checked by the generators as "nonuniform domain
/// fingerprints").
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct DomainFingerprint {
pub domain: DomainId,
pub hash: Hash,
}
/// The trait every domain exposes (per spec).
pub trait WorldDomain {
fn domain_id(&self) -> DomainId;
fn read_surface(&self) -> ReadSurface;
fn write_surface(&self) -> WriteSurface;
fn perturbation_axes(&self) -> Vec<Box<dyn PerturbationAxis>>;
fn fingerprint(&self) -> DomainFingerprint;
}
impl WorldDomain for DomainState {
fn domain_id(&self) -> DomainId {
self.kind.id()
}
fn read_surface(&self) -> ReadSurface {
ReadSurface {
domain: self.kind.id(),
observed: self.observed.to_vec(),
hidden: self.hidden.to_vec(),
}
}
fn write_surface(&self) -> WriteSurface {
WriteSurface {
domain: self.kind.id(),
writable_observed: (0..LANES).collect(),
writable_hidden: (0..HIDDEN_LANES).collect(),
}
}
fn perturbation_axes(&self) -> Vec<Box<dyn PerturbationAxis>> {
let d = self.kind.id();
// Each domain exposes several distinct axes derived from its surface.
// Across 8 domains this is well above the required 10 axes per case.
let mut axes: Vec<Box<dyn PerturbationAxis>> = Vec::new();
axes.push(Box::new(LaneBumpAxis {
domain: d,
lane: 0,
delta: 1,
}));
axes.push(Box::new(LaneBumpAxis {
domain: d,
lane: (self.kind.index() % LANES),
delta: -3,
}));
axes.push(Box::new(LaneScaleAxis {
domain: d,
lane: (self.kind.index() + 1) % LANES,
factor: 3,
}));
axes.push(Box::new(HiddenFlipAxis {
domain: d,
lane: self.kind.index() % HIDDEN_LANES,
}));
axes.push(Box::new(LaneSwapAxis {
domain: d,
lane_a: 0,
lane_b: (self.kind.index() % (LANES - 1)) + 1,
}));
axes
}
fn fingerprint(&self) -> DomainFingerprint {
let mut h = Hasher::new();
h.write_tag("domain-fingerprint");
h.write_u8(self.kind.index() as u8);
h.write_u64(self.kind.mix_const());
self.hash_into(&mut h);
DomainFingerprint {
domain: self.kind.id(),
hash: h.finish(),
}
}
}
+74
View File
@@ -0,0 +1,74 @@
//! `world_model` — the foundational crate. Defines world state, the eight
//! independent domains, perturbation axes, execution contexts, world deltas,
//! and the deterministic primitives (ids, stable hashing, RNG) used by every
//! other crate.
pub mod context;
pub mod domain;
pub mod perturb;
pub mod primitives;
pub mod world;
pub use context::{
standard_executors, ExecutionContext, ExecutorKind, ExecutorProfile, ALL_EXECUTOR_KINDS,
};
pub use domain::{
DomainFingerprint, DomainKind, DomainState, ReadSurface, WorldDomain, WriteSurface,
ALL_DOMAIN_KINDS,
};
pub use perturb::{
HiddenFlipAxis, LaneBumpAxis, LaneScaleAxis, LaneSwapAxis, PerturbationAxis,
TraceDifferenceExpectation,
};
pub use primitives::{
combine_hashes, hash_i64_slice, ContractId, DomainId, Hash, Hasher, ProgramId, Rng, WorldId,
HIDDEN_LANES, LANES, NUM_DOMAINS,
};
pub use world::{
CausalState, DomainDelta, ExecutionState, ObservationState, ScheduledEffect, TimeState,
WorldDelta, WorldSnapshot, REGS,
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rng_is_deterministic() {
let mut a = Rng::new(42);
let mut b = Rng::new(42);
for _ in 0..1000 {
assert_eq!(a.next_u64(), b.next_u64());
}
}
#[test]
fn hashing_is_stable_and_sensitive() {
let h1 = hash_i64_slice("t", &[1, 2, 3]);
let h2 = hash_i64_slice("t", &[1, 2, 3]);
let h3 = hash_i64_slice("t", &[1, 2, 4]);
assert_eq!(h1, h2);
assert_ne!(h1, h3);
}
#[test]
fn domain_fingerprints_are_nonuniform() {
let w = WorldSnapshot::blank(WorldId(1), 7);
let mut prints: Vec<_> = w.domains.iter().map(|d| d.fingerprint().hash).collect();
prints.sort();
prints.dedup();
// even with identical (zero) state, distinct kinds give distinct prints
assert_eq!(prints.len(), NUM_DOMAINS);
}
#[test]
fn perturbation_changes_world() {
let mut w = WorldSnapshot::blank(WorldId(1), 7);
w.domains[0].observed[0] = 100;
let before = w.content_hash();
let axis = LaneBumpAxis { domain: DomainId(0), lane: 0, delta: 5 };
let w2 = axis.apply(&w);
assert_ne!(before, w2.content_hash());
assert_eq!(w2.domains[0].observed[0], 105);
}
}
+152
View File
@@ -0,0 +1,152 @@
//! Perturbation axes. Perturbations are derived from domain surfaces (not a
//! fixed global list) and each declares what trace/world difference it is
//! expected to cause. The metamorphic gates check that these expectations
//! actually hold across the corpus.
use crate::primitives::DomainId;
use crate::world::WorldSnapshot;
/// What difference a perturbation is expected to produce.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TraceDifferenceExpectation {
pub expect_trace_change: bool,
pub expect_delta_change: bool,
pub expect_future_change: bool,
/// If the perturbation is allowed to be observationally neutral, this
/// explains why (spec allows <=5% neutral *with explanation*).
pub neutral_explanation: Option<&'static str>,
}
impl TraceDifferenceExpectation {
pub fn active() -> Self {
TraceDifferenceExpectation {
expect_trace_change: true,
expect_delta_change: true,
expect_future_change: true,
neutral_explanation: None,
}
}
}
/// A perturbation axis derived from a domain surface.
pub trait PerturbationAxis {
fn name(&self) -> String;
fn target(&self) -> DomainId;
fn apply(&self, world: &WorldSnapshot) -> WorldSnapshot;
fn expected_trace_difference(&self) -> TraceDifferenceExpectation;
}
fn domain_mut<'a>(world: &'a mut WorldSnapshot, d: DomainId) -> &'a mut crate::domain::DomainState {
&mut world.domains[d.0 as usize]
}
/// Add a delta to an observed lane.
pub struct LaneBumpAxis {
pub domain: DomainId,
pub lane: usize,
pub delta: i64,
}
impl PerturbationAxis for LaneBumpAxis {
fn name(&self) -> String {
format!("bump(d{},l{},{:+})", self.domain.0, self.lane, self.delta)
}
fn target(&self) -> DomainId {
self.domain
}
fn apply(&self, world: &WorldSnapshot) -> WorldSnapshot {
let mut w = world.clone();
let ds = domain_mut(&mut w, self.domain);
ds.observed[self.lane] = ds.observed[self.lane].wrapping_add(self.delta);
w.mark_perturbed();
w
}
fn expected_trace_difference(&self) -> TraceDifferenceExpectation {
TraceDifferenceExpectation::active()
}
}
/// Multiply an observed lane by a factor.
pub struct LaneScaleAxis {
pub domain: DomainId,
pub lane: usize,
pub factor: i64,
}
impl PerturbationAxis for LaneScaleAxis {
fn name(&self) -> String {
format!("scale(d{},l{},x{})", self.domain.0, self.lane, self.factor)
}
fn target(&self) -> DomainId {
self.domain
}
fn apply(&self, world: &WorldSnapshot) -> WorldSnapshot {
let mut w = world.clone();
let ds = domain_mut(&mut w, self.domain);
ds.observed[self.lane] = ds.observed[self.lane].wrapping_mul(self.factor).wrapping_add(1);
w.mark_perturbed();
w
}
fn expected_trace_difference(&self) -> TraceDifferenceExpectation {
TraceDifferenceExpectation::active()
}
}
/// Flip the sign / toggle a hidden lane. Hidden changes may be observationally
/// neutral on the immediate delta but should still influence future turns.
pub struct HiddenFlipAxis {
pub domain: DomainId,
pub lane: usize,
}
impl PerturbationAxis for HiddenFlipAxis {
fn name(&self) -> String {
format!("hidden(d{},l{})", self.domain.0, self.lane)
}
fn target(&self) -> DomainId {
self.domain
}
fn apply(&self, world: &WorldSnapshot) -> WorldSnapshot {
let mut w = world.clone();
let ds = domain_mut(&mut w, self.domain);
ds.hidden[self.lane] = !ds.hidden[self.lane].wrapping_add(0x5bd1e995);
w.mark_perturbed();
w
}
fn expected_trace_difference(&self) -> TraceDifferenceExpectation {
// Hidden state feeds the runtime, so we still expect change; but it is
// permitted to be observationally neutral on the immediate delta.
TraceDifferenceExpectation {
expect_trace_change: true,
expect_delta_change: false,
expect_future_change: true,
neutral_explanation: Some("hidden lane influences future, not immediate observed delta"),
}
}
}
/// Swap two observed lanes.
pub struct LaneSwapAxis {
pub domain: DomainId,
pub lane_a: usize,
pub lane_b: usize,
}
impl PerturbationAxis for LaneSwapAxis {
fn name(&self) -> String {
format!("swap(d{},l{}<->l{})", self.domain.0, self.lane_a, self.lane_b)
}
fn target(&self) -> DomainId {
self.domain
}
fn apply(&self, world: &WorldSnapshot) -> WorldSnapshot {
let mut w = world.clone();
let ds = domain_mut(&mut w, self.domain);
ds.observed.swap(self.lane_a, self.lane_b);
w.mark_perturbed();
w
}
fn expected_trace_difference(&self) -> TraceDifferenceExpectation {
TraceDifferenceExpectation::active()
}
}
+214
View File
@@ -0,0 +1,214 @@
//! Deterministic primitives shared across the whole framework: stable ids,
//! a stable content hash, and a deterministic RNG. Everything here is
//! reproducible from a seed so that replay is bit-exact.
use std::fmt;
/// Number of independent world domains. The spec mandates >= 8.
pub const NUM_DOMAINS: usize = 8;
/// Observed value lanes per domain.
pub const LANES: usize = 4;
/// Hidden (unobserved) value lanes per domain. These create the
/// hidden/observed state divergence the spec requires.
pub const HIDDEN_LANES: usize = 2;
macro_rules! id_type {
($name:ident, $inner:ty) => {
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct $name(pub $inner);
impl fmt::Debug for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}({})", stringify!($name), self.0)
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
};
}
id_type!(WorldId, u64);
id_type!(ProgramId, u64);
id_type!(ContractId, u64);
/// Identifies one of the [`NUM_DOMAINS`] domains.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DomainId(pub u8);
impl fmt::Debug for DomainId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "DomainId({})", self.0)
}
}
/// A stable 64-bit content hash. Used for canonical comparison, replay
/// hashes, and behavior fingerprints. Implemented with FNV-1a so the value
/// is identical across machines and runs.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Hash(pub u64);
impl fmt::Debug for Hash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Hash({:016x})", self.0)
}
}
impl fmt::Display for Hash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:016x}", self.0)
}
}
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
/// Streaming stable hasher (FNV-1a, 64 bit).
#[derive(Clone, Copy)]
pub struct Hasher {
state: u64,
}
impl Default for Hasher {
fn default() -> Self {
Hasher { state: FNV_OFFSET }
}
}
impl Hasher {
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn write_u8(&mut self, b: u8) {
self.state ^= b as u64;
self.state = self.state.wrapping_mul(FNV_PRIME);
}
#[inline]
pub fn write_u64(&mut self, v: u64) {
for i in 0..8 {
self.write_u8(((v >> (i * 8)) & 0xff) as u8);
}
}
#[inline]
pub fn write_i64(&mut self, v: i64) {
self.write_u64(v as u64);
}
#[inline]
pub fn write_usize(&mut self, v: usize) {
self.write_u64(v as u64);
}
#[inline]
pub fn write_bytes(&mut self, bytes: &[u8]) {
for &b in bytes {
self.write_u8(b);
}
}
/// Mix in a label so structurally different streams that happen to share
/// numbers do not collide.
#[inline]
pub fn write_tag(&mut self, tag: &str) {
self.write_bytes(tag.as_bytes());
self.write_u8(0xff);
}
#[inline]
pub fn finish(&self) -> Hash {
Hash(self.state)
}
}
/// Hash a slice of i64 with a tag.
pub fn hash_i64_slice(tag: &str, vals: &[i64]) -> Hash {
let mut h = Hasher::new();
h.write_tag(tag);
h.write_usize(vals.len());
for &v in vals {
h.write_i64(v);
}
h.finish()
}
/// Combine several hashes into one (order sensitive).
pub fn combine_hashes(tag: &str, hashes: &[Hash]) -> Hash {
let mut h = Hasher::new();
h.write_tag(tag);
for hh in hashes {
h.write_u64(hh.0);
}
h.finish()
}
/// Deterministic SplitMix64 RNG. Fully reproducible from a seed.
#[derive(Clone, Copy, Debug)]
pub struct Rng {
state: u64,
}
impl Rng {
pub fn new(seed: u64) -> Self {
// Avoid the trivial all-zero state.
Rng {
state: seed ^ 0x9e3779b97f4a7c15,
}
}
/// Derive a sub-stream from a seed and a label, so independent concerns
/// never accidentally share a stream.
pub fn derive(seed: u64, tag: &str) -> Self {
let mut h = Hasher::new();
h.write_tag(tag);
h.write_u64(seed);
Rng::new(h.finish().0)
}
#[inline]
pub fn next_u64(&mut self) -> u64 {
self.state = self.state.wrapping_add(0x9e3779b97f4a7c15);
let mut z = self.state;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
z ^ (z >> 31)
}
#[inline]
pub fn next_i64(&mut self) -> i64 {
self.next_u64() as i64
}
/// Uniform-ish integer in `[0, n)`.
#[inline]
pub fn below(&mut self, n: usize) -> usize {
if n == 0 {
return 0;
}
(self.next_u64() % (n as u64)) as usize
}
/// Integer in `[lo, hi]` inclusive.
#[inline]
pub fn range_i64(&mut self, lo: i64, hi: i64) -> i64 {
if hi <= lo {
return lo;
}
let span = (hi - lo) as u64 + 1;
lo + (self.next_u64() % span) as i64
}
#[inline]
pub fn chance(&mut self, p: f64) -> bool {
(self.next_u64() as f64 / u64::MAX as f64) < p
}
#[inline]
pub fn next_bool(&mut self) -> bool {
self.next_u64() & 1 == 1
}
}
+265
View File
@@ -0,0 +1,265 @@
//! The world snapshot and its constituent state machines.
use crate::domain::{DomainState, ALL_DOMAIN_KINDS};
use crate::primitives::{
DomainId, Hash, Hasher, Rng, HIDDEN_LANES, LANES, NUM_DOMAINS,
};
/// Number of world-level execution accumulator registers.
pub const REGS: usize = 4;
/// Cross-domain coupling. A dense `NUM_DOMAINS x NUM_DOMAINS` weight matrix
/// that governs how a change in one domain propagates into others when turns
/// advance. Generated per world; high rank by construction.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct CausalState {
pub coupling: [[i64; NUM_DOMAINS]; NUM_DOMAINS],
}
impl CausalState {
pub fn hash_into(&self, h: &mut Hasher) {
h.write_tag("causal");
for row in &self.coupling {
for &v in row {
h.write_i64(v);
}
}
}
}
/// Which lanes are observable, plus a deterministic observation-noise seed.
/// Drives the divergence between hidden ground truth and observed projection.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct ObservationState {
pub visible: [[bool; LANES]; NUM_DOMAINS],
pub noise_seed: u64,
}
impl ObservationState {
pub fn hash_into(&self, h: &mut Hasher) {
h.write_tag("observation");
for row in &self.visible {
for &b in row {
h.write_u8(b as u8);
}
}
h.write_u64(self.noise_seed);
}
}
/// World-level execution accumulators carried across runes.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct ExecutionState {
pub accumulator: [i64; REGS],
}
impl ExecutionState {
pub fn hash_into(&self, h: &mut Hasher) {
h.write_tag("execstate");
for &v in &self.accumulator {
h.write_i64(v);
}
}
}
/// A future effect scheduled by execution; resolved when turns advance.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct ScheduledEffect {
pub turn_offset: u8,
pub domain: DomainId,
pub lane: usize,
pub hidden: bool,
pub value: i64,
}
/// Temporal state: pending scheduled effects create genuine future dependence.
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct TimeState {
pub pending: Vec<ScheduledEffect>,
}
impl TimeState {
pub fn hash_into(&self, h: &mut Hasher) {
h.write_tag("time");
h.write_usize(self.pending.len());
for e in &self.pending {
h.write_u8(e.turn_offset);
h.write_u8(e.domain.0);
h.write_usize(e.lane);
h.write_u8(e.hidden as u8);
h.write_i64(e.value);
}
}
}
/// The full world snapshot (per spec).
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct WorldSnapshot {
pub id: crate::primitives::WorldId,
pub turn: u64,
pub domains: Vec<DomainState>,
pub causal_state: CausalState,
pub observation_state: ObservationState,
pub execution_state: ExecutionState,
pub time_state: TimeState,
pub seed: u64,
/// Provenance counter for perturbations; never consumed by the runtime.
pub perturb_nonce: u64,
}
impl WorldSnapshot {
/// An all-zero baseline world (the generators fill it with real state).
pub fn blank(id: crate::primitives::WorldId, seed: u64) -> Self {
let domains = ALL_DOMAIN_KINDS.iter().map(|&k| DomainState::new(k)).collect();
WorldSnapshot {
id,
turn: 0,
domains,
causal_state: CausalState {
coupling: [[0; NUM_DOMAINS]; NUM_DOMAINS],
},
observation_state: ObservationState {
visible: [[true; LANES]; NUM_DOMAINS],
noise_seed: seed ^ 0xa5a5a5a5,
},
execution_state: ExecutionState {
accumulator: [0; REGS],
},
time_state: TimeState::default(),
seed,
perturb_nonce: 0,
}
}
pub fn mark_perturbed(&mut self) {
self.perturb_nonce = self.perturb_nonce.wrapping_add(1);
}
pub fn domain(&self, d: DomainId) -> &DomainState {
&self.domains[d.0 as usize]
}
pub fn domain_mut(&mut self, d: DomainId) -> &mut DomainState {
&mut self.domains[d.0 as usize]
}
/// Observed projection: only visible observed lanes, with deterministic
/// observation noise. Hidden lanes are excluded entirely. This is what an
/// outside observer can measure, and differs from ground truth.
pub fn observed_projection(&self) -> Vec<i64> {
let mut rng = Rng::new(self.observation_state.noise_seed ^ self.turn);
let mut out = Vec::with_capacity(NUM_DOMAINS * LANES);
for (di, d) in self.domains.iter().enumerate() {
for lane in 0..LANES {
if self.observation_state.visible[di][lane] {
let noise = rng.range_i64(-1, 1);
out.push(d.observed[lane].wrapping_add(noise));
} else {
out.push(0);
}
}
}
out
}
/// Ground-truth state vector (observed + hidden), no noise. Used by the
/// runtime and by canonical hashing.
pub fn ground_truth(&self) -> Vec<i64> {
let mut out = Vec::with_capacity(NUM_DOMAINS * (LANES + HIDDEN_LANES));
for d in &self.domains {
out.extend_from_slice(&d.observed);
out.extend_from_slice(&d.hidden);
}
out
}
pub fn content_hash(&self) -> Hash {
let mut h = Hasher::new();
h.write_tag("world");
h.write_u64(self.id.0);
h.write_u64(self.turn);
h.write_u64(self.seed);
for d in &self.domains {
d.hash_into(&mut h);
}
self.causal_state.hash_into(&mut h);
self.observation_state.hash_into(&mut h);
self.execution_state.hash_into(&mut h);
self.time_state.hash_into(&mut h);
h.finish()
}
}
/// Difference of a single domain (after - before, wrapping).
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct DomainDelta {
pub domain: DomainId,
pub observed: [i64; LANES],
pub hidden: [i64; HIDDEN_LANES],
}
impl DomainDelta {
pub fn is_zero(&self) -> bool {
self.observed.iter().all(|&v| v == 0) && self.hidden.iter().all(|&v| v == 0)
}
}
/// The change produced by an execution.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct WorldDelta {
pub domain_deltas: Vec<DomainDelta>,
pub turn_advance: u64,
}
impl WorldDelta {
/// Compute `after - before`.
pub fn between(before: &WorldSnapshot, after: &WorldSnapshot) -> WorldDelta {
let mut deltas = Vec::with_capacity(NUM_DOMAINS);
for i in 0..NUM_DOMAINS {
let b = &before.domains[i];
let a = &after.domains[i];
let mut observed = [0i64; LANES];
let mut hidden = [0i64; HIDDEN_LANES];
for l in 0..LANES {
observed[l] = a.observed[l].wrapping_sub(b.observed[l]);
}
for l in 0..HIDDEN_LANES {
hidden[l] = a.hidden[l].wrapping_sub(b.hidden[l]);
}
deltas.push(DomainDelta {
domain: b.id(),
observed,
hidden,
});
}
WorldDelta {
domain_deltas: deltas,
turn_advance: after.turn.wrapping_sub(before.turn),
}
}
/// Domains that actually changed.
pub fn touched_domains(&self) -> Vec<DomainId> {
self.domain_deltas
.iter()
.filter(|d| !d.is_zero())
.map(|d| d.domain)
.collect()
}
pub fn hash(&self) -> Hash {
let mut h = Hasher::new();
h.write_tag("world-delta");
h.write_u64(self.turn_advance);
for d in &self.domain_deltas {
h.write_u8(d.domain.0);
for &v in &d.observed {
h.write_i64(v);
}
for &v in &d.hidden {
h.write_i64(v);
}
}
h.finish()
}
}