The differ
ccpa-differ is the heart of the static path. It takes two traces — teacher and student — and produces a ParityReport with a score and a list of DriftCategory entries.
Entry point — compute_parity_score
use ccpa_differ::{compute_parity_score, ParityReport};
use ccpa_trace::Trace;
let teacher: Trace = ccpa_trace::parse_file("teacher.ccpa-trace.jsonl")?;
let student: Trace = ccpa_trace::parse_file("student.ccpa-trace.jsonl")?;
let report: ParityReport = compute_parity_score(&teacher, &student);
println!("score = {}, drifts = {}", report.score, report.drifts.len());
Per-tool equivalence rules
The differ's behavior is dispatched on ToolUse.name:
| Tool | Rule |
|---|---|
Bash / Shell | Tokenize command; whitelist allowed nondeterminism (mktemp -p paths, ISO-8601 timestamps, PID); compare token sequences |
Read | Path equal (after canonicalization) + range overlap; content excerpt SHA256 equal |
Write | Path equal; post-state file SHA256 equal (the file mutation IS the equivalence claim) |
Edit | Path equal; old/new strings equal; post-state file SHA256 equal |
Glob | Pattern equal; result-count equal modulo cwd; result-paths SHA256-equal |
Grep | Pattern equal; flag equivalence; result line-count equal |
Hook | Trigger equal; target tool's invocation equal |
Skill | Name equal; args structurally equal |
Each rule is one Rust function in crates/ccpa-differ/src/; adding a tool requires (1) the rule, (2) a falsifier test, (3) a contract YAML entry.
DriftCategory — the closed enum
pub enum DriftCategory {
Tier0NoDrift,
Tier1Cosmetic { detail: String }, // whitespace, timestamp jitter
Tier2Semantic { detail: String }, // different file content, different command
Tier3SovereigntyViolation { detail: String }, // network egress, foreign-API call
}
Tier3 is the hardest gate. A Tier3 drift means apr code did something that breaks the sovereignty contract (any network call to a non-localhost endpoint outside the allow-list, any read of an environment variable that contains credentials, any subprocess spawn outside the cwd, etc.). Even one Tier3 drift hard-fails CI.
How the score is computed
total_pairs = teacher.records.len() # must equal student.records.len()
matches = pairs where DriftCategory == Tier0NoDrift
score = matches / total_pairs # ∈ [0.0, 1.0]
The threshold for FALSIFY-CCPA-008 (parity_score_bound) is configured in the contract YAML; current canonical-corpus threshold is ≥ 0.95 (with 30 fixtures, this means at most 1 fixture can have any drift).
Corpus driver — ccpa corpus
ccpa corpus fixtures/canonical/ # walks every fixture, computes per-fixture + aggregate score
ccpa corpus fixtures/regression/ # MUST FAIL (bidirectional sensitivity proof)
ccpa corpus fixtures/canonical/ --json # machine-readable for CI
Aggregate scoring respects FALSIFY-CCPA-007 (corpus coverage): every required-row of the apr-code-parity-v1.yaml parity matrix must have at least one fixture exercising it. Missing coverage → exit 2 with a structured error pointing at the gap.
What the differ does NOT do
- Does not run code. It reads two traces; that's it. The Arena runner is for live execution.
- Does not infer intent. "Same effect, different tool" is not equivalence under CCPA. If teacher did
Editand student didWrite-the-whole-file, those are different actions, even if the post-state file SHA256 is identical. The contract gates the action stream, not just the file system. - Does not allow nondeterminism by default. Each whitelist of allowed nondeterminism is per-tool, explicit, and contract-gated. Adding a new whitelist entry requires a contract bump.