gates: diff-test orchestration — corpus pin + tests + golden per target

Adds the diff-test CLI ('just gates <target>'): loads
targets/<name>/target.toml (new [corpus] section: manifest, root,
golden_sha256; commands now repo-root-relative), verifies the corpus
manifest FIRST and refuses to run any gate on mismatch, then runs the
upstream test suite and golden replay (stdout sha256 vs pinned hash).
Differential fuzz and sanitizers remain per-attempt manual steps in
Phase 1 and are reported as skipped-with-reason; all-skipped is not a
pass.

Verified end-to-end on both vendored targets (comrak: 1 pinned file,
848 tests, golden PASS; tokei: 545 pinned files, 250 tests, golden
PASS) and the refusal path (tampered corpus -> 'HASH MISMATCH ...
refusing to run' before any gate executes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EcKPWQ3MQD4NdEHwar96u2
This commit is contained in:
Claude
2026-07-06 23:30:40 +00:00
parent 73763321c6
commit 0d29d51f72
9 changed files with 190 additions and 6 deletions
+1
View File
@@ -8,6 +8,7 @@
//! declares an explicit FP tolerance).
pub mod pin;
pub mod target;
pub mod policy;
use serde::{Deserialize, Serialize};
+93
View File
@@ -0,0 +1,93 @@
//! Gate orchestration: corpus pin → upstream tests → golden replay.
//! Refuses to run anything if the corpus manifest mismatches (SPEC §3.2).
//! Differential fuzz and sanitizers remain per-attempt manual steps in
//! Phase 1 and are reported as skipped with a reason.
use anyhow::Result;
use clap::Parser;
use diff_test::target::{expected_golden_hash, TargetSpec};
use diff_test::{all_passed, pin, GateLayer, GateOutcome};
use sha2::{Digest, Sha256};
use std::path::PathBuf;
use std::process::{Command, Stdio};
#[derive(Parser)]
#[command(name = "diff-test", about = "hotpath equivalence-gate orchestrator")]
struct Cli {
/// Target name under targets/.
target: String,
/// Repository root (defaults to current directory).
#[arg(long, default_value = ".")]
root: PathBuf,
}
fn sh(root: &PathBuf, cmd: &str) -> Result<std::process::Output> {
Ok(Command::new("sh")
.arg("-c")
.arg(cmd)
.current_dir(root)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()?)
}
fn main() -> Result<()> {
let cli = Cli::parse();
let root = cli.root.canonicalize()?;
let spec = TargetSpec::load(&root, &cli.target)?;
let mut results: Vec<(GateLayer, GateOutcome)> = Vec::new();
// Corpus pin is a precondition, not a gate: mismatch = refuse to run.
let verified = pin::verify_manifest(&root.join(&spec.corpus.manifest), &root.join(&spec.corpus.root))?;
println!("corpus pin: {} files verified against {}", verified, spec.corpus.manifest.display());
print!("upstream tests ... ");
let t = sh(&root, &spec.gates.tests)?;
let tests_outcome = if t.status.success() {
println!("PASS");
GateOutcome::Passed
} else {
println!("FAIL");
GateOutcome::Failed {
detail: String::from_utf8_lossy(&t.stderr).chars().take(2000).collect(),
}
};
results.push((GateLayer::UpstreamTests, tests_outcome));
print!("golden replay ... ");
let golden_cmd = spec.gates.golden.replace("{binary}", &spec.build.binary);
let g = sh(&root, &golden_cmd)?;
let expected = expected_golden_hash(&root.join(&spec.corpus.golden_sha256))?;
let actual = format!("{:x}", Sha256::digest(&g.stdout));
let golden_outcome = if g.status.success() && actual == expected {
println!("PASS (stdout sha256 = pinned golden)");
GateOutcome::Passed
} else {
println!("FAIL");
GateOutcome::Failed {
detail: format!("exit={:?} expected={expected} actual={actual}", g.status.code()),
}
};
results.push((GateLayer::GoldenReplay, golden_outcome));
results.push((
GateLayer::DifferentialFuzz,
GateOutcome::Skipped { reason: "per-attempt manual step in Phase 1 (needs old/new pair)".into() },
));
results.push((
GateLayer::Sanitizers,
GateOutcome::Skipped { reason: "per-attempt manual step in Phase 1 (nightly toolchain)".into() },
));
for (layer, outcome) in &results {
if let GateOutcome::Skipped { reason } = outcome {
println!("{layer:?}: skipped — {reason}");
}
}
if all_passed(&results) {
println!("gates: PASS ({})", cli.target);
Ok(())
} else {
anyhow::bail!("gates: FAIL ({})", cli.target)
}
}
+72
View File
@@ -0,0 +1,72 @@
//! Per-target spec (`targets/<name>/target.toml`). All commands and
//! paths are relative to the repository root; `{binary}` in commands is
//! substituted with the built binary path.
use serde::Deserialize;
use std::path::{Path, PathBuf};
#[derive(Debug, Deserialize)]
pub struct TargetSpec {
pub source: Source,
pub build: Build,
pub gates: Gates,
pub corpus: Corpus,
}
#[derive(Debug, Deserialize)]
pub struct Source {
pub repo: String,
pub commit: String,
pub license: String,
#[serde(default)]
pub submodules: toml::value::Table,
}
#[derive(Debug, Deserialize)]
pub struct Build {
/// Baseline build command, run from repo root.
pub baseline: String,
/// Built binary path, relative to repo root.
pub binary: String,
}
#[derive(Debug, Deserialize)]
pub struct Gates {
/// Upstream test suite command, run from repo root.
pub tests: String,
/// Golden replay command; stdout is hashed. `{binary}` substituted.
pub golden: String,
}
#[derive(Debug, Deserialize)]
pub struct Corpus {
/// sha256sum-format manifest of the corpus inputs.
pub manifest: PathBuf,
/// Directory the manifest's relative paths resolve against.
pub root: PathBuf,
/// File whose last whitespace-delimited-first-field line is the
/// expected sha256 of the golden command's stdout.
pub golden_sha256: PathBuf,
}
impl TargetSpec {
pub fn load(repo_root: &Path, name: &str) -> anyhow::Result<Self> {
let path = repo_root.join("targets").join(name).join("target.toml");
let raw = std::fs::read_to_string(&path)
.map_err(|e| anyhow::anyhow!("cannot read {}: {e}", path.display()))?;
Ok(toml::from_str(&raw)?)
}
}
/// Parse the expected stdout hash out of a GOLDEN.sha256 file
/// (comment lines starting with '#' ignored).
pub fn expected_golden_hash(path: &Path) -> anyhow::Result<String> {
let raw = std::fs::read_to_string(path)?;
raw.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.filter_map(|l| l.split_whitespace().next())
.next_back()
.map(str::to_string)
.ok_or_else(|| anyhow::anyhow!("no hash entry in {}", path.display()))
}