changes claude never committed
This commit is contained in:
@@ -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())
|
||||
}
|
||||
Reference in New Issue
Block a user