Files
cubelinux-2/cubecode/src/trace.rs
T

264 lines
10 KiB
Rust

//! Execution-trace artifacts: store a captured machine-code trace as a
//! first-class AI-artifact record (`Kind::Layer`) linked back to its source,
//! and replay it to reproduce the run — so an agent can persist & reproduce
//! interesting executions as ordinary CZYX records.
//!
//! A trace is the exact `Vec<Op>` a program actually executed ([`Vm::run_captured`]).
//! We persist it as a record body (via [`crate::opcode::encode`]) with
//! `doc_type = "layer"` (the AI-artifact kind) and a `linked_records` edge to
//! the source cell. Loading it back (via [`CodeCell::from_record`]) decodes the
//! ops and [`replay`] reproduces the observable behaviour with no access to the
//! source program.
use crate::{opcode, replay, Kind, Op, RunResult, Vm};
use cubecoords::{CubeHeader, Czyx};
use cubestore::{CubeBackend, CubeStore};
/// Store a captured execution trace as an AI-artifact record at `coord`,
/// linked to its `source` cell. `name` becomes the record title; the body is
/// the encoded trace; `doc_type` is `Kind::Layer`.
pub fn store_trace<B: CubeBackend>(
store: &mut CubeStore<B>,
coord: Czyx,
source: Czyx,
trace: &[Op],
name: &str,
) {
let mut h = CubeHeader::new();
h.title = Some(name.to_string());
h.doc_type = Some(Kind::Layer.as_str().to_string());
h.linked_records = vec![source];
h.refresh_flags();
store.put_record(coord, &h, &opcode::encode(trace));
}
/// Load a stored trace record at `coord` and replay it, reproducing the run's
/// observable behaviour (data-stack result + `SYS_TRACE` output) from the
/// stored record — no access to the original program. Returns `None` if the
/// record is missing or its body is not a valid trace.
pub fn replay_trace<B: CubeBackend>(
store: &CubeStore<B>,
coord: Czyx,
) -> Option<(RunResult, Vec<u8>)> {
let (_h, body) = store.get_record(&coord)?;
let ops = opcode::decode(&body).ok()?;
Some(replay(&ops))
}
/// Capture a run and persist it as a "golden": the executed trace stored as a
/// `Kind::Layer` record (linked to `source`) and the observed output stored as a
/// companion `golden` record (linked to the trace). Returns the observed output.
pub fn capture_golden<B: CubeBackend + Clone>(
store: &mut CubeStore<B>,
source: Czyx,
trace_coord: Czyx,
golden_coord: Czyx,
name: &str,
) -> Vec<u8> {
let mut vm = Vm::new((*store).clone());
let (_r, trace) = vm.run_captured(source);
let out = vm.output().to_vec();
store_trace(store, trace_coord, source, &trace, name);
let mut h = CubeHeader::new();
h.title = Some(format!("golden:{name}"));
h.doc_type = Some("golden".into());
h.linked_records = vec![trace_coord];
h.refresh_flags();
store.put_record(golden_coord, &h, &out);
out
}
/// Golden-trace regression check: re-run `source` fresh AND replay the stored
/// trace, and assert BOTH reproduce the recorded golden output.
/// * replay vs golden -> the stored trace is a faithful reproduction;
/// * fresh run vs golden -> the program has not drifted (or it would change
/// the observed output).
/// Returns `Ok(())` if the behavior is unchanged, else `Err(reason)`.
pub fn verify_golden<B: CubeBackend + Clone>(
store: &CubeStore<B>,
source: Czyx,
trace_coord: Czyx,
golden_coord: Czyx,
) -> Result<(), String> {
let golden = store
.get_record(&golden_coord)
.map(|(_, b)| b)
.ok_or("golden record missing")?;
let (_, replay_out) = replay_trace(store, trace_coord).ok_or("trace record missing/invalid")?;
if replay_out != golden {
return Err(format!(
"replay output differs from golden ({} vs {} bytes)",
replay_out.len(),
golden.len()
));
}
let mut vm = Vm::new((*store).clone());
let _ = vm.run(source);
let fresh = vm.output().to_vec();
if fresh != golden {
return Err(format!(
"fresh run differs from golden (program changed: {} vs {} bytes)",
fresh.len(),
golden.len()
));
}
Ok(())
}
/// A hop in the experiment-lineage graph: a record and the records it links to.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LineageHop {
/// The record's coordinate.
pub coord: Czyx,
/// Its `doc_type` (kind: fn/kernel/layer/checkpoint/variant/golden/...).
pub doc_type: String,
/// Its human title, if any.
pub title: String,
/// The CZYX records this record links to (its edges).
pub links: Vec<Czyx>,
}
/// Walk the linked-record graph starting at `start`, following
/// `linked_records` edges (breadth-first) up to `depth` levels. Returns the
/// ordered list of reachable records — the experiment lineage chain
/// (source -> data -> weights -> trace -> golden, etc.). Each record is the
/// kind the CUBE associates; provenance is a graph walk, not a lookup.
pub fn lineage<B: CubeBackend>(
store: &CubeStore<B>,
start: Czyx,
depth: usize,
) -> Vec<LineageHop> {
let mut out = Vec::new();
let mut frontier = vec![start];
let mut seen: std::collections::HashSet<Czyx> = std::collections::HashSet::new();
for _ in 0..=depth {
if frontier.is_empty() {
break;
}
let mut next = Vec::new();
for c in frontier {
if !seen.insert(c) {
continue;
}
if let Some((h, _)) = store.get_record(&c) {
let links = h.linked_records.clone();
out.push(LineageHop {
coord: c,
doc_type: h.doc_type.clone().unwrap_or_default(),
title: h.title.clone().unwrap_or_default(),
links: links.clone(),
});
next.extend(links);
}
}
frontier = next;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{SYS_TRACE, Vm};
use cubestore::HashBackend;
#[test]
fn trace_stored_as_layer_and_replayed_without_source() {
let mut store = CubeStore::new(HashBackend::new());
let entry = Czyx::new(1, 1, 1, 7);
let ops = [
Op::Const(0), Op::Const(1), Op::Syscall(SYS_TRACE),
Op::Const(1), Op::Const(1), Op::Syscall(SYS_TRACE),
Op::Const(2), Op::Const(2), Op::Syscall(SYS_TRACE),
Op::Halt,
];
let mut h = CubeHeader::new();
h.title = Some("solitaire".into());
h.doc_type = Some("fn".into());
h.refresh_flags();
store.put_record(entry, &h, &opcode::encode(&ops));
// 1) Capture the run as a machine-code trace.
let mut vm = Vm::new(store.clone());
let (_r, trace) = vm.run_captured(entry);
let produced = vm.output().to_vec();
assert!(!trace.is_empty());
// 2) Store it as a Kind::Layer AI-artifact record linked to the source.
let trace_coord = Czyx::new(9, 0, 0, 1);
store_trace(&mut store, trace_coord, entry, &trace, "solitaire-run-1");
// 3) The record is a "layer" artifact with an edge back to the source.
let (th, _tb) = store.get_record(&trace_coord).unwrap();
assert_eq!(th.doc_type.as_deref(), Some("layer"));
assert_eq!(th.linked_records, vec![entry]);
// 4) Reproduce the run purely from the stored trace record.
let (result, replayed) = replay_trace(&store, trace_coord).unwrap();
assert_eq!(result, RunResult::Halted { top: None });
assert_eq!(replayed, produced, "stored trace must reproduce the run");
assert!(replayed.contains(&b'\n'));
}
#[test]
fn golden_regression_detects_drift() {
let mut store = CubeStore::new(HashBackend::new());
let entry = Czyx::new(1, 1, 1, 9);
let mk = |val: u8| {
vec![Op::Const(val), Op::Syscall(SYS_TRACE), Op::Halt]
};
let mut h = CubeHeader::new();
h.doc_type = Some("fn".into());
h.refresh_flags();
store.put_record(entry, &h, &opcode::encode(&mk(7)));
let trace_c = Czyx::new(9, 0, 0, 1);
let golden_c = Czyx::new(9, 0, 0, 2);
let out = capture_golden(&mut store, entry, trace_c, golden_c, "regress");
assert_eq!(out, b"\x07\n");
assert!(verify_golden(&store, entry, trace_c, golden_c).is_ok());
// Drift the program: change the emitted value -> fresh run differs.
store.put_record(entry, &h, &opcode::encode(&mk(9)));
let r = verify_golden(&store, entry, trace_c, golden_c);
assert!(r.is_err(), "program drift must be detected: {r:?}");
// The stored trace is immutable: it still replays the ORIGINAL golden.
let (_, rep) = replay_trace(&store, trace_c).unwrap();
assert_eq!(rep, b"\x07\n", "stored trace still reproduces the original run");
}
#[test]
fn lineage_walks_source_data_weights_trace() {
let mut store = CubeStore::new(HashBackend::new());
// Build experiment lineage: source -> data -> weights -> trace -> golden.
let mut mk = |coord: Czyx, doc: &str, title: &str, links: Vec<Czyx>| {
let mut h = CubeHeader::new();
h.title = Some(title.into());
h.doc_type = Some(doc.into());
h.linked_records = links;
h.refresh_flags();
store.put_record(coord, &h, &[]);
};
let source = Czyx::new(1, 1, 1, 1);
let data = Czyx::new(2, 0, 0, 1);
let weights = Czyx::new(3, 0, 0, 1);
let trace = Czyx::new(4, 0, 0, 1);
let golden = Czyx::new(5, 0, 0, 1);
mk(source, "fn", "train", vec![data, weights]);
mk(data, "data", "batch", vec![]);
mk(weights, "layer", "model-v1", vec![]);
mk(trace, "layer", "trace:train", vec![source]);
mk(golden, "golden", "golden:train", vec![trace]);
// Walk from the trace back to its lineage.
let hops = lineage(&store, trace, 4);
assert!(hops.iter().any(|h| h.coord == trace && h.doc_type == "layer"));
assert!(hops.iter().any(|h| h.coord == source && h.doc_type == "fn"));
// Depth-limited: from a leaf with no links, only itself.
let leaf = lineage(&store, data, 3);
assert_eq!(leaf.len(), 1);
assert_eq!(leaf[0].coord, data);
assert_eq!(leaf[0].links, vec![]);
}
}