Audit pass: full-corpus evidence, full-gate mutation, 3-way metamorphic, recomputable causal, bundle attestation
1 (artifact bundle): ci_reports writes evidence/MANIFEST.tsv (content hash+len of every evidence file); attestation verifies completeness+integrity; merge-gates requires the bundle. 2 (every leaf full evidence): run_all_to streams a full trace+delta record for EVERY leaf (base 'b' and perturbation 'p'), not a sample. 3 (attestation completeness): attestation enforces a leaf<->trace bijection (traces == leaves and covered set == leaf set), not just consistency. 4 (collapse derives from full traces): trace_feature_row single-sourced into collapse_analysis; attestation recomputes each collapse row from the retained full trace and requires bit-exact match. 5 (mutants through full gates): evaluate_mutants runs each mutant through engine_acceptance over 128 generated cases (trace/equivalence/domain/ metamorphic/causal), replacing the 64-input local predicates. 6 (trace+delta+future): metamorphic enforces consumed->trace per-case plus consumed-aggregate delta (>=0.65) and future (>=0.80) rates. 7 (recomputable causal): per-edge intervention records written to evidence/causal_evidence.tsv; attestation RE-EXECUTES each from its seed and recomputes base_dv/alt_dv. 8 (no string/comment proof): removed web_assets JS-substring test and the tautological string assert in ci_reports. Verified at fast scale end-to-end: 6600/6600 traces reconstructed, leaf bijection, 1600/1600 causal records recomputed, 600/600 collapse rows derived, bundle intact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -74,8 +74,9 @@ jobs:
|
|||||||
compliance_report; do
|
compliance_report; do
|
||||||
test -s "ci_out/${r}.json" || { echo "MISSING ARTIFACT: ${r}.json"; exit 1; }
|
test -s "ci_out/${r}.json" || { echo "MISSING ARTIFACT: ${r}.json"; exit 1; }
|
||||||
done
|
done
|
||||||
test -s ci_out/evidence/leaves.tsv || { echo "MISSING EVIDENCE: leaves.tsv"; exit 1; }
|
for e in leaves.tsv traces.tsv causal_evidence.tsv collapse_feature_rows.tsv MANIFEST.tsv claims.tsv; do
|
||||||
test -s ci_out/evidence/traces.tsv || { echo "MISSING EVIDENCE: traces.tsv"; exit 1; }
|
test -s "ci_out/evidence/${e}" || { echo "MISSING EVIDENCE: ${e}"; exit 1; }
|
||||||
|
done
|
||||||
|
|
||||||
# Independent attestation (findings 2, 3): a SEPARATE process recomputes
|
# Independent attestation (findings 2, 3): a SEPARATE process recomputes
|
||||||
# the Merkle root from the retained leaves and checks it against the
|
# the Merkle root from the retained leaves and checks it against the
|
||||||
|
|||||||
@@ -15,3 +15,8 @@ path = "src/main.rs"
|
|||||||
# and hashing the verifier reconstructs from raw evidence.
|
# and hashing the verifier reconstructs from raw evidence.
|
||||||
world_model = { path = "../world_model" }
|
world_model = { path = "../world_model" }
|
||||||
trace_model = { path = "../trace_model" }
|
trace_model = { path = "../trace_model" }
|
||||||
|
# For independent RE-EXECUTION of retained per-edge causal interventions.
|
||||||
|
generators = { path = "../generators" }
|
||||||
|
reference_runtime = { path = "../reference_runtime" }
|
||||||
|
# For recomputing collapse feature rows from full traces (proving derivation).
|
||||||
|
collapse_analysis = { path = "../collapse_analysis" }
|
||||||
|
|||||||
+280
-15
@@ -17,10 +17,57 @@
|
|||||||
//! executions performed by a trusted third party — that requires external
|
//! executions performed by a trusted third party — that requires external
|
||||||
//! re-execution / signing infrastructure (see the BLOCKED note in the report).
|
//! re-execution / signing infrastructure (see the BLOCKED note in the report).
|
||||||
|
|
||||||
|
use generators::generate_accepted_case;
|
||||||
|
use reference_runtime::{execute, EngineConfig, ResolutionInput};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use trace_model::{ExecutionTrace, ReplayRecord};
|
use trace_model::{ExecutionTrace, ReplayRecord};
|
||||||
use world_model::{Hash, Hasher, WorldDelta};
|
use world_model::{Hash, Hasher, WorldDelta, HIDDEN_LANES, LANES, NUM_DOMAINS};
|
||||||
|
|
||||||
|
/// Re-execute one retained causal record from its seed and recompute the
|
||||||
|
/// destination delta before and after perturbing the recorded source lane.
|
||||||
|
/// Returns `(base_dv, alt_dv)`; the caller compares against the retained values.
|
||||||
|
pub fn recompute_causal_record(
|
||||||
|
seed: u64,
|
||||||
|
from_domain: usize,
|
||||||
|
from_lane: usize,
|
||||||
|
from_hidden: bool,
|
||||||
|
to_domain: usize,
|
||||||
|
to_lane: usize,
|
||||||
|
to_hidden: bool,
|
||||||
|
) -> (i64, i64) {
|
||||||
|
let (case, _) = generate_accepted_case(seed);
|
||||||
|
let cfg = EngineConfig::reference();
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
let base = execute(&cfg, &input);
|
||||||
|
let mut w = input.world.clone();
|
||||||
|
let fd = from_domain % NUM_DOMAINS;
|
||||||
|
if from_hidden {
|
||||||
|
let l = from_lane % HIDDEN_LANES;
|
||||||
|
w.domains[fd].hidden[l] = w.domains[fd].hidden[l].wrapping_add(0x9_27c1);
|
||||||
|
} else {
|
||||||
|
let l = from_lane % LANES;
|
||||||
|
w.domains[fd].observed[l] = w.domains[fd].observed[l].wrapping_add(0x9_27c1);
|
||||||
|
}
|
||||||
|
let mut alt_input = input.clone();
|
||||||
|
alt_input.world = w;
|
||||||
|
let alt = execute(&cfg, &alt_input);
|
||||||
|
let dd = to_domain % NUM_DOMAINS;
|
||||||
|
let read = |d: &world_model::DomainDelta| -> i64 {
|
||||||
|
if to_hidden {
|
||||||
|
d.hidden[to_lane % HIDDEN_LANES]
|
||||||
|
} else {
|
||||||
|
d.observed[to_lane % LANES]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(read(&base.delta.domain_deltas[dd]), read(&alt.delta.domain_deltas[dd]))
|
||||||
|
}
|
||||||
|
|
||||||
/// Recompute the Merkle root over `leaves` (binary tree, FNV-combined). This is
|
/// Recompute the Merkle root over `leaves` (binary tree, FNV-combined). This is
|
||||||
/// an independent re-implementation of the producer's algorithm; agreement is
|
/// an independent re-implementation of the producer's algorithm; agreement is
|
||||||
@@ -68,13 +115,17 @@ fn claim<'a>(claims: &'a [(String, String)], key: &str) -> Option<&'a str> {
|
|||||||
/// delta + seeds, independently of any reported digest. Returns the recomputed
|
/// delta + seeds, independently of any reported digest. Returns the recomputed
|
||||||
/// leaf, or `None` if the record is malformed.
|
/// leaf, or `None` if the record is malformed.
|
||||||
///
|
///
|
||||||
/// `line` is `ws ps cs prs future leaf <TAB> <trace> <TAB> <delta>`.
|
/// `line` is `kind ws ps cs prs future leaf <TAB> <trace> <TAB> <delta>` where
|
||||||
pub fn recompute_trace_leaf(line: &str) -> Option<(u64, u64)> {
|
/// `kind` is `b` (base) or `p` (perturbation). Returns `(recomputed_leaf,
|
||||||
|
/// claimed_leaf, is_base)`.
|
||||||
|
pub fn recompute_trace_leaf(line: &str) -> Option<(u64, u64, bool)> {
|
||||||
let mut parts = line.splitn(3, '\t');
|
let mut parts = line.splitn(3, '\t');
|
||||||
let header = parts.next()?;
|
let header = parts.next()?;
|
||||||
let trace_s = parts.next()?;
|
let trace_s = parts.next()?;
|
||||||
let delta_s = parts.next()?;
|
let delta_s = parts.next()?;
|
||||||
let mut h = header.split_whitespace();
|
let mut h = header.split_whitespace();
|
||||||
|
let kind = h.next()?;
|
||||||
|
let is_base = kind == "b";
|
||||||
let hx = |s: Option<&str>| -> Option<u64> { u64::from_str_radix(s?.trim(), 16).ok() };
|
let hx = |s: Option<&str>| -> Option<u64> { u64::from_str_radix(s?.trim(), 16).ok() };
|
||||||
let world_seed = hx(h.next())?;
|
let world_seed = hx(h.next())?;
|
||||||
let program_seed = hx(h.next())?;
|
let program_seed = hx(h.next())?;
|
||||||
@@ -95,7 +146,7 @@ pub fn recompute_trace_leaf(line: &str) -> Option<(u64, u64)> {
|
|||||||
delta_hash: delta.hash(),
|
delta_hash: delta.hash(),
|
||||||
future_hash: Hash(future),
|
future_hash: Hash(future),
|
||||||
};
|
};
|
||||||
Some((rr.hash().0, claimed_leaf))
|
Some((rr.hash().0, claimed_leaf, is_base))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Outcome of an attestation.
|
/// Outcome of an attestation.
|
||||||
@@ -107,6 +158,11 @@ pub struct Attestation {
|
|||||||
pub claimed_root: Option<u64>,
|
pub claimed_root: Option<u64>,
|
||||||
pub traces_verified: usize,
|
pub traces_verified: usize,
|
||||||
pub traces_total: usize,
|
pub traces_total: usize,
|
||||||
|
pub causal_total: usize,
|
||||||
|
pub causal_recomputed: usize,
|
||||||
|
pub causal_confirmed: usize,
|
||||||
|
pub collapse_total: usize,
|
||||||
|
pub collapse_derived: usize,
|
||||||
pub checks: Vec<(String, bool)>,
|
pub checks: Vec<(String, bool)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,26 +203,174 @@ pub fn verify_dir(dir: &Path) -> Result<Attestation, String> {
|
|||||||
.map_err(|e| format!("cannot read {}: {e}", traces_path.display()))?;
|
.map_err(|e| format!("cannot read {}: {e}", traces_path.display()))?;
|
||||||
let mut traces_total = 0usize;
|
let mut traces_total = 0usize;
|
||||||
let mut traces_verified = 0usize;
|
let mut traces_verified = 0usize;
|
||||||
|
let mut covered: HashSet<u64> = HashSet::new();
|
||||||
for line in traces_txt.lines() {
|
for line in traces_txt.lines() {
|
||||||
if line.starts_with('#') || line.trim().is_empty() {
|
if line.starts_with('#') || line.trim().is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
traces_total += 1;
|
traces_total += 1;
|
||||||
match recompute_trace_leaf(line) {
|
if let Some((recomputed_leaf, claimed_leaf, _is_base)) = recompute_trace_leaf(line) {
|
||||||
Some((recomputed_leaf, claimed_leaf)) => {
|
|
||||||
if recomputed_leaf == claimed_leaf && leaf_set.contains(&recomputed_leaf) {
|
if recomputed_leaf == claimed_leaf && leaf_set.contains(&recomputed_leaf) {
|
||||||
traces_verified += 1;
|
traces_verified += 1;
|
||||||
|
covered.insert(recomputed_leaf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let traces_present = traces_total > 0;
|
let traces_present = traces_total > 0;
|
||||||
checks.push(("full-trace evidence present".into(), traces_present));
|
checks.push(("full-trace evidence present".into(), traces_present));
|
||||||
checks.push((
|
checks.push((
|
||||||
"every sampled full trace recomputes to a retained leaf".into(),
|
"every full trace recomputes to a retained leaf".into(),
|
||||||
traces_present && traces_verified == traces_total,
|
traces_present && traces_verified == traces_total,
|
||||||
));
|
));
|
||||||
|
// Completeness (finding 3): every retained leaf must be covered by a full
|
||||||
|
// trace record, and there must be exactly one record per leaf — a bijection,
|
||||||
|
// not just a consistent count.
|
||||||
|
checks.push((
|
||||||
|
"every leaf has full retained evidence (bijection)".into(),
|
||||||
|
traces_present
|
||||||
|
&& traces_total == leaves.len()
|
||||||
|
&& covered.len() == leaf_set.len(),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Causal intervention evidence (finding 7): RE-EXECUTE each retained record
|
||||||
|
// from its seed and confirm the retained base_dv/alt_dv reproduce, then
|
||||||
|
// require the confirmed fraction to clear the threshold.
|
||||||
|
let causal_path = dir.join("evidence/causal_evidence.tsv");
|
||||||
|
let causal_txt = std::fs::read_to_string(&causal_path)
|
||||||
|
.map_err(|e| format!("cannot read {}: {e}", causal_path.display()))?;
|
||||||
|
let mut causal_total = 0usize;
|
||||||
|
let mut causal_recomputed = 0usize;
|
||||||
|
let mut causal_confirmed = 0usize;
|
||||||
|
for line in causal_txt.lines() {
|
||||||
|
if line.starts_with("seed") || line.starts_with('#') || line.trim().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let f: Vec<&str> = line.split('\t').collect();
|
||||||
|
if f.len() < 9 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let seed = match u64::from_str_radix(f[0].trim(), 16) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
let p = |i: usize| f[i].trim().parse::<i64>().ok();
|
||||||
|
let (Some(fd), Some(fl), Some(fh), Some(td), Some(tl), Some(th), Some(bdv), Some(adv)) =
|
||||||
|
(p(1), p(2), p(3), p(4), p(5), p(6), p(7), p(8))
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
causal_total += 1;
|
||||||
|
let (rb, ra) = recompute_causal_record(
|
||||||
|
seed, fd as usize, fl as usize, fh != 0, td as usize, tl as usize, th != 0,
|
||||||
|
);
|
||||||
|
if rb == bdv && ra == adv {
|
||||||
|
causal_recomputed += 1;
|
||||||
|
}
|
||||||
|
if rb != ra {
|
||||||
|
causal_confirmed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Collapse derivation (finding 4): recompute each collapse feature row from
|
||||||
|
// the corresponding retained FULL trace + delta and require bit-exact match,
|
||||||
|
// proving the summary the collapse gate consumed derives from the full trace.
|
||||||
|
let crows_path = dir.join("evidence/collapse_feature_rows.tsv");
|
||||||
|
let crows_txt = std::fs::read_to_string(&crows_path)
|
||||||
|
.map_err(|e| format!("cannot read {}: {e}", crows_path.display()))?;
|
||||||
|
let claimed_rows: Vec<Vec<u64>> = crows_txt
|
||||||
|
.lines()
|
||||||
|
.filter(|l| !l.trim().is_empty())
|
||||||
|
.map(|l| l.split_whitespace().filter_map(|t| t.parse::<u64>().ok()).collect())
|
||||||
|
.collect();
|
||||||
|
// Re-read the trace records in order to recompute their feature rows.
|
||||||
|
let mut collapse_total = 0usize;
|
||||||
|
let mut collapse_derived = 0usize;
|
||||||
|
{
|
||||||
|
let mut idx = 0usize;
|
||||||
|
for line in traces_txt.lines() {
|
||||||
|
// Collapse rows correspond to BASE executions in order.
|
||||||
|
if !line.starts_with("b ") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if idx >= claimed_rows.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let mut parts = line.splitn(3, '\t');
|
||||||
|
let _hdr = parts.next();
|
||||||
|
let trace_s = parts.next();
|
||||||
|
let delta_s = parts.next();
|
||||||
|
if let (Some(ts), Some(ds)) = (trace_s, delta_s) {
|
||||||
|
if let (Some(trace), Some(delta)) =
|
||||||
|
(ExecutionTrace::deserialize(ts), WorldDelta::deserialize(ds))
|
||||||
|
{
|
||||||
|
let recomputed = collapse_analysis::trace_feature_row(&trace, &delta);
|
||||||
|
let recomputed_bits: Vec<u64> = recomputed.iter().map(|v| v.to_bits()).collect();
|
||||||
|
collapse_total += 1;
|
||||||
|
if recomputed_bits == claimed_rows[idx] {
|
||||||
|
collapse_derived += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let collapse_present = collapse_total > 0;
|
||||||
|
checks.push(("collapse feature rows present".into(), collapse_present));
|
||||||
|
checks.push((
|
||||||
|
"every collapse summary derives from a full trace".into(),
|
||||||
|
collapse_present && collapse_derived == collapse_total,
|
||||||
|
));
|
||||||
|
|
||||||
|
let causal_present = causal_total > 0;
|
||||||
|
let causal_frac = if causal_total > 0 { causal_confirmed as f64 / causal_total as f64 } else { 0.0 };
|
||||||
|
checks.push(("causal evidence present".into(), causal_present));
|
||||||
|
checks.push((
|
||||||
|
"every causal record recomputes (base_dv/alt_dv reproduce)".into(),
|
||||||
|
causal_present && causal_recomputed == causal_total,
|
||||||
|
));
|
||||||
|
checks.push((
|
||||||
|
"recomputed causal confirmation >= 0.50".into(),
|
||||||
|
causal_present && causal_frac >= 0.50,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Artifact bundle (finding 1): the merge-scale claim requires a complete,
|
||||||
|
// content-hashed bundle. Verify the manifest lists the required files and
|
||||||
|
// each file's recomputed hash + length match.
|
||||||
|
let manifest_path = dir.join("evidence/MANIFEST.tsv");
|
||||||
|
let manifest_txt = std::fs::read_to_string(&manifest_path)
|
||||||
|
.map_err(|e| format!("cannot read {}: {e}", manifest_path.display()))?;
|
||||||
|
let required = [
|
||||||
|
"leaves.tsv",
|
||||||
|
"claims.tsv",
|
||||||
|
"causal_evidence.tsv",
|
||||||
|
"collapse_feature_rows.tsv",
|
||||||
|
"traces.tsv",
|
||||||
|
];
|
||||||
|
let mut listed: HashSet<String> = HashSet::new();
|
||||||
|
let mut bundle_intact = true;
|
||||||
|
for line in manifest_txt.lines() {
|
||||||
|
if line.starts_with("file") || line.trim().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let f: Vec<&str> = line.split('\t').collect();
|
||||||
|
if f.len() < 3 {
|
||||||
|
bundle_intact = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let name = f[0].trim();
|
||||||
|
let claimed_hash = u64::from_str_radix(f[1].trim(), 16).ok();
|
||||||
|
let claimed_len = f[2].trim().parse::<usize>().ok();
|
||||||
|
let bytes = std::fs::read(dir.join("evidence").join(name)).unwrap_or_default();
|
||||||
|
let mut h = Hasher::new();
|
||||||
|
h.write_tag("evidence-file");
|
||||||
|
h.write_bytes(&bytes);
|
||||||
|
if claimed_hash != Some(h.finish().0) || claimed_len != Some(bytes.len()) {
|
||||||
|
bundle_intact = false;
|
||||||
|
}
|
||||||
|
listed.insert(name.to_string());
|
||||||
|
}
|
||||||
|
let bundle_complete = required.iter().all(|r| listed.contains(*r));
|
||||||
|
checks.push(("artifact bundle manifest complete".into(), bundle_complete));
|
||||||
|
checks.push(("artifact bundle files intact (hash + length)".into(), bundle_intact && bundle_complete));
|
||||||
|
|
||||||
let ok = checks.iter().all(|(_, b)| *b);
|
let ok = checks.iter().all(|(_, b)| *b);
|
||||||
Ok(Attestation {
|
Ok(Attestation {
|
||||||
@@ -176,6 +380,11 @@ pub fn verify_dir(dir: &Path) -> Result<Attestation, String> {
|
|||||||
claimed_root,
|
claimed_root,
|
||||||
traces_verified,
|
traces_verified,
|
||||||
traces_total,
|
traces_total,
|
||||||
|
causal_total,
|
||||||
|
causal_recomputed,
|
||||||
|
causal_confirmed,
|
||||||
|
collapse_total,
|
||||||
|
collapse_derived,
|
||||||
checks,
|
checks,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -227,12 +436,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn sample_delta(seed: u64) -> WorldDelta {
|
fn sample_delta(seed: u64) -> WorldDelta {
|
||||||
|
let domain_deltas = (0..NUM_DOMAINS)
|
||||||
|
.map(|d| {
|
||||||
let mut observed = [0i64; LANES];
|
let mut observed = [0i64; LANES];
|
||||||
observed[0] = seed as i64;
|
observed[0] = seed as i64 + d as i64;
|
||||||
WorldDelta {
|
DomainDelta { domain: DomainId(d as u8), observed, hidden: [0i64; HIDDEN_LANES] }
|
||||||
domain_deltas: vec![DomainDelta { domain: DomainId(2), observed, hidden: [0i64; HIDDEN_LANES] }],
|
})
|
||||||
turn_advance: 0,
|
.collect();
|
||||||
}
|
WorldDelta { domain_deltas, turn_advance: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn record(seed: u64) -> (ReplayRecord, ExecutionTrace, WorldDelta) {
|
fn record(seed: u64) -> (ReplayRecord, ExecutionTrace, WorldDelta) {
|
||||||
@@ -280,12 +491,66 @@ mod tests {
|
|||||||
trace_s = trace_s.replacen("trace-v1 ", "trace-v1 999 ", 1);
|
trace_s = trace_s.replacen("trace-v1 ", "trace-v1 999 ", 1);
|
||||||
}
|
}
|
||||||
traces.push_str(&format!(
|
traces.push_str(&format!(
|
||||||
"{:016x} {:016x} {:016x} {:016x} {:016x} {:016x}\t{}\t{}\n",
|
"b {:016x} {:016x} {:016x} {:016x} {:016x} {:016x}\t{}\t{}\n",
|
||||||
rr.world_seed, rr.program_seed, rr.contract_seed, rr.perturbation_seed,
|
rr.world_seed, rr.program_seed, rr.contract_seed, rr.perturbation_seed,
|
||||||
rr.future_hash.0, rr.hash().0, trace_s, delta.serialize(),
|
rr.future_hash.0, rr.hash().0, trace_s, delta.serialize(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
std::fs::write(ev.join("traces.tsv"), traces).unwrap();
|
std::fs::write(ev.join("traces.tsv"), traces).unwrap();
|
||||||
|
|
||||||
|
// Real, recomputable causal evidence from actual reference executions.
|
||||||
|
let mut causal = String::from(
|
||||||
|
"seed\tfrom_domain\tfrom_lane\tfrom_hidden\tto_domain\tto_lane\tto_hidden\tbase_dv\talt_dv\n",
|
||||||
|
);
|
||||||
|
let cfg = EngineConfig::reference();
|
||||||
|
for &cseed in &[0xC0FFEEu64, 0xBEEF, 0x1234, 0x5EED, 0xABCD] {
|
||||||
|
let (case, _) = generate_accepted_case(cseed);
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
let base = execute(&cfg, &input);
|
||||||
|
let edges = base.trace.causal_graph.edges.clone();
|
||||||
|
let stride = (edges.len() / 12).max(1);
|
||||||
|
for e in edges.iter().step_by(stride).take(12) {
|
||||||
|
let fd = e.from.domain as usize % NUM_DOMAINS;
|
||||||
|
let fl = e.from.lane as usize;
|
||||||
|
let fh = e.from.hidden;
|
||||||
|
let td = e.to.domain as usize % NUM_DOMAINS;
|
||||||
|
let tl = e.to.lane as usize;
|
||||||
|
let th = e.to.hidden;
|
||||||
|
let (b, a) = recompute_causal_record(cseed, fd, fl, fh, td, tl, th);
|
||||||
|
causal.push_str(&format!(
|
||||||
|
"{:016x}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\n",
|
||||||
|
cseed, fd, fl, fh as u8, td, tl, th as u8, b, a
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::fs::write(ev.join("causal_evidence.tsv"), causal).unwrap();
|
||||||
|
|
||||||
|
// Collapse feature rows derived from the same synthetic traces, in order.
|
||||||
|
let mut crows = String::new();
|
||||||
|
for (_, trace, delta) in &records {
|
||||||
|
let row = collapse_analysis::trace_feature_row(trace, delta);
|
||||||
|
let cells: Vec<String> = row.iter().map(|v| v.to_bits().to_string()).collect();
|
||||||
|
crows.push_str(&cells.join(" "));
|
||||||
|
crows.push('\n');
|
||||||
|
}
|
||||||
|
std::fs::write(ev.join("collapse_feature_rows.tsv"), crows).unwrap();
|
||||||
|
|
||||||
|
// Bundle manifest over the written files.
|
||||||
|
let mut manifest = String::from("file\thash\tbytes\n");
|
||||||
|
for name in ["leaves.tsv", "claims.tsv", "causal_evidence.tsv", "collapse_feature_rows.tsv", "traces.tsv"] {
|
||||||
|
let bytes = std::fs::read(ev.join(name)).unwrap_or_default();
|
||||||
|
let mut h = Hasher::new();
|
||||||
|
h.write_tag("evidence-file");
|
||||||
|
h.write_bytes(&bytes);
|
||||||
|
manifest.push_str(&format!("{}\t{:016x}\t{}\n", name, h.finish().0, bytes.len()));
|
||||||
|
}
|
||||||
|
std::fs::write(ev.join("MANIFEST.tsv"), manifest).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ fn main() {
|
|||||||
" full traces verified: {}/{} (reconstructed + leaf re-derived)",
|
" full traces verified: {}/{} (reconstructed + leaf re-derived)",
|
||||||
att.traces_verified, att.traces_total
|
att.traces_verified, att.traces_total
|
||||||
);
|
);
|
||||||
|
eprintln!(
|
||||||
|
" causal records recomputed: {}/{} ({} confirmed)",
|
||||||
|
att.causal_recomputed, att.causal_total, att.causal_confirmed
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
" collapse rows derived from full traces: {}/{}",
|
||||||
|
att.collapse_derived, att.collapse_total
|
||||||
|
);
|
||||||
for (name, ok) in &att.checks {
|
for (name, ok) in &att.checks {
|
||||||
eprintln!(" [{}] {}", if *ok { "PASS" } else { "FAIL" }, name);
|
eprintln!(" [{}] {}", if *ok { "PASS" } else { "FAIL" }, name);
|
||||||
}
|
}
|
||||||
|
|||||||
+268
-196
@@ -22,7 +22,7 @@ use reference_runtime::{
|
|||||||
canonical, execute, Canonical, EngineConfig, ResolutionInput, ResolutionResult, Runtime,
|
canonical, execute, Canonical, EngineConfig, ResolutionInput, ResolutionResult, Runtime,
|
||||||
};
|
};
|
||||||
use runtime_under_test::{native_resolve, RuntimeUnderTest};
|
use runtime_under_test::{native_resolve, RuntimeUnderTest};
|
||||||
use semantic_mutation::{generate_mutants, DetectionClass, MutationOutcome};
|
use semantic_mutation::{generate_mutants, MutationOutcome};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use world_model::{
|
use world_model::{
|
||||||
@@ -211,23 +211,39 @@ pub fn causal_trace_fails(median_edges: f64, p95_rank: f64) -> bool {
|
|||||||
median_edges < TRACE_EDGES_MIN || p95_rank < TRACE_RANK_P95_MIN
|
median_edges < TRACE_EDGES_MIN || p95_rank < TRACE_RANK_P95_MIN
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-config causal gate over an input corpus (used to kill mutants by the same
|
/// Number of generated cases each mutant is run through (a real corpus, not a
|
||||||
/// predicate the acceptance trace gate uses).
|
/// fixed 64-input local sample).
|
||||||
fn config_causal_fails(cfg: &EngineConfig, inputs: &[ResolutionInput]) -> bool {
|
pub const MUTATION_GATE_CASES: usize = 128;
|
||||||
let edges: Vec<f64> =
|
|
||||||
inputs.iter().map(|i| execute(cfg, i).trace.causal_edge_count() as f64).collect();
|
|
||||||
let ranks: Vec<f64> =
|
|
||||||
inputs.iter().map(|i| execute(cfg, i).trace.causal_rank() as f64).collect();
|
|
||||||
causal_trace_fails(median(&edges), percentile(&ranks, 0.05))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Per-config domain-participation gate over an input corpus.
|
/// Run an engine config through the FULL set of engine-behavior acceptance gates
|
||||||
fn config_domain_fails(cfg: &EngineConfig, inputs: &[ResolutionInput]) -> bool {
|
/// over `n_cases` freshly generated cases (the same gate functions and corpus
|
||||||
let n = inputs.len().max(1) as f64;
|
/// shape the acceptance run uses) and return the names of the gates it fails.
|
||||||
|
/// The reference returns an empty vec; a broken engine returns the gates that
|
||||||
|
/// catch it.
|
||||||
|
pub fn engine_acceptance(cfg: &EngineConfig, n_cases: usize) -> Vec<String> {
|
||||||
|
let refcfg = EngineConfig::reference();
|
||||||
|
let mut edges = Vec::with_capacity(n_cases);
|
||||||
|
let mut ranks = Vec::with_capacity(n_cases);
|
||||||
|
let mut equiv_fail = false;
|
||||||
let mut appears = [0u64; NUM_DOMAINS];
|
let mut appears = [0u64; NUM_DOMAINS];
|
||||||
let mut mutated = [0u64; NUM_DOMAINS];
|
let mut mutated = [0u64; NUM_DOMAINS];
|
||||||
for i in inputs {
|
let mut influence_changed = [false; NUM_DOMAINS];
|
||||||
let r = execute(cfg, i);
|
let mut consumed = 0usize;
|
||||||
|
let mut consumed_trace_violation = 0usize;
|
||||||
|
let mut consumed_delta = 0usize;
|
||||||
|
let mut consumed_future = 0usize;
|
||||||
|
let n = n_cases.max(1) as f64;
|
||||||
|
|
||||||
|
for i in 0..n_cases {
|
||||||
|
let (case, _) = generate_accepted_case(case_seed(i));
|
||||||
|
let input = input_from_case(&case);
|
||||||
|
let r = execute(cfg, &input);
|
||||||
|
let rr = execute(&refcfg, &input);
|
||||||
|
edges.push(r.trace.causal_edge_count() as f64);
|
||||||
|
ranks.push(r.trace.causal_rank() as f64);
|
||||||
|
if canonical(&r) != canonical(&rr) {
|
||||||
|
equiv_fail = true;
|
||||||
|
}
|
||||||
for d in 0..NUM_DOMAINS {
|
for d in 0..NUM_DOMAINS {
|
||||||
if r.trace.read_graph.access_count[d] > 0 || r.trace.write_graph.access_count[d] > 0 {
|
if r.trace.read_graph.access_count[d] > 0 || r.trace.write_graph.access_count[d] > 0 {
|
||||||
appears[d] += 1;
|
appears[d] += 1;
|
||||||
@@ -238,86 +254,90 @@ fn config_domain_fails(cfg: &EngineConfig, inputs: &[ResolutionInput]) -> bool {
|
|||||||
mutated[dd.domain.0 as usize] += 1;
|
mutated[dd.domain.0 as usize] += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Per-domain measured influence: masking each domain in `cfg` must change
|
||||||
|
// this config's own output.
|
||||||
|
let base_h = (r.trace.canonical_hash(), r.delta.hash());
|
||||||
|
for d in 0..NUM_DOMAINS {
|
||||||
|
if influence_changed[d] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut mc = cfg.clone();
|
||||||
|
mc.domain_mask[d] = false;
|
||||||
|
let m = execute(&mc, &input);
|
||||||
|
if (m.trace.canonical_hash(), m.delta.hash()) != base_h {
|
||||||
|
influence_changed[d] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Metamorphic over this case's perturbations.
|
||||||
|
let bt = r.trace.canonical_hash();
|
||||||
|
let bd = r.delta.hash();
|
||||||
|
let bf = r.replay.future_hash;
|
||||||
|
for pc in &case.perturbations {
|
||||||
|
let pin = input_with_world(&case, pc.world.clone());
|
||||||
|
let pr = execute(cfg, &pin);
|
||||||
|
let read = r.trace.read_graph.access_count[pc.target_domain % NUM_DOMAINS] > 0;
|
||||||
|
if read {
|
||||||
|
consumed += 1;
|
||||||
|
if pr.delta.hash() != bd {
|
||||||
|
consumed_delta += 1;
|
||||||
|
}
|
||||||
|
if pr.replay.future_hash != bf {
|
||||||
|
consumed_future += 1;
|
||||||
|
}
|
||||||
|
if pc.expectation.expect_trace_change
|
||||||
|
&& pr.trace.canonical_hash() == bt
|
||||||
|
&& pc.expectation.neutral_explanation.is_none()
|
||||||
|
{
|
||||||
|
consumed_trace_violation += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
(0..NUM_DOMAINS).any(|d| {
|
|
||||||
(appears[d] as f64 / n) < DOMAIN_APPEARS_MIN || (mutated[d] as f64 / n) < DOMAIN_MUTATED_MIN
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-config temporal/future gate: temporal edges present, future sensitive to
|
let mut fails = Vec::new();
|
||||||
/// perturbation, and the 3-turn future reproduces the reference.
|
if causal_trace_fails(median(&edges), percentile(&ranks, 0.05)) {
|
||||||
fn config_temporal_fails(
|
fails.push("causal_rank/trace".to_string());
|
||||||
cfg: &EngineConfig,
|
|
||||||
inputs: &[ResolutionInput],
|
|
||||||
ref_future: &[Hash],
|
|
||||||
) -> bool {
|
|
||||||
let tedges: Vec<f64> =
|
|
||||||
inputs.iter().map(|i| execute(cfg, i).trace.temporal_graph.edge_count() as f64).collect();
|
|
||||||
if median(&tedges) < 1.0 {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
let mut altered = 0usize;
|
if equiv_fail {
|
||||||
for i in inputs {
|
fails.push("runtime_equivalence".to_string());
|
||||||
let base = execute(cfg, i).replay.future_hash;
|
|
||||||
let mut p = i.clone();
|
|
||||||
p.world.domains[0].observed[0] = p.world.domains[0].observed[0].wrapping_add(101);
|
|
||||||
p.world.mark_perturbed();
|
|
||||||
if execute(cfg, &p).replay.future_hash != base {
|
|
||||||
altered += 1;
|
|
||||||
}
|
}
|
||||||
|
if (0..NUM_DOMAINS).any(|d| {
|
||||||
|
(appears[d] as f64 / n) < DOMAIN_APPEARS_MIN
|
||||||
|
|| (mutated[d] as f64 / n) < DOMAIN_MUTATED_MIN
|
||||||
|
|| !influence_changed[d]
|
||||||
|
}) {
|
||||||
|
fails.push("domain_participation".to_string());
|
||||||
}
|
}
|
||||||
if (altered as f64 / inputs.len().max(1) as f64) < FUTURE_ALT_MIN {
|
let mc = consumed.max(1) as f64;
|
||||||
return true;
|
if consumed == 0
|
||||||
|
|| (consumed_trace_violation as f64 / mc) > 0.01
|
||||||
|
|| (consumed_delta as f64 / mc) < CONSUMED_DELTA_MIN
|
||||||
|
|| (consumed_future as f64 / mc) < CONSUMED_FUTURE_MIN
|
||||||
|
{
|
||||||
|
fails.push("metamorphic_response".to_string());
|
||||||
}
|
}
|
||||||
inputs
|
let ce = causal_explanation_gate(cfg, n_cases, 4);
|
||||||
.iter()
|
if !ce.failures.is_empty() {
|
||||||
.zip(ref_future)
|
fails.push("causal_explanation".to_string());
|
||||||
.any(|(i, rf)| execute(cfg, i).replay.future_hash != *rf)
|
}
|
||||||
|
fails
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-config equivalence gate: the config diverges from the reference canonical
|
/// The mutation gate: every mutant must be rejected by the FULL engine-behavior
|
||||||
/// view on at least one input.
|
/// acceptance gates over a real generated-case corpus (not 64 fixed inputs).
|
||||||
fn config_equivalence_fails(
|
pub fn evaluate_mutants(count: usize, _inputs: &[ResolutionInput]) -> MutationOutcome {
|
||||||
cfg: &EngineConfig,
|
|
||||||
inputs: &[ResolutionInput],
|
|
||||||
ref_canon: &[Canonical],
|
|
||||||
) -> bool {
|
|
||||||
inputs
|
|
||||||
.iter()
|
|
||||||
.zip(ref_canon)
|
|
||||||
.any(|(i, rc)| canonical(&execute(cfg, i)) != *rc)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The mutation gate: every mutant must be rejected by the **real acceptance
|
|
||||||
/// gate predicate** it targets — the same functions `run_all` decides with.
|
|
||||||
pub fn evaluate_mutants(count: usize, inputs: &[ResolutionInput]) -> MutationOutcome {
|
|
||||||
let refcfg = EngineConfig::reference();
|
|
||||||
let ref_canon: Vec<Canonical> = inputs.iter().map(|i| canonical(&execute(&refcfg, i))).collect();
|
|
||||||
let ref_future: Vec<Hash> = inputs.iter().map(|i| execute(&refcfg, i).replay.future_hash).collect();
|
|
||||||
let mutants = generate_mutants(count);
|
let mutants = generate_mutants(count);
|
||||||
let mut killed = 0;
|
let mut killed = 0;
|
||||||
let mut survivors = Vec::new();
|
let mut survivors = Vec::new();
|
||||||
for m in &mutants {
|
for m in &mutants {
|
||||||
let rejected = match m.expected {
|
let fails = engine_acceptance(&m.config, MUTATION_GATE_CASES);
|
||||||
DetectionClass::RuntimeEquivalence => {
|
if fails.is_empty() {
|
||||||
config_equivalence_fails(&m.config, inputs, &ref_canon)
|
|
||||||
}
|
|
||||||
DetectionClass::CausalGate => config_causal_fails(&m.config, inputs),
|
|
||||||
DetectionClass::TemporalGate => config_temporal_fails(&m.config, inputs, &ref_future),
|
|
||||||
DetectionClass::DomainParticipation => config_domain_fails(&m.config, inputs),
|
|
||||||
};
|
|
||||||
if rejected {
|
|
||||||
killed += 1;
|
|
||||||
} else {
|
|
||||||
survivors.push((
|
survivors.push((
|
||||||
m.id,
|
m.id,
|
||||||
format!(
|
format!("mutant {} ({}) passed all acceptance gates", m.id, m.name),
|
||||||
"mutant {} ({}) not rejected by the real acceptance gate {}",
|
|
||||||
m.id,
|
|
||||||
m.name,
|
|
||||||
m.expected.name()
|
|
||||||
),
|
|
||||||
));
|
));
|
||||||
|
} else {
|
||||||
|
killed += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MutationOutcome { total: mutants.len(), killed, survivors }
|
MutationOutcome { total: mutants.len(), killed, survivors }
|
||||||
@@ -476,50 +496,11 @@ fn compressibility(features: &[i64]) -> f64 {
|
|||||||
(1.0 - h / 8.0).clamp(0.0, 1.0)
|
(1.0 - h / 8.0).clamp(0.0, 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Real serialized-trace feature row used by the collapse analysis. Layout per
|
/// Trace feature row used by the collapse analysis — the single definition lives
|
||||||
/// domain block (width 9): `[infl_out, infl_in, flow_out, flow_in, read, write,
|
/// in `collapse_analysis::trace_feature_row` so the attestor can recompute the
|
||||||
/// temporal, obs_delta, hid_delta]`, followed by globals `[causal_rank,
|
/// same row from the retained full trace and prove the summary derives from it.
|
||||||
/// edge_count, touched, divergence_mean]`. This is genuine trace structure, not
|
|
||||||
/// a hash-derived proxy.
|
|
||||||
fn trace_feature_row(r: &ResolutionResult) -> Vec<f64> {
|
fn trace_feature_row(r: &ResolutionResult) -> Vec<f64> {
|
||||||
let infl = r.trace.causal_graph.influence_matrix();
|
collapse_analysis::trace_feature_row(&r.trace, &r.delta)
|
||||||
let mut flow = [[0.0f64; NUM_DOMAINS]; NUM_DOMAINS];
|
|
||||||
for &(a, b, bits) in &r.trace.information_flow.edges {
|
|
||||||
flow[a as usize % NUM_DOMAINS][b as usize % NUM_DOMAINS] += bits as f64;
|
|
||||||
}
|
|
||||||
let mut temporal = [0.0f64; NUM_DOMAINS];
|
|
||||||
for &(_s, _off, d) in &r.trace.temporal_graph.edges {
|
|
||||||
temporal[d as usize % NUM_DOMAINS] += 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut row = Vec::with_capacity(FEATURE_W);
|
|
||||||
for d in 0..NUM_DOMAINS {
|
|
||||||
let infl_out: f64 = (0..NUM_DOMAINS).map(|j| infl[d][j]).sum();
|
|
||||||
let infl_in: f64 = (0..NUM_DOMAINS).map(|i| infl[i][d]).sum();
|
|
||||||
let flow_out: f64 = (0..NUM_DOMAINS).map(|j| flow[d][j]).sum();
|
|
||||||
let flow_in: f64 = (0..NUM_DOMAINS).map(|i| flow[i][d]).sum();
|
|
||||||
let read = r.trace.read_graph.access_count[d] as f64;
|
|
||||||
let write = r.trace.write_graph.access_count[d] as f64;
|
|
||||||
let temp = temporal[d];
|
|
||||||
let obs_delta: f64 = r.delta.domain_deltas[d]
|
|
||||||
.observed
|
|
||||||
.iter()
|
|
||||||
.map(|&v| (v as f64).abs())
|
|
||||||
.sum();
|
|
||||||
let hid_delta: f64 = r.delta.domain_deltas[d]
|
|
||||||
.hidden
|
|
||||||
.iter()
|
|
||||||
.map(|&v| (v as f64).abs())
|
|
||||||
.sum();
|
|
||||||
row.extend_from_slice(&[
|
|
||||||
infl_out, infl_in, flow_out, flow_in, read, write, temp, obs_delta, hid_delta,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
row.push(r.trace.causal_rank() as f64);
|
|
||||||
row.push(r.trace.causal_edge_count() as f64);
|
|
||||||
row.push(r.trace.touched_domain_count() as f64);
|
|
||||||
row.push(r.trace.context_divergence());
|
|
||||||
row
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Outcome of checking a perturbation's **declared** metamorphic expectation
|
/// Outcome of checking a perturbation's **declared** metamorphic expectation
|
||||||
@@ -552,6 +533,8 @@ pub fn metamorphic_outcome(
|
|||||||
if exp.expect_trace_change && perturbed_domain_read {
|
if exp.expect_trace_change && perturbed_domain_read {
|
||||||
if trace_changed {
|
if trace_changed {
|
||||||
ExpectationOutcome::Upheld
|
ExpectationOutcome::Upheld
|
||||||
|
} else if exp.neutral_explanation.is_some() {
|
||||||
|
ExpectationOutcome::ExplainedNeutral
|
||||||
} else {
|
} else {
|
||||||
ExpectationOutcome::Violation
|
ExpectationOutcome::Violation
|
||||||
}
|
}
|
||||||
@@ -562,6 +545,13 @@ pub fn metamorphic_outcome(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Consumed-perturbation aggregate thresholds for delta/future. Per-case strict
|
||||||
|
/// enforcement of delta/future is unsound (clamping and masking legitimately
|
||||||
|
/// leave them unchanged), so these are enforced as fractions over the consumed
|
||||||
|
/// perturbations. Reference rates measured at ~0.77 (delta) and ~0.90 (future).
|
||||||
|
pub const CONSUMED_DELTA_MIN: f64 = 0.65;
|
||||||
|
pub const CONSUMED_FUTURE_MIN: f64 = 0.80;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Result aggregates.
|
// Result aggregates.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -611,6 +601,9 @@ pub struct MetamorphicGates {
|
|||||||
/// Count of perturbations the program actually consumed (gate is vacuous
|
/// Count of perturbations the program actually consumed (gate is vacuous
|
||||||
/// without these).
|
/// without these).
|
||||||
pub consumed: usize,
|
pub consumed: usize,
|
||||||
|
/// Fraction of consumed perturbations that changed the delta / the future.
|
||||||
|
pub consumed_delta_rate: f64,
|
||||||
|
pub consumed_future_rate: f64,
|
||||||
pub failures: Vec<String>,
|
pub failures: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,6 +658,10 @@ pub struct CiResults {
|
|||||||
/// replay-record leaf, and checks the leaf is in `merkle_leaves` — proving
|
/// replay-record leaf, and checks the leaf is in `merkle_leaves` — proving
|
||||||
/// the leaves are backed by full trace structure, not a summary.
|
/// the leaves are backed by full trace structure, not a summary.
|
||||||
pub trace_evidence: Vec<TraceEvidence>,
|
pub trace_evidence: Vec<TraceEvidence>,
|
||||||
|
/// The collapse feature rows, in the same order as the first base executions
|
||||||
|
/// in the trace evidence, so the attestor can recompute each from the full
|
||||||
|
/// trace and prove the summary derives from it (finding 4).
|
||||||
|
pub collapse_feature_rows: Vec<Vec<f64>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One sampled full-trace evidence record.
|
/// One sampled full-trace evidence record.
|
||||||
@@ -755,6 +752,8 @@ pub fn run_all_to(scale: Scale, mut evidence_sink: Option<&mut dyn Write>) -> Ci
|
|||||||
let mut meta_expect_violation = 0usize;
|
let mut meta_expect_violation = 0usize;
|
||||||
let mut meta_explained_neutral = 0usize;
|
let mut meta_explained_neutral = 0usize;
|
||||||
let mut meta_consumed = 0usize;
|
let mut meta_consumed = 0usize;
|
||||||
|
let mut meta_consumed_delta = 0usize;
|
||||||
|
let mut meta_consumed_future = 0usize;
|
||||||
let mut min_perturbations = usize::MAX;
|
let mut min_perturbations = usize::MAX;
|
||||||
|
|
||||||
let mut contract_pass = 0usize;
|
let mut contract_pass = 0usize;
|
||||||
@@ -814,11 +813,13 @@ pub fn run_all_to(scale: Scale, mut evidence_sink: Option<&mut dyn Write>) -> Ci
|
|||||||
let mut c_expect_violation = 0usize;
|
let mut c_expect_violation = 0usize;
|
||||||
let mut c_explained_neutral = 0usize;
|
let mut c_explained_neutral = 0usize;
|
||||||
let mut c_consumed = 0usize;
|
let mut c_consumed = 0usize;
|
||||||
|
let mut c_consumed_delta = 0usize;
|
||||||
|
let mut c_consumed_future = 0usize;
|
||||||
let mut c_pert = 0usize;
|
let mut c_pert = 0usize;
|
||||||
// Capture each perturbation execution so the committed case can run
|
// Capture each perturbation execution so the committed case can run
|
||||||
// the full 100% reference/runtime comparison without recomputing the
|
// the full 100% reference/runtime comparison without recomputing the
|
||||||
// reference side.
|
// reference side.
|
||||||
let mut pert_execs: Vec<(ResolutionInput, Canonical, Hash)> = Vec::new();
|
let mut pert_execs: Vec<(ResolutionInput, Canonical, Hash, String)> = Vec::new();
|
||||||
for pc in &case.perturbations {
|
for pc in &case.perturbations {
|
||||||
let pinput = input_with_world(&case, pc.world.clone());
|
let pinput = input_with_world(&case, pc.world.clone());
|
||||||
let pr = execute(&cfg, &pinput);
|
let pr = execute(&cfg, &pinput);
|
||||||
@@ -842,13 +843,32 @@ pub fn run_all_to(scale: Scale, mut evidence_sink: Option<&mut dyn Write>) -> Ci
|
|||||||
r.trace.read_graph.access_count[pc.target_domain % NUM_DOMAINS] > 0;
|
r.trace.read_graph.access_count[pc.target_domain % NUM_DOMAINS] > 0;
|
||||||
if perturbed_read {
|
if perturbed_read {
|
||||||
c_consumed += 1;
|
c_consumed += 1;
|
||||||
|
if ad {
|
||||||
|
c_consumed_delta += 1;
|
||||||
|
}
|
||||||
|
if af {
|
||||||
|
c_consumed_future += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
match metamorphic_outcome(&pc.expectation, perturbed_read, at) {
|
match metamorphic_outcome(&pc.expectation, perturbed_read, at) {
|
||||||
ExpectationOutcome::Upheld => {}
|
ExpectationOutcome::Upheld => {}
|
||||||
ExpectationOutcome::ExplainedNeutral => c_explained_neutral += 1,
|
ExpectationOutcome::ExplainedNeutral => c_explained_neutral += 1,
|
||||||
ExpectationOutcome::Violation => c_expect_violation += 1,
|
ExpectationOutcome::Violation => c_expect_violation += 1,
|
||||||
}
|
}
|
||||||
pert_execs.push((pinput, canonical(&pr), pr.replay.hash()));
|
// Full-trace evidence line for THIS perturbation leaf (finding 2):
|
||||||
|
// every leaf, base and perturbation, carries a recomputable trace.
|
||||||
|
let pline = format!(
|
||||||
|
"p {:016x} {:016x} {:016x} {:016x} {:016x} {:016x}\t{}\t{}",
|
||||||
|
pr.replay.world_seed,
|
||||||
|
pr.replay.program_seed,
|
||||||
|
pr.replay.contract_seed,
|
||||||
|
pr.replay.perturbation_seed,
|
||||||
|
pr.replay.future_hash.0,
|
||||||
|
pr.replay.hash().0,
|
||||||
|
pr.trace.serialize(),
|
||||||
|
pr.delta.serialize(),
|
||||||
|
);
|
||||||
|
pert_execs.push((pinput, canonical(&pr), pr.replay.hash(), pline));
|
||||||
}
|
}
|
||||||
let future_sensitivity = if c_pert > 0 {
|
let future_sensitivity = if c_pert > 0 {
|
||||||
c_alt_future as f64 / c_pert as f64
|
c_alt_future as f64 / c_pert as f64
|
||||||
@@ -901,7 +921,7 @@ pub fn run_all_to(scale: Scale, mut evidence_sink: Option<&mut dyn Write>) -> Ci
|
|||||||
if let Some(w) = evidence_sink.as_deref_mut() {
|
if let Some(w) = evidence_sink.as_deref_mut() {
|
||||||
let _ = writeln!(
|
let _ = writeln!(
|
||||||
w,
|
w,
|
||||||
"{:016x} {:016x} {:016x} {:016x} {:016x} {:016x}\t{}\t{}",
|
"b {:016x} {:016x} {:016x} {:016x} {:016x} {:016x}\t{}\t{}",
|
||||||
r.replay.world_seed,
|
r.replay.world_seed,
|
||||||
r.replay.program_seed,
|
r.replay.program_seed,
|
||||||
r.replay.contract_seed,
|
r.replay.contract_seed,
|
||||||
@@ -919,7 +939,7 @@ pub fn run_all_to(scale: Scale, mut evidence_sink: Option<&mut dyn Write>) -> Ci
|
|||||||
delta: r.delta.clone(),
|
delta: r.delta.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (pinput, pref_canon, pleaf) in &pert_execs {
|
for (pinput, pref_canon, pleaf, pline) in &pert_execs {
|
||||||
let prut = rut.resolve(pinput.clone());
|
let prut = rut.resolve(pinput.clone());
|
||||||
equiv_total += 1;
|
equiv_total += 1;
|
||||||
if *pref_canon == canonical(&prut) {
|
if *pref_canon == canonical(&prut) {
|
||||||
@@ -929,6 +949,9 @@ pub fn run_all_to(scale: Scale, mut evidence_sink: Option<&mut dyn Write>) -> Ci
|
|||||||
.push(format!("case {} (perturbation) reference != runtime_under_test", i));
|
.push(format!("case {} (perturbation) reference != runtime_under_test", i));
|
||||||
}
|
}
|
||||||
merkle_leaves.push(*pleaf);
|
merkle_leaves.push(*pleaf);
|
||||||
|
if let Some(w) = evidence_sink.as_deref_mut() {
|
||||||
|
let _ = writeln!(w, "{}", pline);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actual_executions += 1;
|
actual_executions += 1;
|
||||||
@@ -964,6 +987,8 @@ pub fn run_all_to(scale: Scale, mut evidence_sink: Option<&mut dyn Write>) -> Ci
|
|||||||
meta_expect_violation += c_expect_violation;
|
meta_expect_violation += c_expect_violation;
|
||||||
meta_explained_neutral += c_explained_neutral;
|
meta_explained_neutral += c_explained_neutral;
|
||||||
meta_consumed += c_consumed;
|
meta_consumed += c_consumed;
|
||||||
|
meta_consumed_delta += c_consumed_delta;
|
||||||
|
meta_consumed_future += c_consumed_future;
|
||||||
perturbation_runs += c_pert;
|
perturbation_runs += c_pert;
|
||||||
min_perturbations = min_perturbations.min(c_pert);
|
min_perturbations = min_perturbations.min(c_pert);
|
||||||
|
|
||||||
@@ -1094,11 +1119,27 @@ pub fn run_all_to(scale: Scale, mut evidence_sink: Option<&mut dyn Write>) -> Ci
|
|||||||
r_violation
|
r_violation
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
// Non-vacuity: the relation must actually be exercised — there must be
|
|
||||||
// perturbations the program consumed for the enforcement to mean anything.
|
|
||||||
if meta_total > 0 && meta_consumed == 0 {
|
if meta_total > 0 && meta_consumed == 0 {
|
||||||
meta_failures.push("metamorphic enforcement vacuous: no consumed perturbations".into());
|
meta_failures.push("metamorphic enforcement vacuous: no consumed perturbations".into());
|
||||||
}
|
}
|
||||||
|
// Delta and future expectations: per-case strict enforcement is unsound
|
||||||
|
// (clamping/masking), so enforce them as fractions over consumed
|
||||||
|
// perturbations.
|
||||||
|
let mc = meta_consumed.max(1) as f64;
|
||||||
|
let consumed_delta_rate = meta_consumed_delta as f64 / mc;
|
||||||
|
let consumed_future_rate = meta_consumed_future as f64 / mc;
|
||||||
|
if consumed_delta_rate < CONSUMED_DELTA_MIN {
|
||||||
|
meta_failures.push(format!(
|
||||||
|
"consumed-perturbation delta-change rate {:.4} < {}",
|
||||||
|
consumed_delta_rate, CONSUMED_DELTA_MIN
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if consumed_future_rate < CONSUMED_FUTURE_MIN {
|
||||||
|
meta_failures.push(format!(
|
||||||
|
"consumed-perturbation future-change rate {:.4} < {}",
|
||||||
|
consumed_future_rate, CONSUMED_FUTURE_MIN
|
||||||
|
));
|
||||||
|
}
|
||||||
let metamorphic = MetamorphicGates {
|
let metamorphic = MetamorphicGates {
|
||||||
total: meta_total,
|
total: meta_total,
|
||||||
altered_trace: r_trace,
|
altered_trace: r_trace,
|
||||||
@@ -1107,6 +1148,8 @@ pub fn run_all_to(scale: Scale, mut evidence_sink: Option<&mut dyn Write>) -> Ci
|
|||||||
expectation_violations: r_violation,
|
expectation_violations: r_violation,
|
||||||
explained_neutral: r_explained,
|
explained_neutral: r_explained,
|
||||||
consumed: meta_consumed,
|
consumed: meta_consumed,
|
||||||
|
consumed_delta_rate,
|
||||||
|
consumed_future_rate,
|
||||||
failures: meta_failures,
|
failures: meta_failures,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1116,7 +1159,7 @@ pub fn run_all_to(scale: Scale, mut evidence_sink: Option<&mut dyn Write>) -> Ci
|
|||||||
|
|
||||||
// ---- Collapse gates (real trace information) ----
|
// ---- Collapse gates (real trace information) ----
|
||||||
progress!("collapse analysis (11 attacks over real trace features)...");
|
progress!("collapse analysis (11 attacks over real trace features)...");
|
||||||
let corpus = BehaviorCorpus::build(collapse_rows);
|
let corpus = BehaviorCorpus::build(collapse_rows.clone());
|
||||||
let collapse = analyze(&corpus);
|
let collapse = analyze(&corpus);
|
||||||
|
|
||||||
// ---- Mutation gates (killed by named gate) ----
|
// ---- Mutation gates (killed by named gate) ----
|
||||||
@@ -1262,6 +1305,7 @@ pub fn run_all_to(scale: Scale, mut evidence_sink: Option<&mut dyn Write>) -> Ci
|
|||||||
coverage,
|
coverage,
|
||||||
merkle_leaves: retained_leaves,
|
merkle_leaves: retained_leaves,
|
||||||
trace_evidence,
|
trace_evidence,
|
||||||
|
collapse_feature_rows: collapse_rows,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1279,6 +1323,31 @@ pub struct CausalExplanationGate {
|
|||||||
pub edges_confirmed: usize,
|
pub edges_confirmed: usize,
|
||||||
pub confirmed_fraction: f64,
|
pub confirmed_fraction: f64,
|
||||||
pub failures: Vec<String>,
|
pub failures: Vec<String>,
|
||||||
|
/// Per-edge intervention records, sufficient to independently recompute each
|
||||||
|
/// confirmation (finding 7).
|
||||||
|
pub evidence: Vec<CausalEvidenceRecord>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One recomputable per-edge intervention record: regenerate the case from
|
||||||
|
/// `case_seed`, perturb the recorded source lane, and the destination lane's
|
||||||
|
/// delta must move from `base_dv` to `alt_dv` (`confirmed = base_dv != alt_dv`).
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct CausalEvidenceRecord {
|
||||||
|
pub case_seed: u64,
|
||||||
|
pub from_domain: u8,
|
||||||
|
pub from_lane: u8,
|
||||||
|
pub from_hidden: bool,
|
||||||
|
pub to_domain: u8,
|
||||||
|
pub to_lane: u8,
|
||||||
|
pub to_hidden: bool,
|
||||||
|
pub base_dv: i64,
|
||||||
|
pub alt_dv: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CausalEvidenceRecord {
|
||||||
|
pub fn confirmed(&self) -> bool {
|
||||||
|
self.base_dv != self.alt_dv
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Minimum fraction of recorded causal edges that must be intervention-confirmed.
|
/// Minimum fraction of recorded causal edges that must be intervention-confirmed.
|
||||||
@@ -1305,11 +1374,13 @@ pub fn causal_confirmation(
|
|||||||
cases: usize,
|
cases: usize,
|
||||||
edges_per_case: usize,
|
edges_per_case: usize,
|
||||||
scramble: bool,
|
scramble: bool,
|
||||||
) -> (usize, usize) {
|
) -> (usize, usize, Vec<CausalEvidenceRecord>) {
|
||||||
let mut tested = 0usize;
|
let mut tested = 0usize;
|
||||||
let mut confirmed = 0usize;
|
let mut confirmed = 0usize;
|
||||||
|
let mut records = Vec::new();
|
||||||
for i in 0..cases {
|
for i in 0..cases {
|
||||||
let (case, _) = generate_accepted_case(case_seed(i));
|
let seed = case_seed(i);
|
||||||
|
let (case, _) = generate_accepted_case(seed);
|
||||||
let input = input_from_case(&case);
|
let input = input_from_case(&case);
|
||||||
let base = execute(cfg, &input);
|
let base = execute(cfg, &input);
|
||||||
let edges = &base.trace.causal_graph.edges;
|
let edges = &base.trace.causal_graph.edges;
|
||||||
@@ -1340,9 +1411,22 @@ pub fn causal_confirmation(
|
|||||||
if base_dv != alt_dv {
|
if base_dv != alt_dv {
|
||||||
confirmed += 1;
|
confirmed += 1;
|
||||||
}
|
}
|
||||||
|
if !scramble {
|
||||||
|
records.push(CausalEvidenceRecord {
|
||||||
|
case_seed: seed,
|
||||||
|
from_domain: sd as u8,
|
||||||
|
from_lane: sl as u8,
|
||||||
|
from_hidden: shidden,
|
||||||
|
to_domain: dd as u8,
|
||||||
|
to_lane: e.to.lane,
|
||||||
|
to_hidden: e.to.hidden,
|
||||||
|
base_dv,
|
||||||
|
alt_dv,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(tested, confirmed)
|
}
|
||||||
|
(tested, confirmed, records)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn causal_explanation_gate(
|
pub fn causal_explanation_gate(
|
||||||
@@ -1350,7 +1434,7 @@ pub fn causal_explanation_gate(
|
|||||||
cases: usize,
|
cases: usize,
|
||||||
edges_per_case: usize,
|
edges_per_case: usize,
|
||||||
) -> CausalExplanationGate {
|
) -> CausalExplanationGate {
|
||||||
let (tested, confirmed) = causal_confirmation(cfg, cases, edges_per_case, false);
|
let (tested, confirmed, evidence) = causal_confirmation(cfg, cases, edges_per_case, false);
|
||||||
let frac = if tested > 0 { confirmed as f64 / tested as f64 } else { 0.0 };
|
let frac = if tested > 0 { confirmed as f64 / tested as f64 } else { 0.0 };
|
||||||
let mut failures = Vec::new();
|
let mut failures = Vec::new();
|
||||||
if tested < CAUSAL_CONFIRM_SAMPLE_MIN {
|
if tested < CAUSAL_CONFIRM_SAMPLE_MIN {
|
||||||
@@ -1370,6 +1454,7 @@ pub fn causal_explanation_gate(
|
|||||||
edges_confirmed: confirmed,
|
edges_confirmed: confirmed,
|
||||||
confirmed_fraction: frac,
|
confirmed_fraction: frac,
|
||||||
failures,
|
failures,
|
||||||
|
evidence,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1634,23 +1719,21 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn consumed_perturbation_must_alter_trace() {
|
fn consumed_perturbation_must_alter_trace() {
|
||||||
let active = TraceDifferenceExpectation::active();
|
let active = TraceDifferenceExpectation::active();
|
||||||
// Program consumed the perturbed domain but the trace did not change:
|
assert_eq!(metamorphic_outcome(&active, true, true), ExpectationOutcome::Upheld);
|
||||||
// a hard metamorphic violation.
|
// Consumed but trace did not change, not permitted neutral: violation.
|
||||||
assert_eq!(
|
assert_eq!(metamorphic_outcome(&active, true, false), ExpectationOutcome::Violation);
|
||||||
metamorphic_outcome(&active, true, false),
|
// Not consumed and nothing changed: legitimately neutral.
|
||||||
ExpectationOutcome::Violation
|
assert_eq!(metamorphic_outcome(&active, false, false), ExpectationOutcome::ExplainedNeutral);
|
||||||
);
|
}
|
||||||
// Consumed and the trace changed: upheld.
|
|
||||||
assert_eq!(
|
/// Delta and future expectations are enforced as consumed-aggregate rates
|
||||||
metamorphic_outcome(&active, true, true),
|
/// (per-case is unsound). A run whose consumed perturbations rarely change
|
||||||
ExpectationOutcome::Upheld
|
/// the delta or future must fail; the reference run is well above both.
|
||||||
);
|
#[test]
|
||||||
// Not consumed and nothing changed: legitimately neutral, not a
|
fn metamorphic_delta_future_rates_enforced() {
|
||||||
// violation (the program cannot react to input it never reads).
|
let r = run_all(Scale::tiny());
|
||||||
assert_eq!(
|
assert!(r.metamorphic.consumed_delta_rate >= CONSUMED_DELTA_MIN, "delta rate {}", r.metamorphic.consumed_delta_rate);
|
||||||
metamorphic_outcome(&active, false, false),
|
assert!(r.metamorphic.consumed_future_rate >= CONSUMED_FUTURE_MIN, "future rate {}", r.metamorphic.consumed_future_rate);
|
||||||
ExpectationOutcome::ExplainedNeutral
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finding 10: the causal-explanation gate measures real cause→effect, not
|
/// Finding 10: the causal-explanation gate measures real cause→effect, not
|
||||||
@@ -1666,7 +1749,7 @@ mod tests {
|
|||||||
assert!(gate.failures.is_empty(), "reference fails causal gate: {:?}", gate.failures);
|
assert!(gate.failures.is_empty(), "reference fails causal gate: {:?}", gate.failures);
|
||||||
assert!(gate.confirmed_fraction >= CAUSAL_CONFIRM_MIN);
|
assert!(gate.confirmed_fraction >= CAUSAL_CONFIRM_MIN);
|
||||||
|
|
||||||
let (t, c) = causal_confirmation(&cfg, 120, 8, true);
|
let (t, c, _) = causal_confirmation(&cfg, 120, 8, true);
|
||||||
let scrambled = c as f64 / t.max(1) as f64;
|
let scrambled = c as f64 / t.max(1) as f64;
|
||||||
assert!(
|
assert!(
|
||||||
scrambled < CAUSAL_CONFIRM_MIN,
|
scrambled < CAUSAL_CONFIRM_MIN,
|
||||||
@@ -1679,6 +1762,32 @@ mod tests {
|
|||||||
gate.confirmed_fraction,
|
gate.confirmed_fraction,
|
||||||
scrambled
|
scrambled
|
||||||
);
|
);
|
||||||
|
// Per-edge evidence is retained and independently recomputable: replay
|
||||||
|
// each record from its seed and confirm base_dv/alt_dv reproduce.
|
||||||
|
assert!(!gate.evidence.is_empty());
|
||||||
|
let mut rechecked = 0;
|
||||||
|
for rec in gate.evidence.iter().take(64) {
|
||||||
|
let (case, _) = generate_accepted_case(rec.case_seed);
|
||||||
|
let input = input_from_case(&case);
|
||||||
|
let base = execute(&cfg, &input);
|
||||||
|
let mut w = input.world.clone();
|
||||||
|
if rec.from_hidden {
|
||||||
|
let l = rec.from_lane as usize % HIDDEN_LANES;
|
||||||
|
w.domains[rec.from_domain as usize].hidden[l] =
|
||||||
|
w.domains[rec.from_domain as usize].hidden[l].wrapping_add(0x9_27c1);
|
||||||
|
} else {
|
||||||
|
let l = rec.from_lane as usize % LANES;
|
||||||
|
w.domains[rec.from_domain as usize].observed[l] =
|
||||||
|
w.domains[rec.from_domain as usize].observed[l].wrapping_add(0x9_27c1);
|
||||||
|
}
|
||||||
|
let alt = execute(&cfg, &input_with_world(&case, w));
|
||||||
|
let bdv = lane_delta(&base.delta.domain_deltas[rec.to_domain as usize], rec.to_lane as usize, rec.to_hidden);
|
||||||
|
let adv = lane_delta(&alt.delta.domain_deltas[rec.to_domain as usize], rec.to_lane as usize, rec.to_hidden);
|
||||||
|
assert_eq!(bdv, rec.base_dv, "retained base_dv not recomputable");
|
||||||
|
assert_eq!(adv, rec.alt_dv, "retained alt_dv not recomputable");
|
||||||
|
rechecked += 1;
|
||||||
|
}
|
||||||
|
assert!(rechecked >= 64);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finding 5: mutants are killed by the SAME acceptance-gate predicates the
|
/// Finding 5: mutants are killed by the SAME acceptance-gate predicates the
|
||||||
@@ -1687,52 +1796,15 @@ mod tests {
|
|||||||
/// real predicate it targets.
|
/// real predicate it targets.
|
||||||
#[test]
|
#[test]
|
||||||
fn mutants_killed_by_real_acceptance_gates() {
|
fn mutants_killed_by_real_acceptance_gates() {
|
||||||
use rune_ir::{RuneProgram, RuneToken, ALL_OPS};
|
// The reference passes the FULL engine-behavior acceptance gates over a
|
||||||
use world_model::{standard_executors, ProgramId, Rng, WorldId, WorldSnapshot};
|
// real generated-case corpus (not 64 fixed inputs).
|
||||||
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..LANES {
|
|
||||||
d.observed[l] = rng.range_i64(-5000, 5000);
|
|
||||||
}
|
|
||||||
for l in 0..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..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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let inputs: Vec<ResolutionInput> = (0..16).map(|s| rich_input(s + 1)).collect();
|
|
||||||
// Reference passes every per-config acceptance predicate.
|
|
||||||
let refcfg = EngineConfig::reference();
|
let refcfg = EngineConfig::reference();
|
||||||
let rc: Vec<Canonical> = inputs.iter().map(|i| canonical(&execute(&refcfg, i))).collect();
|
assert!(
|
||||||
let rf: Vec<Hash> = inputs.iter().map(|i| execute(&refcfg, i).replay.future_hash).collect();
|
engine_acceptance(&refcfg, MUTATION_GATE_CASES).is_empty(),
|
||||||
assert!(!config_causal_fails(&refcfg, &inputs));
|
"reference fails an acceptance gate"
|
||||||
assert!(!config_domain_fails(&refcfg, &inputs));
|
);
|
||||||
assert!(!config_temporal_fails(&refcfg, &inputs, &rf));
|
// Every mutant is rejected by those same full gates.
|
||||||
assert!(!config_equivalence_fails(&refcfg, &inputs, &rc));
|
let outcome = evaluate_mutants(520, &[]);
|
||||||
// Every mutant is rejected by the real acceptance gate it targets.
|
|
||||||
let outcome = evaluate_mutants(520, &inputs);
|
|
||||||
assert!(outcome.passed(), "survivors: {:?}", outcome.survivors);
|
assert!(outcome.passed(), "survivors: {:?}", outcome.survivors);
|
||||||
assert_eq!(outcome.killed, outcome.total);
|
assert_eq!(outcome.killed, outcome.total);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,58 @@ fn write_evidence(dir: &Path, r: &CiResults) {
|
|||||||
r.provenance.engines_agree,
|
r.provenance.engines_agree,
|
||||||
);
|
);
|
||||||
fs::write(ev.join("claims.tsv"), claims).expect("write claims");
|
fs::write(ev.join("claims.tsv"), claims).expect("write claims");
|
||||||
|
|
||||||
|
// Per-edge causal intervention evidence (finding 7): one recomputable record
|
||||||
|
// per tested edge.
|
||||||
|
let mut causal = String::from(
|
||||||
|
"seed\tfrom_domain\tfrom_lane\tfrom_hidden\tto_domain\tto_lane\tto_hidden\tbase_dv\talt_dv\n",
|
||||||
|
);
|
||||||
|
for rec in &r.causal_explanation.evidence {
|
||||||
|
causal.push_str(&format!(
|
||||||
|
"{:016x}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\n",
|
||||||
|
rec.case_seed,
|
||||||
|
rec.from_domain,
|
||||||
|
rec.from_lane,
|
||||||
|
rec.from_hidden as u8,
|
||||||
|
rec.to_domain,
|
||||||
|
rec.to_lane,
|
||||||
|
rec.to_hidden as u8,
|
||||||
|
rec.base_dv,
|
||||||
|
rec.alt_dv,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
fs::write(ev.join("causal_evidence.tsv"), causal).expect("write causal evidence");
|
||||||
|
|
||||||
|
// Collapse feature rows (finding 4): the summaries the collapse gate consumes,
|
||||||
|
// in the same order as the first base trace records. Stored as f64 bits so an
|
||||||
|
// attestor can recompute each row from the full trace and compare bit-exact.
|
||||||
|
let mut crows = String::new();
|
||||||
|
for row in &r.collapse_feature_rows {
|
||||||
|
let cells: Vec<String> = row.iter().map(|v| v.to_bits().to_string()).collect();
|
||||||
|
crows.push_str(&cells.join(" "));
|
||||||
|
crows.push('\n');
|
||||||
|
}
|
||||||
|
fs::write(ev.join("collapse_feature_rows.tsv"), crows).expect("write collapse rows");
|
||||||
|
|
||||||
|
// Artifact bundle manifest (finding 1): the merge-scale claim is only valid
|
||||||
|
// if this complete, content-hashed bundle is retained. The attestor verifies
|
||||||
|
// every listed file exists and its hash + length match.
|
||||||
|
let bundle = [
|
||||||
|
"leaves.tsv",
|
||||||
|
"claims.tsv",
|
||||||
|
"causal_evidence.tsv",
|
||||||
|
"collapse_feature_rows.tsv",
|
||||||
|
"traces.tsv",
|
||||||
|
];
|
||||||
|
let mut manifest = String::from("file\thash\tbytes\n");
|
||||||
|
for name in bundle {
|
||||||
|
let bytes = fs::read(ev.join(name)).unwrap_or_default();
|
||||||
|
let mut h = world_model::Hasher::new();
|
||||||
|
h.write_tag("evidence-file");
|
||||||
|
h.write_bytes(&bytes);
|
||||||
|
manifest.push_str(&format!("{}\t{:016x}\t{}\n", name, h.finish().0, bytes.len()));
|
||||||
|
}
|
||||||
|
fs::write(ev.join("MANIFEST.tsv"), manifest).expect("write manifest");
|
||||||
// Note: evidence/traces.tsv (the FULL per-execution trace corpus) is streamed
|
// Note: evidence/traces.tsv (the FULL per-execution trace corpus) is streamed
|
||||||
// during the run in main(), covering 100% of base executions — not written
|
// during the run in main(), covering 100% of base executions — not written
|
||||||
// here from a capped sample.
|
// here from a capped sample.
|
||||||
@@ -147,6 +199,8 @@ fn build_reports(dir: &Path, r: &CiResults) {
|
|||||||
("altered_delta".into(), Json::Num(r.metamorphic.altered_delta)),
|
("altered_delta".into(), Json::Num(r.metamorphic.altered_delta)),
|
||||||
("altered_future".into(), Json::Num(r.metamorphic.altered_future)),
|
("altered_future".into(), Json::Num(r.metamorphic.altered_future)),
|
||||||
("consumed_perturbation_trace_violations".into(), Json::Num(r.metamorphic.expectation_violations)),
|
("consumed_perturbation_trace_violations".into(), Json::Num(r.metamorphic.expectation_violations)),
|
||||||
|
("consumed_delta_change_rate".into(), Json::Num(r.metamorphic.consumed_delta_rate)),
|
||||||
|
("consumed_future_change_rate".into(), Json::Num(r.metamorphic.consumed_future_rate)),
|
||||||
("legitimately_neutral".into(), Json::Num(r.metamorphic.explained_neutral)),
|
("legitimately_neutral".into(), Json::Num(r.metamorphic.explained_neutral)),
|
||||||
("consumed_perturbations".into(), Json::Int(r.metamorphic.consumed as i64)),
|
("consumed_perturbations".into(), Json::Int(r.metamorphic.consumed as i64)),
|
||||||
("failures".into(), fails(&r.metamorphic.failures)),
|
("failures".into(), fails(&r.metamorphic.failures)),
|
||||||
|
|||||||
@@ -21,7 +21,46 @@
|
|||||||
pub mod linalg;
|
pub mod linalg;
|
||||||
|
|
||||||
use linalg::{ols_r2, pca_scores, Mat};
|
use linalg::{ols_r2, pca_scores, Mat};
|
||||||
use world_model::NUM_DOMAINS;
|
use trace_model::ExecutionTrace;
|
||||||
|
use world_model::{WorldDelta, NUM_DOMAINS};
|
||||||
|
|
||||||
|
/// The single definition of a trace feature row. The collapse corpus is built
|
||||||
|
/// from these rows; an attestor recomputes the same row from the retained FULL
|
||||||
|
/// trace and delta and checks equality, which is how the summary is proven to
|
||||||
|
/// derive from the full trace. Layout per domain block (`BLOCK_W`):
|
||||||
|
/// `[infl_out, infl_in, flow_out, flow_in, read, write, temporal, obs_delta,
|
||||||
|
/// hid_delta]`, then globals `[causal_rank, edge_count, touched, divergence]`.
|
||||||
|
pub fn trace_feature_row(trace: &ExecutionTrace, delta: &WorldDelta) -> Vec<f64> {
|
||||||
|
let infl = trace.causal_graph.influence_matrix();
|
||||||
|
let mut flow = [[0.0f64; NUM_DOMAINS]; NUM_DOMAINS];
|
||||||
|
for &(a, b, bits) in &trace.information_flow.edges {
|
||||||
|
flow[a as usize % NUM_DOMAINS][b as usize % NUM_DOMAINS] += bits as f64;
|
||||||
|
}
|
||||||
|
let mut temporal = [0.0f64; NUM_DOMAINS];
|
||||||
|
for &(_s, _off, d) in &trace.temporal_graph.edges {
|
||||||
|
temporal[d as usize % NUM_DOMAINS] += 1.0;
|
||||||
|
}
|
||||||
|
let mut row = Vec::with_capacity(FEATURE_W);
|
||||||
|
for d in 0..NUM_DOMAINS {
|
||||||
|
let infl_out: f64 = (0..NUM_DOMAINS).map(|j| infl[d][j]).sum();
|
||||||
|
let infl_in: f64 = (0..NUM_DOMAINS).map(|i| infl[i][d]).sum();
|
||||||
|
let flow_out: f64 = (0..NUM_DOMAINS).map(|j| flow[d][j]).sum();
|
||||||
|
let flow_in: f64 = (0..NUM_DOMAINS).map(|i| flow[i][d]).sum();
|
||||||
|
let read = trace.read_graph.access_count[d] as f64;
|
||||||
|
let write = trace.write_graph.access_count[d] as f64;
|
||||||
|
let temp = temporal[d];
|
||||||
|
let obs_delta: f64 = delta.domain_deltas[d].observed.iter().map(|&v| (v as f64).abs()).sum();
|
||||||
|
let hid_delta: f64 = delta.domain_deltas[d].hidden.iter().map(|&v| (v as f64).abs()).sum();
|
||||||
|
row.extend_from_slice(&[
|
||||||
|
infl_out, infl_in, flow_out, flow_in, read, write, temp, obs_delta, hid_delta,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
row.push(trace.causal_rank() as f64);
|
||||||
|
row.push(trace.causal_edge_count() as f64);
|
||||||
|
row.push(trace.touched_domain_count() as f64);
|
||||||
|
row.push(trace.context_divergence());
|
||||||
|
row
|
||||||
|
}
|
||||||
|
|
||||||
/// Width of one per-domain feature block in a trace feature row.
|
/// Width of one per-domain feature block in a trace feature row.
|
||||||
/// `[infl_out, infl_in, flow_out, flow_in, read, write, temporal, obs_delta, hid_delta]`
|
/// `[infl_out, infl_in, flow_out, flow_in, read, write, temporal, obs_delta, hid_delta]`
|
||||||
|
|||||||
@@ -33,12 +33,4 @@ mod tests {
|
|||||||
assert!(resolve("/style.css").is_some());
|
assert!(resolve("/style.css").is_some());
|
||||||
assert!(resolve("/nope").is_none());
|
assert!(resolve("/nope").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn client_only_sends_intent() {
|
|
||||||
// Guard against the client ever embedding a second simulation: the
|
|
||||||
// browser code must not reference the reference engine internals.
|
|
||||||
assert!(!APP_JS.contains("EngineConfig"));
|
|
||||||
assert!(APP_JS.contains("only ever sends INTENT"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user