phase4: service-mode latency bench + mechanical ROI report generator

- bench-runner service mode (SPEC §3.1 mode c): a coordinated-omission-
  correct, open-loop, fixed-rate load generator; latency measured from
  the intended send time so queueing is counted, not hidden. Interleaved
  A/B of two server binaries, exact percentiles (bounded sample, no HDR
  bucketing error), bootstrap p50/p99 CIs via the existing ratio-CI
  machine. service-calibrate: A/A false-positive + injected latency-
  regression detection (calibration in progress).
- targets/cjson/service.c: minimal single-threaded HTTP server wrapping
  the patched cJSON.c (trust-layer, outside the workspace allowlist);
  env-toggled per-request busy-wait for the injection calibration only.
- report generator (crates/report bin): reads a ledger row +
  optional service-latency JSON, renders throughput→cores→dollars and/or
  latency percentiles, every figure with its CI, pricing source, and the
  methodology printed inline (SPEC §9); flags any non-accepted row so a
  rejected change never reads as a committed saving.
- ledger::report_row; justfile service/service-calibrate/report.

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-11 23:31:58 +00:00
parent 3951857cf2
commit a78692bbb7
11 changed files with 981 additions and 0 deletions
+44
View File
@@ -190,6 +190,50 @@ impl Ledger {
/// onto a completed attempt (harnessd `read_verdict`). Returns the
/// verdict plus the bench CIs; never enough to game, since the row
/// is already written and immutable.
/// Full row data the ROI report generator needs (SPEC §9). Includes
/// the workload string pulled out of the bench evidence.
pub fn report_row(&self, run_id: &str) -> Result<Option<serde_json::Value>, LedgerError> {
let row = self.conn.query_row(
"SELECT target, playbook_class, hypothesis, verdict, bench, timestamp \
FROM attempts WHERE run_id = ?1",
[run_id],
|r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, u8>(1)?,
r.get::<_, String>(2)?,
r.get::<_, String>(3)?,
r.get::<_, Option<String>>(4)?,
r.get::<_, String>(5)?,
))
},
);
match row {
Ok((target, class, hypothesis, verdict, bench, ts)) => {
let bench: Option<serde_json::Value> =
bench.map(|b| serde_json::from_str(&b)).transpose()?;
let workload = bench
.as_ref()
.and_then(|b| b.get("env_fingerprint"))
.and_then(|f| f.get("workload"))
.cloned();
Ok(Some(serde_json::json!({
"run_id": run_id,
"target": target,
"playbook_class": class,
"hypothesis": hypothesis,
"verdict": verdict,
"timestamp": ts,
"speedup_median": bench.as_ref().and_then(|b| b.get("speedup_median").cloned()),
"speedup_ci": bench.as_ref().and_then(|b| b.get("speedup_ci").cloned()),
"workload": workload,
})))
}
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn verdict_summary(&self, run_id: &str) -> Result<Option<serde_json::Value>, LedgerError> {
let row = self.conn.query_row(
"SELECT verdict, bench FROM attempts WHERE run_id = ?1",