Compare commits
2
Commits
0305f683cd
...
2fe989bcb3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fe989bcb3 | ||
|
|
39386a81c9 |
@@ -0,0 +1,3 @@
|
|||||||
|
/target
|
||||||
|
/ci_out
|
||||||
|
Cargo.lock
|
||||||
+23
@@ -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"
|
||||||
@@ -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 1–7 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.
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -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 1–7 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "world_model"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/lib.rs"
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 1–7 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.
|
||||||
Reference in New Issue
Block a user