Compare commits

..
2 Commits
Author SHA1 Message Date
linus-d 2fe989bcb3 changes claude never committed 2026-06-21 18:00:52 -07:00
linus-d 39386a81c9 added plan 2026-06-21 15:29:01 -07:00
34 changed files with 5812 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
/target
/ci_out
Cargo.lock
+23
View File
@@ -0,0 +1,23 @@
[workspace]
resolver = "2"
members = [
"crates/world_model",
"crates/rune_ir",
"crates/trace_model",
"crates/generators",
"crates/reference_runtime",
"crates/runtime_under_test",
"crates/collapse_analysis",
"crates/semantic_mutation",
"crates/replay_corpus",
"crates/ci_reports",
]
[workspace.package]
version = "0.1.0"
edition = "2021"
license = "MIT"
[profile.release]
opt-level = 3
lto = "thin"
+92
View File
@@ -0,0 +1,92 @@
# Magicka VM — Phase 0/1
> The deliverable is a Rust engine whose tests make a fake universe fail.
This repository implements the Phase 0/1 specification in `plan.md`: an
**adversarial testing framework first**, then a **reference runtime** that
passes it, then a **runtime under test** that matches the reference. No spell
content, templates, or cosmetic runes — the value is in the tests that refuse
to let the universe collapse into a single score, resource, effect axis,
executor, rune, hidden formula, or decorative domain.
## Workspace layout
Built in the mandatory order from the spec:
| # | Crate | Role |
|---|-------|------|
| 1 | `world_model` | 8 independent domains, world snapshot, perturbation axes, deltas, deterministic primitives (ids, stable hash, RNG) |
| | `rune_ir` | Rune token / program model (no stream is ever rejected) |
| 2 | `trace_model` | Execution trace + all graphs, behavior fingerprint, replay record, fault log, trace metrics |
| 3 | `generators` | Worlds, programs, executors, contracts, perturbations; rejects flat cases |
| 4 | `collapse_analysis` | The 11 compression attacks + collapse gates |
| 5 | `semantic_mutation` | Structurally generated mutant runtimes; proves every one is killed |
| 6 | `replay_corpus` | Permanent, bit-exact replay cases |
| 7 | `reference_runtime` | The executable spec engine (`Runtime` trait, `resolve`) |
| 8 | `runtime_under_test` | Config-driven engine, proven equivalent to the reference |
| | `ci_reports` | Orchestrator + `ci` binary; emits the 8 required reports |
The optimized runtime may not begin before steps 17 pass CI; until then the
runtime under test is the reference engine driven through the same config
surface, which is equivalent by construction. Mutation swaps in a *mutated*
config to prove the suite detects any divergence.
## The engine in one paragraph
A world is 8 domains, each with 4 observed + 2 hidden integer lanes, a dense
8×8 coupling matrix, partial observability, and pending scheduled effects. A
rune program is interpreted under ≥3 executors; each opcode reads several
domains, mixes them through a nonlinear avalanche keyed by per-domain
constants, the world coupling, and the executor's salt, then writes back —
recording causal/read/write/information-flow/temporal edges as it goes.
Scheduled effects and coupling diffusion propagate changes 3 turns into the
future. Because every output bit depends on all inputs, the universe is
high-rank, incompressible, future-sensitive, and executor-divergent — exactly
the properties the gates demand.
## Running CI
```bash
cargo test # unit tests for every crate
cargo run --release -p ci_reports --bin ci # full gate run (fast scale)
```
Reports are written to `ci_out/` (8 JSON files + `ci_summary.md`). The binary
exits non-zero if any gate fails.
### Scales
`MAGICKA_SCALE` selects the corpus size; individual counts can be overridden.
| Scale | executions | replay | mutants | notes |
|-------|-----------|--------|---------|-------|
| `tiny` | 120 | 120 | 520 | smoke (~80 ms) |
| `fast` (default) | 600 | 600 | 520 | every gate, ~0.3 s |
| `full` | 1,000,000 | 10,000 | 600 | merge-blocking spec gates |
```bash
MAGICKA_SCALE=full cargo run --release -p ci_reports --bin ci
# or override individual counts:
MAGICKA_EXECUTIONS=20000 MAGICKA_REPLAY=10000 cargo run --release -p ci_reports --bin ci
```
The gate *thresholds* are identical across scales — only the corpus size
changes. Fast CI runs a representative slice; merge-blocking CI runs `full`.
## Gates enforced (all must pass)
- **runtime_equivalence** — 100% of executions: `canonical(reference) == canonical(runtime_under_test)` over delta, trace, faults, replay hash, and 3-turn future hash.
- **causal_rank / trace** — median causal edges ≥ 24, 95% causal rank ≥ 6, median touched domains ≥ 4, 95% ≥ 3, fingerprint collision rate < 5%, largest cluster < 2%.
- **domain_participation** — each domain appears in ≥ 35% of traces, influences ≥ 20%, is mutated in ≥ 20%; removing any domain loses ≥ 10% behavioral diversity; merging any pair loses ≥ 8%; no read-only or write-only domain.
- **metamorphic_response** — ≥ 90% of perturbations alter the trace, ≥ 75% the delta, ≥ 50% the 3-turn future; ≤ 5% unexplained neutral.
- **compression_resistance** — best 1/2/4-factor models predict < 40/55/70%; no single domain > 30%, no pair > 55%; every compressed model loses ≥ 35% information. All 11 attack families are run.
- **mutation_survivor** — ≥ 500 structurally generated mutants, 0 survivors.
- **contract** — every admitted case satisfies its semantic contract (min causal rank, domain participation, future sensitivity, context divergence, max compressibility); contract-violating cases are rejected at admission.
- **replay** — 100% deterministic, 0 hash drift.
- **coverage** — generated/contract rejection accounting; no admitted case fails the generated gates.
## Determinism
Everything is seed-derived and integer-only (SplitMix64 RNG, FNV-1a content
hashing, wrapping/guarded arithmetic). No floating point enters a canonical
hash, so replay is bit-exact across machines and runs. No external crates.
+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()
}
}
+284
View File
@@ -0,0 +1,284 @@
ChatGPT3:26 PM
Rust Phase 0/1 Implementor Specification
Objective
Build the testing framework first, then the runtime.
No spell content is accepted until CI proves the system resists collapse into:
single score
single resource
single effect axis
single executor behavior
single rune behavior
single hidden damage formula
decorative world state
Hard CI Gates
Minimum per full CI run:
Generated worlds: 50,000
Generated rune programs: 250,000
Executions: 1,000,000
Perturbations per execution: 10
Semantic mutants per run: 500
Replay corpus cases: 10,000 minimum
Reference/runtime comparison: 100% of executions
Fast CI may run 10%, but merge-blocking CI must run full gates.
Repository Layout
crates/
world_model/
rune_ir/
trace_model/
generators/
reference_runtime/
runtime_under_test/
collapse_analysis/
semantic_mutation/
replay_corpus/
ci_reports/
Implementation order is mandatory:
1. world_model
2. trace_model
3. generators
4. collapse_analysis
5. semantic_mutation
6. replay_corpus
7. reference_runtime
8. runtime_under_test
The optimized runtime may not begin before steps 17 pass CI.
World Model
pub struct WorldSnapshot {
pub id: 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,
}
Minimum domain count:
8 independent world domains
Each domain must expose:
pub trait WorldDomain {
fn domain_id(&self) -> DomainId;
fn read_surface(&self) -> ReadSurface;
fn write_surface(&self) -> WriteSurface;
fn perturbation_axes(&self) -> Vec<PerturbationAxis>;
fn fingerprint(&self) -> DomainFingerprint;
}
Domain acceptance:
Each domain appears in ≥ 35% of traces.
Each domain influences execution in ≥ 20% of corpus.
Each domain is mutated by execution in ≥ 20% of corpus.
Removing any domain reduces corpus behavioral diversity by ≥ 10%.
Merging any two domains loses ≥ 8% predictive accuracy.
Rune Program Model
pub struct RuneProgram {
pub id: ProgramId,
pub tokens: Vec<RuneToken>,
pub seed: u64,
}
No rune stream may be rejected as the primary safety path.
Execution always returns:
pub struct ResolutionResult {
pub delta: WorldDelta,
pub trace: ExecutionTrace,
pub faults: FaultLog,
pub replay: ReplayRecord,
}
Engine crashes, panics, undefined Rust behavior, or unlogged failures fail CI.
Trace Model
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,
}
Trace gates:
Median causal edges per execution: ≥ 24
95% of executions causal rank: ≥ 6
Median touched domains per execution: ≥ 4
95% of executions touched domains: ≥ 3
Behavior fingerprint collision rate: < 5%
Largest behavior cluster: < 2% of corpus
Generator Requirements
Generators must reject flat cases.
pub struct GeneratedCase {
pub world: WorldSnapshot,
pub program: RuneProgram,
pub contexts: Vec<ExecutionContext>,
pub contract: SemanticContract,
pub perturbations: Vec<PerturbedCase>,
}
Generated case gates:
estimated_causal_rank ≥ 6
domain_entropy ≥ configured minimum
perturbation_axes ≥ 10
executor_count ≥ 3
future_dependence present within 3 turns
nonzero hidden/observed state divergence
nonuniform domain fingerprints
Semantic Contract
pub struct SemanticContract {
pub min_causal_rank: usize,
pub min_domain_participation: usize,
pub min_future_sensitivity: f64,
pub min_context_divergence: f64,
pub max_compressibility: f64,
}
Default thresholds:
min_causal_rank: 6
min_domain_participation: 4
min_future_sensitivity: 0.50
min_context_divergence: 0.40
max_compressibility: 0.70
A case passes only if measured trace behavior satisfies its contract.
Metamorphic Testing
For every base execution, produce 10 perturbations.
Perturbations are generated from domain surfaces, not a fixed list.
pub trait PerturbationAxis {
fn apply(&self, world: &WorldSnapshot) -> WorldSnapshot;
fn expected_trace_difference(&self) -> TraceDifferenceExpectation;
}
Metamorphic gates:
≥ 90% perturbations alter trace
≥ 75% perturbations alter world delta
≥ 50% perturbations alter state within 3 future turns
≤ 5% perturbations may be observationally neutral without explanation
Collapse Analysis
The framework must attempt compression attacks.
pub trait CollapseAttack {
fn compress(&self, corpus: &BehaviorCorpus) -> CompressedModel;
fn report(&self) -> CollapseReport;
}
Required attack families:
domain removal
domain merging
constant folding
causal edge deletion
state aliasing
latent factor modeling
behavior clustering
surrogate prediction
temporal flattening
observation flattening
executor identity erasure
Collapse gates:
Best 1-factor model predicts < 40%
Best 2-factor model predicts < 55%
Best 4-factor model predicts < 70%
No single domain explains > 30% outcome variance
No pair of domains explains > 55%
Compressed model loses ≥ 35% trace information
If a smaller model predicts above these thresholds, fail CI.
Semantic Mutation
Mutants are generated structurally from the model.
pub trait SemanticMutator {
fn mutate(&self, runtime: RuntimeArtifact) -> RuntimeArtifact;
fn expected_detection_reason(&self) -> DetectionClass;
}
Mutation gates:
500 semantic mutants minimum
0 surviving mutants
Every mutant must fail at least one named acceptance gate
Survivor report blocks merge
A mutant surviving means the tests are invalid, not that the mutant is acceptable.
Reference Runtime
The reference runtime is the executable spec.
pub trait Runtime {
fn resolve(&self, input: ResolutionInput) -> ResolutionResult;
}
Every execution runs:
let expected = reference.resolve(input.clone());
let actual = runtime_under_test.resolve(input);
assert_eq!(canonical(expected), canonical(actual));
Canonical comparison includes:
world delta
trace
faults
replay hash
future-state hash over 3 turns
Replay Corpus
Every failure becomes permanent.
pub struct ReplayCase {
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,
}
Replay gates:
10,000 cases minimum
100% deterministic replay
0 hash drift unless migration explicitly updates corpus
Reports Required Per CI Run
Generate machine-readable JSON and human-readable markdown:
domain_participation_report
causal_rank_report
compression_resistance_report
metamorphic_response_report
mutation_survivor_report
runtime_equivalence_report
replay_report
coverage_report
Merge blocked unless all reports pass.
Absolute Rejection Conditions
Reject if:
A smaller model predicts behavior above thresholds.
Any domain is decorative.
Any domain is read-only or write-only across corpus.
Most programs share the same behavior fingerprint.
Most outcomes reduce to one numeric axis.
Reference/runtime differ.
Replay is nondeterministic.
Any semantic mutant survives.
Trace evidence cannot explain causality.
Phase 0/1 Completion
Phase 0/1 is complete only when:
the adversarial framework exists first
the reference runtime passes it
the optimized runtime matches the reference runtime
collapse attacks fail to simplify the universe
mutation tests kill every generated simplification
generated programs produce diverse, causal, replayable behavior
No spell list. No templates. No cosmetic runes. The deliverable is a Rust engine whose tests make a fake universe fail.