9eb6d6b4f6
Slice 8, closing the #319 retirement's prose and bench debt. Bench argv rides exec (fixed_cost single-run probe, campaign surfaces, and the seed helper that still called the retired sweep at runtime — caught by the bench run itself); all five fingerprints unchanged against the committed baselines, confirming record-line parity through the retirement. Live docs (README, authoring guide, project layout, glossary) describe only exec + campaign documents + graph introspect --params; three glossary entries explicitly mark their verb retired. Ledger amendments per the C29 discipline — C25's executor-verb-set re-settled (exec + the --override residue), C14's dual grammar reduced to the one dispatch, C24's document-built runs/families + gated-intake route list (lockstep with the code comment), C12's override clause and identity anchor repointed to the runner-layer hash computation, C18/C22/C27/C01 mention rewrites — superseded sentences moved verbatim to the history sidecars (c25's created). Stale dual-grammar doc comment on is_blueprint_file rewritten. refs #319
123 lines
5.3 KiB
Rust
123 lines
5.3 KiB
Rust
//! CLI fixed cost: the spawn floor (`aura --help`, no fingerprint — nothing is
|
|
//! computed) and a minimal data-only project exec whose fingerprint is the
|
|
//! FNV-1a hash of the exec's single stdout JSON line. Fresh scratch project per
|
|
//! repetition, so store counters start identical.
|
|
|
|
use super::{median, RepOutcome};
|
|
use crate::child::run_timed;
|
|
use crate::surfaces::campaign::build_minimal_project;
|
|
use crate::synth::fnv1a;
|
|
use std::collections::BTreeMap;
|
|
use std::path::Path;
|
|
use std::time::Instant;
|
|
|
|
/// Fingerprint of an `aura exec` record line, invariant across rebuilds: the
|
|
/// line's `manifest.commit` is the aura binary's compile-time build sha
|
|
/// (crates/aura-cli/build.rs — the invariant-8 audit trail), so hashing the
|
|
/// raw line would flip the fingerprint on EVERY new commit and report a
|
|
/// phantom correctness change at each cycle close. Parse, blank that one
|
|
/// volatile field, re-serialize (serde_json's map is sorted — deterministic
|
|
/// bytes), then hash.
|
|
pub fn run_line_fingerprint(line: &str) -> Result<String, String> {
|
|
let mut v: serde_json::Value =
|
|
serde_json::from_str(line).map_err(|e| format!("run record line parses: {e}"))?;
|
|
let commit = v
|
|
.get_mut("manifest")
|
|
.and_then(|m| m.get_mut("commit"))
|
|
.ok_or("run record line carries manifest.commit")?;
|
|
*commit = serde_json::Value::String(String::new());
|
|
let canonical = serde_json::to_string(&v).expect("re-serializing a parsed Value cannot fail");
|
|
Ok(format!("run_line_fnv={:016x}", fnv1a(canonical.as_bytes())))
|
|
}
|
|
|
|
/// Spawn samples folded into one repetition: single-digit-millisecond spawns
|
|
/// jitter well past the 10% NOTICE threshold, so each repetition reports the
|
|
/// median over a batch instead of one throw of the scheduler dice.
|
|
pub const HELP_SAMPLES: u32 = 20;
|
|
pub const RUN_SAMPLES: u32 = 10;
|
|
|
|
pub fn rep(bin: &Path) -> Result<RepOutcome, String> {
|
|
// Spawn floor: median wall over a batch of `aura --help` spawns.
|
|
let mut help_walls = Vec::new();
|
|
for _ in 0..HELP_SAMPLES {
|
|
let t = Instant::now();
|
|
let help = std::process::Command::new(bin)
|
|
.arg("--help")
|
|
.output()
|
|
.map_err(|e| format!("spawn aura --help: {e}"))?;
|
|
help_walls.push(t.elapsed().as_secs_f64() * 1000.0);
|
|
if !help.status.success() {
|
|
return Err(format!("aura --help exited {:?}", help.status.code()));
|
|
}
|
|
}
|
|
|
|
// Project-load + minimal run: one scratch project, a batch of runs, the
|
|
// median wall. The fingerprint is the FIRST run's record line — later
|
|
// runs append to the same store, so their lines legitimately differ (run
|
|
// counters), while the first run of a fresh scratch is deterministic.
|
|
let scratch = build_minimal_project()?;
|
|
let mut run_walls = Vec::new();
|
|
let mut first_line = None;
|
|
for _ in 0..RUN_SAMPLES {
|
|
let timed = run_timed(bin, &["exec", "bench_sma_a.json"], &scratch.0)?;
|
|
if timed.exit != Some(0) {
|
|
return Err(format!("aura run exited {:?}: {}", timed.exit, timed.stdout));
|
|
}
|
|
run_walls.push(timed.wall_s * 1000.0);
|
|
if first_line.is_none() {
|
|
first_line = Some(
|
|
timed
|
|
.stdout
|
|
.lines()
|
|
.next()
|
|
.ok_or("aura run must print its JSON record line")?
|
|
.to_string(),
|
|
);
|
|
}
|
|
}
|
|
let line = first_line.expect("RUN_SAMPLES >= 1 sets the first line");
|
|
let mut metrics = BTreeMap::new();
|
|
metrics.insert("help_ms".to_string(), median(help_walls));
|
|
metrics.insert("run_ms".to_string(), median(run_walls));
|
|
Ok(RepOutcome { metrics, fingerprint: run_line_fingerprint(&line)? })
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// The symptom this fixes, pinned: two record lines that differ ONLY in
|
|
/// `manifest.commit` (a rebuild at a new commit) must fingerprint
|
|
/// identically — the raw-line hash flipped here and reported a phantom
|
|
/// correctness change on the first post-commit bench run.
|
|
#[test]
|
|
fn run_line_fingerprint_is_invariant_across_build_commits() {
|
|
let at_a = r#"{"manifest":{"commit":"aaaa","seed":0},"metrics":{"total_pips":0.5}}"#;
|
|
let at_b = r#"{"manifest":{"commit":"bbbb","seed":0},"metrics":{"total_pips":0.5}}"#;
|
|
assert_eq!(
|
|
run_line_fingerprint(at_a).expect("parses"),
|
|
run_line_fingerprint(at_b).expect("parses"),
|
|
);
|
|
}
|
|
|
|
/// The fingerprint still catches what it exists for: a computed-result
|
|
/// change (any non-provenance byte) yields a different hash.
|
|
#[test]
|
|
fn run_line_fingerprint_still_detects_result_changes() {
|
|
let base = r#"{"manifest":{"commit":"aaaa","seed":0},"metrics":{"total_pips":0.5}}"#;
|
|
let drifted = r#"{"manifest":{"commit":"aaaa","seed":0},"metrics":{"total_pips":0.6}}"#;
|
|
assert_ne!(
|
|
run_line_fingerprint(base).expect("parses"),
|
|
run_line_fingerprint(drifted).expect("parses"),
|
|
);
|
|
}
|
|
|
|
/// A line without the provenance field is malformed input, not a silent
|
|
/// pass — the guard that keeps the blanking honest if the record schema
|
|
/// ever moves.
|
|
#[test]
|
|
fn run_line_fingerprint_rejects_a_line_without_manifest_commit() {
|
|
assert!(run_line_fingerprint(r#"{"metrics":{"total_pips":0.5}}"#).is_err());
|
|
}
|
|
}
|