Files
zlareb1 3e19a38b61 Add noise subcommand and per-commit time-series recipe
Closes the remaining two use-case gaps from the prior critical pass:

1. **Per-scenario noise characterization.** `methodology.md` claims a
   ~5 % rps and ~10 % p99 noise floor; until now we had no in-skill
   way to verify those claims against the live dataset.
   `scripts/build_noise_stats.py` reads `merged_metrics.tsv`, groups
   by `(scenario, backend)`, and for each headline metric emits
   `n_runs`, `median`, `stddev`, `cv = stddev / |median|`, and `p95`.
   Output: `noise_stats.tsv`, one row per `(scenario, backend, metric)`.
   Skips groups with `< 3` runs (stddev needs ≥ 3 points).

   Smoke test on the current dataset confirms the rubric:
   - `rps` cv across no-fault scenarios is 0.21–4.43 % — under the
     documented 5 % floor on every one.
   - `prod-mix-no-fault[default]`: rps cv 1.31 %, read_p99_ms cv
     3.99 %. p99 is 3× noisier than rps, validating the 10/30 vs
     5/15 per-metric band split.

2. **Per-commit time-series via free-form recipe.** Added an awk
   one-liner to the `SKILL.md` Phase 4 "Free-form recipes" block
   that emits `(run_ended, sha8, value)` rows for one
   `(scenario, backend, metric)` triple. No code; the data is
   already in `merged_metrics.tsv`.

`keeper_stress.py`: new `noise` subcommand wraps
`build_noise_stats.main` (no args). `SKILL.md` Phase 3: new
"Noise calibration" row in the intent table.

Pipeline parity preserved: `cumulative_gains.tsv` md5 still
`e5a27e01008ef7e23597ee041f68103b`; 18 unit tests still pass.
2026-05-06 09:00:12 +00:00

82 lines
2.9 KiB
Python

#!/usr/bin/env python3
"""Single CLI entrypoint for the keeper-stress-analysis pipeline.
Usage:
python3 keeper_stress.py <subcommand> [args...]
Subcommands:
merge Build `merged_metrics.tsv` from the five staging TSVs.
(See `build_metrics_table.py`.)
prmap Build `pr_to_nightly.tsv` from `pr_meta.tsv` + bench summary.
(See `build_pr_nightly_map.py`.) Honors `KEEPER_SKILL_THRESHOLD`.
deltas Compute per-PR scenario deltas, per-PR summary, and
per-nightly summary. (See `compute_deltas.py`.) Honors
`KEEPER_SKILL_THRESHOLD`.
prmetrics Build `per_pr_metrics_long.tsv` (flat per-PR-metric form).
(See `build_per_pr_metrics_tsv.py`.)
prisol Build `pr_branch_isolated.tsv` (PR-branch pool isolation).
(See `build_pr_branch_isolated.py`.)
cumulative Build `cumulative_gains.tsv` and the summary pivot.
(See `build_cumulative_gains.py`.)
diff A B Build `commit_diff.tsv` comparing two arbitrary SHAs
(8-char prefix or full). (See `build_commit_diff.py`.)
noise Build `noise_stats.tsv` with per-(scenario, backend, metric)
median / stddev / cv / p95 across all runs in the window.
(See `build_noise_stats.py`.)
Each subcommand operates on files in this script's directory (where
`rebuild.sh` copies the SQL queries and scripts before running).
"""
import sys
import build_commit_diff
import build_cumulative_gains
import build_metrics_table
import build_noise_stats
import build_per_pr_metrics_tsv
import build_pr_branch_isolated
import build_pr_nightly_map
import compute_deltas
def _deltas(_args):
"""`compute_deltas.py` exposes two entrypoints; run both."""
compute_deltas.compute_pr_deltas()
compute_deltas.compute_nightly_summary()
def _diff(args):
if len(args) != 2:
print("usage: keeper_stress.py diff <shaA> <shaB>", file=sys.stderr)
return 2
return build_commit_diff.main(args[0], args[1])
SUBCOMMANDS = {
"merge": lambda args: build_metrics_table.main(),
"prmap": lambda args: build_pr_nightly_map.main(),
"deltas": _deltas,
"prmetrics": lambda args: build_per_pr_metrics_tsv.main(),
"prisol": lambda args: build_pr_branch_isolated.main(),
"cumulative": lambda args: build_cumulative_gains.main(),
"diff": _diff,
"noise": lambda args: build_noise_stats.main(),
}
def _usage():
names = " ".join(sorted(SUBCOMMANDS))
return f"usage: keeper_stress.py <{names.replace(' ', '|')}> [args...]"
def main(argv):
if len(argv) < 2 or argv[1] not in SUBCOMMANDS:
print(_usage(), file=sys.stderr)
return 2
rc = SUBCOMMANDS[argv[1]](argv[2:])
return rc or 0
if __name__ == "__main__":
sys.exit(main(sys.argv))