changes claude never committed

This commit is contained in:
2026-06-21 18:00:52 -07:00
parent 39386a81c9
commit 2fe989bcb3
33 changed files with 5528 additions and 0 deletions
+433
View File
@@ -0,0 +1,433 @@
//! `trace_model` — the execution trace and all of its graphs, plus the
//! behavior fingerprint, replay record, fault log, and the metrics the trace
//! gates check (causal rank, causal edges, touched domains, fingerprint
//! collisions, executor divergence).
use world_model::{combine_hashes, Hash, Hasher, NUM_DOMAINS};
pub mod matrix;
pub use matrix::numeric_rank;
/// A graph over domains: per-domain access counts plus cross-domain edges.
/// Used for both the read graph and the write graph.
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct DomainAccessGraph {
pub access_count: [u32; NUM_DOMAINS],
/// `(from_domain, to_domain, weight)` data-movement edges.
pub edges: Vec<(u8, u8, u32)>,
}
impl DomainAccessGraph {
pub fn touched(&self) -> Vec<usize> {
(0..NUM_DOMAINS).filter(|&i| self.access_count[i] > 0).collect()
}
pub fn touched_count(&self) -> usize {
self.access_count.iter().filter(|&&c| c > 0).count()
}
pub fn hash(&self) -> Hash {
let mut h = Hasher::new();
h.write_tag("access-graph");
for &c in &self.access_count {
h.write_u64(c as u64);
}
h.write_usize(self.edges.len());
for &(a, b, w) in &self.edges {
h.write_u8(a);
h.write_u8(b);
h.write_u64(w as u64);
}
h.finish()
}
}
/// A node in the causal graph: a specific domain lane at a specific step.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub struct CausalNode {
pub domain: u8,
pub lane: u8,
pub hidden: bool,
pub step: u32,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct CausalEdge {
pub from: CausalNode,
pub to: CausalNode,
pub weight: i64,
}
/// The causal dependency graph of an execution.
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct CausalGraph {
pub edges: Vec<CausalEdge>,
}
impl CausalGraph {
pub fn edge_count(&self) -> usize {
self.edges.len()
}
/// Domains that participate as either source or sink of a causal edge.
pub fn touched_domains(&self) -> Vec<usize> {
let mut seen = [false; NUM_DOMAINS];
for e in &self.edges {
seen[e.from.domain as usize % NUM_DOMAINS] = true;
seen[e.to.domain as usize % NUM_DOMAINS] = true;
}
(0..NUM_DOMAINS).filter(|&i| seen[i]).collect()
}
pub fn touched_domain_count(&self) -> usize {
self.touched_domains().len()
}
/// Aggregate domain-by-domain influence matrix (weights summed).
pub fn influence_matrix(&self) -> [[f64; NUM_DOMAINS]; NUM_DOMAINS] {
let mut m = [[0.0f64; NUM_DOMAINS]; NUM_DOMAINS];
for e in &self.edges {
let i = e.from.domain as usize % NUM_DOMAINS;
let j = e.to.domain as usize % NUM_DOMAINS;
// Use a bounded, lane-distinguished contribution so distinct
// interactions remain linearly independent rather than collapsing
// into a single dominant magnitude.
let lane_phase = 1.0 + (e.from.lane as f64) + 4.0 * (e.to.lane as f64);
m[i][j] += lane_phase * ((e.weight & 0xffff) as f64 + 1.0);
}
m
}
/// Causal rank: numeric rank of the influence matrix.
pub fn causal_rank(&self) -> usize {
let m = self.influence_matrix();
let rows: Vec<Vec<f64>> = m.iter().map(|r| r.to_vec()).collect();
numeric_rank(&rows)
}
pub fn hash(&self) -> Hash {
let mut h = Hasher::new();
h.write_tag("causal-graph");
h.write_usize(self.edges.len());
for e in &self.edges {
h.write_u8(e.from.domain);
h.write_u8(e.from.lane);
h.write_u8(e.from.hidden as u8);
h.write_u64(e.from.step as u64);
h.write_u8(e.to.domain);
h.write_u8(e.to.lane);
h.write_u8(e.to.hidden as u8);
h.write_u64(e.to.step as u64);
h.write_i64(e.weight);
}
h.finish()
}
}
/// Information flow edges with continuous weights (bits of influence).
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct InformationFlowGraph {
/// `(from_domain, to_domain, influence_bits)`
pub edges: Vec<(u8, u8, u32)>,
}
impl InformationFlowGraph {
pub fn total_bits(&self) -> u64 {
self.edges.iter().map(|&(_, _, b)| b as u64).sum()
}
pub fn hash(&self) -> Hash {
let mut h = Hasher::new();
h.write_tag("info-flow");
h.write_usize(self.edges.len());
for &(a, b, w) in &self.edges {
h.write_u8(a);
h.write_u8(b);
h.write_u64(w as u64);
}
h.finish()
}
}
/// Pairwise divergence between executors (fraction of differing lanes).
#[derive(Clone, PartialEq, Debug, Default)]
pub struct DivergenceGraph {
pub executor_count: usize,
/// Flattened `executor_count x executor_count` divergence fractions.
pub pairwise: Vec<f64>,
}
impl DivergenceGraph {
pub fn get(&self, i: usize, j: usize) -> f64 {
if self.executor_count == 0 {
return 0.0;
}
self.pairwise[i * self.executor_count + j]
}
/// Mean off-diagonal divergence.
pub fn mean_divergence(&self) -> f64 {
let n = self.executor_count;
if n < 2 {
return 0.0;
}
let mut sum = 0.0;
let mut cnt = 0;
for i in 0..n {
for j in 0..n {
if i != j {
sum += self.get(i, j);
cnt += 1;
}
}
}
sum / cnt as f64
}
pub fn hash(&self) -> Hash {
let mut h = Hasher::new();
h.write_tag("divergence");
h.write_usize(self.executor_count);
for &v in &self.pairwise {
h.write_i64((v * 1_000_000.0) as i64);
}
h.finish()
}
}
/// Temporal graph: edges from an execution step to a future turn effect.
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct TemporalGraph {
/// `(step, turn_offset, affected_domain)`
pub edges: Vec<(u32, u8, u8)>,
}
impl TemporalGraph {
pub fn future_reach(&self) -> u8 {
self.edges.iter().map(|&(_, t, _)| t).max().unwrap_or(0)
}
pub fn edge_count(&self) -> usize {
self.edges.len()
}
pub fn hash(&self) -> Hash {
let mut h = Hasher::new();
h.write_tag("temporal");
h.write_usize(self.edges.len());
for &(s, t, d) in &self.edges {
h.write_u64(s as u64);
h.write_u8(t);
h.write_u8(d);
}
h.finish()
}
}
/// Summary of how perturbations affected this execution. Populated by the
/// metamorphic harness; default/empty in a bare resolve.
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct PerturbationResponse {
pub total: usize,
pub altered_trace: usize,
pub altered_delta: usize,
pub altered_future: usize,
pub neutral_unexplained: usize,
}
impl PerturbationResponse {
pub fn hash(&self) -> Hash {
let mut h = Hasher::new();
h.write_tag("perturbation-response");
h.write_usize(self.total);
h.write_usize(self.altered_trace);
h.write_usize(self.altered_delta);
h.write_usize(self.altered_future);
h.write_usize(self.neutral_unexplained);
h.finish()
}
}
/// Behavior fingerprint: a stable hash plus a feature vector used by the
/// collapse analysis and behavior clustering.
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct BehaviorFingerprint {
pub hash: Hash,
pub features: Vec<i64>,
}
impl BehaviorFingerprint {
pub fn from_features(features: Vec<i64>) -> Self {
let mut h = Hasher::new();
h.write_tag("behavior");
h.write_usize(features.len());
for &f in &features {
h.write_i64(f);
}
BehaviorFingerprint {
hash: h.finish(),
features,
}
}
}
/// Faults are always logged, never panicked. Their presence is normal.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum FaultCode {
GuardedDivByZero,
Saturated,
OverflowWrapped,
EmptyAccumulator,
UnreachableBranch,
NoEffectToken,
}
impl FaultCode {
pub fn name(self) -> &'static str {
match self {
FaultCode::GuardedDivByZero => "guarded_div_by_zero",
FaultCode::Saturated => "saturated",
FaultCode::OverflowWrapped => "overflow_wrapped",
FaultCode::EmptyAccumulator => "empty_accumulator",
FaultCode::UnreachableBranch => "unreachable_branch",
FaultCode::NoEffectToken => "no_effect_token",
}
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Fault {
pub code: FaultCode,
pub step: u32,
pub detail_code: i64,
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct FaultLog {
pub faults: Vec<Fault>,
}
impl FaultLog {
pub fn push(&mut self, code: FaultCode, step: u32, detail_code: i64) {
self.faults.push(Fault {
code,
step,
detail_code,
});
}
pub fn hash(&self) -> Hash {
let mut h = Hasher::new();
h.write_tag("fault-log");
h.write_usize(self.faults.len());
for f in &self.faults {
h.write_u8(f.code as u8);
h.write_u64(f.step as u64);
h.write_i64(f.detail_code);
}
h.finish()
}
}
/// Replay record: seeds plus the three canonical hashes.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct ReplayRecord {
pub world_seed: u64,
pub program_seed: u64,
pub contract_seed: u64,
pub perturbation_seed: u64,
pub trace_hash: Hash,
pub delta_hash: Hash,
pub future_hash: Hash,
}
impl ReplayRecord {
pub fn hash(&self) -> Hash {
let mut h = Hasher::new();
h.write_tag("replay-record");
h.write_u64(self.world_seed);
h.write_u64(self.program_seed);
h.write_u64(self.contract_seed);
h.write_u64(self.perturbation_seed);
h.write_u64(self.trace_hash.0);
h.write_u64(self.delta_hash.0);
h.write_u64(self.future_hash.0);
h.finish()
}
}
/// The full execution trace (per spec).
#[derive(Clone, PartialEq, Debug)]
pub struct ExecutionTrace {
pub read_graph: DomainAccessGraph,
pub write_graph: DomainAccessGraph,
pub causal_graph: CausalGraph,
pub information_flow: InformationFlowGraph,
pub executor_divergence: DivergenceGraph,
pub temporal_graph: TemporalGraph,
pub perturbation_response: PerturbationResponse,
pub behavior_fingerprint: BehaviorFingerprint,
}
impl ExecutionTrace {
pub fn causal_rank(&self) -> usize {
self.causal_graph.causal_rank()
}
pub fn causal_edge_count(&self) -> usize {
self.causal_graph.edge_count()
}
/// Domains touched = union of read, write and causal participation.
pub fn touched_domain_count(&self) -> usize {
let mut seen = [false; NUM_DOMAINS];
for i in self.read_graph.touched() {
seen[i] = true;
}
for i in self.write_graph.touched() {
seen[i] = true;
}
for i in self.causal_graph.touched_domains() {
seen[i] = true;
}
seen.iter().filter(|&&b| b).count()
}
pub fn context_divergence(&self) -> f64 {
self.executor_divergence.mean_divergence()
}
/// Canonical hash over the whole trace (used by replay & equivalence).
pub fn canonical_hash(&self) -> Hash {
combine_hashes(
"execution-trace",
&[
self.read_graph.hash(),
self.write_graph.hash(),
self.causal_graph.hash(),
self.information_flow.hash(),
self.executor_divergence.hash(),
self.temporal_graph.hash(),
self.perturbation_response.hash(),
self.behavior_fingerprint.hash,
],
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rank_of_identity_is_full() {
let id: Vec<Vec<f64>> = (0..5)
.map(|i| (0..5).map(|j| if i == j { 1.0 } else { 0.0 }).collect())
.collect();
assert_eq!(numeric_rank(&id), 5);
}
#[test]
fn rank_of_zero_is_zero() {
let z: Vec<Vec<f64>> = vec![vec![0.0; 4]; 4];
assert_eq!(numeric_rank(&z), 0);
}
#[test]
fn rank_of_rank_one_is_one() {
// every row a multiple of [1,2,3]
let m: Vec<Vec<f64>> = (1..=4).map(|k| vec![k as f64, 2.0 * k as f64, 3.0 * k as f64]).collect();
assert_eq!(numeric_rank(&m), 1);
}
}
+82
View File
@@ -0,0 +1,82 @@
//! Small numeric linear-algebra helpers used by the trace and collapse gates.
/// Numeric rank of a matrix via Gaussian elimination with partial pivoting.
/// Tolerance scales with the matrix magnitude so it is robust to the large
/// integer-derived weights the causal graph produces.
pub fn numeric_rank(rows_in: &[Vec<f64>]) -> usize {
if rows_in.is_empty() {
return 0;
}
let mut rows: Vec<Vec<f64>> = rows_in.to_vec();
let nrows = rows.len();
let ncols = rows[0].len();
let max_abs = rows
.iter()
.flat_map(|r| r.iter())
.fold(0.0f64, |m, &v| m.max(v.abs()));
if max_abs == 0.0 {
return 0;
}
let tol = 1e-9 * max_abs * (nrows.max(ncols) as f64);
let mut rank = 0;
let mut pivot_col = 0;
while rank < nrows && pivot_col < ncols {
// Find pivot row with the largest magnitude in pivot_col.
let mut best = rank;
let mut best_val = rows[rank][pivot_col].abs();
for r in (rank + 1)..nrows {
let v = rows[r][pivot_col].abs();
if v > best_val {
best_val = v;
best = r;
}
}
if best_val <= tol {
pivot_col += 1;
continue;
}
rows.swap(rank, best);
let pivot = rows[rank][pivot_col];
for r in 0..nrows {
if r != rank {
let factor = rows[r][pivot_col] / pivot;
if factor != 0.0 {
for c in pivot_col..ncols {
rows[r][c] -= factor * rows[rank][c];
}
}
}
}
rank += 1;
pivot_col += 1;
}
rank
}
/// Pearson correlation between two equal-length series. Returns 0 if either is
/// constant.
pub fn correlation(xs: &[f64], ys: &[f64]) -> f64 {
let n = xs.len().min(ys.len());
if n == 0 {
return 0.0;
}
let nf = n as f64;
let mx = xs[..n].iter().sum::<f64>() / nf;
let my = ys[..n].iter().sum::<f64>() / nf;
let mut cov = 0.0;
let mut vx = 0.0;
let mut vy = 0.0;
for i in 0..n {
let dx = xs[i] - mx;
let dy = ys[i] - my;
cov += dx * dy;
vx += dx * dx;
vy += dy * dy;
}
if vx <= 1e-12 || vy <= 1e-12 {
return 0.0;
}
cov / (vx.sqrt() * vy.sqrt())
}