gitea-saas-core#42: review-gate calibration — measure the false-approve rate before selling auto-merge #47
79
CALIBRATION.md
Normal file
79
CALIBRATION.md
Normal file
@ -0,0 +1,79 @@
|
||||
# CALIBRATION.md — review-gate false-approve rate (issue #42)
|
||||
|
||||
## Outcome
|
||||
|
||||
The number the gold tier's auto-merge promise needs: **how often the review
|
||||
gate approves a change it should have bounced.**
|
||||
|
||||
Measured over a corpus of **25 real farm PRs with known outcomes** (drawn
|
||||
from 11 pbwraith repos' merged and bounced history) plus the two
|
||||
owner-documented regressions the issue cites (botmanager#223's shipper and
|
||||
botmanager !22). The gate under test is the shipped
|
||||
`gitea_saas.reviewer.Reviewer` at its DEFAULT (deterministic) config, run
|
||||
unchanged against recorded inputs — issue body, changed files, diff, and the
|
||||
observed test/hygiene exit from farm history.
|
||||
|
||||
## The number
|
||||
|
||||
| rate | value |
|
||||
|---|---|
|
||||
| false-approve rate (approved, should have bounced) | **0 / 14 = 0.0%** |
|
||||
| false-approve prevalence (over 25 reviewable PRs) | 0.0% |
|
||||
| false-bounce rate (bounced, should have approved) | n/a (0 bounces produced) |
|
||||
| unverifiable — NEEDS_GUIDANCE (fail-closed refusal) | **11 / 25 = 44.0%** |
|
||||
|
||||
On the operator's own terms — the two owner-documented cases added to the 14
|
||||
the gate can evaluate — the false-approve rate is **2 / 16 = 12.5%**.
|
||||
|
||||
## Recommendation: NO
|
||||
|
||||
Gold-tier auto-merge (**auto-merges on a green APPROVE**) is **not safe to
|
||||
sell at this rate**. The 0.0% headline is an artifact of refusal, not of
|
||||
gate quality:
|
||||
|
||||
1. The gate approved only the 14 corpus PRs whose issues carry `- [ ]`
|
||||
checklists (or a section titled exactly `## Acceptance`) and refused the
|
||||
other 11 (44%) — real merged or bounced work it could not verify. A gate
|
||||
that cannot evaluate 44% of real issues cannot back an auto-merge
|
||||
guarantee.
|
||||
2. The approve path has **no check that would have stopped either documented
|
||||
regression**: botmanager#223's break was green under the configured gate
|
||||
(pytest gating a frontend-only change; green on a case-sensitive Linux
|
||||
worktree while the target is case-insensitive Windows) and botmanager !22
|
||||
was green in isolation (no sibling-merge/integration verification). Both
|
||||
mechanisms survive in the approve path today.
|
||||
3. Accident of format, not fail-closed design: botmanager#220 (the shipper)
|
||||
reached NEEDS_GUIDANCE only because its issue titles the section
|
||||
`## Acceptance criteria` with a numbered list, which
|
||||
`parse_acceptance()` does not recognise. With the issue's own real
|
||||
acceptance items parsed (one-line parser accommodation, measured
|
||||
sensitivity), the gate returns **APPROVE on that head — a false approve**.
|
||||
|
||||
Before selling gold auto-merge: (a) close the acceptance-format coverage
|
||||
gap, (b) select the test command/suite from the changed-file set (the
|
||||
gitea-worker-ops !44 direction), (c) add sibling-merge/integration
|
||||
verification, then (d) re-run this calibration on the fixed gate and a
|
||||
larger corpus.
|
||||
|
||||
## How to re-run
|
||||
|
||||
From the repo root (one line, no network, deterministic):
|
||||
|
||||
python tests/calibration/run_calibration.py
|
||||
|
||||
It re-runs the existing reviewer against the committed corpus
|
||||
(`tests/calibration/corpus/*.json` + `tests/calibration/manifest.json`) and
|
||||
rewrites this report of record: `tests/calibration/calibration_report.md`.
|
||||
|
||||
Corpus provenance: the raw Gitea snapshots were fetched read-only on
|
||||
2026-09-01 (see `tests/calibration/build_corpus.py`); the committed corpus
|
||||
is the frozen snapshot the numbers are derived from.
|
||||
|
||||
## Scope compliance
|
||||
|
||||
- New files only: `tests/calibration/` (corpus, runner, builder, report,
|
||||
tests) and this file.
|
||||
- `gitea_saas/reviewer.py` and `gitea_saas/review_policy.py` are **read-only**
|
||||
— not modified. The calibration measures the existing gate; it does not
|
||||
tune it (tuning against a corpus you also authored is how you get a good
|
||||
number and a bad reviewer).
|
||||
269
tests/calibration/build_corpus.py
Normal file
269
tests/calibration/build_corpus.py
Normal file
@ -0,0 +1,269 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the calibration corpus for gitea-saas-core#42.
|
||||
|
||||
Turns the raw Gitea snapshots in /tmp/corpus_raw (fetched 2026-09-01 by
|
||||
corpus_fetch.py, read-only API calls) into the committed corpus under
|
||||
tests/calibration/corpus/ plus tests/calibration/manifest.json.
|
||||
|
||||
The manifest is the curated measurement metadata: for every real PR the
|
||||
KNOWN correct outcome (should_approve / should_bounce), the recorded gate
|
||||
inputs (test exit etc. as observed in farm history), and the category.
|
||||
The corpus JSON files carry the immutable recorded inputs the reviewer is
|
||||
re-run against (issue body, changed files, diff).
|
||||
|
||||
Nothing in here touches gitea_saas/reviewer.py or review_policy.py.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
RAW_DIR = Path("/tmp/corpus_raw")
|
||||
OUT_DIR = Path(__file__).resolve().parent
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Curated manifest: id, repo, pr, known outcome, category, recorded gate inputs
|
||||
# ---------------------------------------------------------------------------
|
||||
# categories:
|
||||
# merged-clean - merged into main, no bounce record
|
||||
# merged-after-bounce - merged; had a bounce cycle earlier in its history
|
||||
# bounced-open - open PR still carrying `bounced` (never merged)
|
||||
# wrong-check-target - owner-documented false-approve (frontend change gated
|
||||
# by a backend-only test run)
|
||||
# sibling-merge-regression - owner-documented false-approve (test green in
|
||||
# isolation, red in the tree after sibling merge)
|
||||
|
||||
COMMON_MERGED = {
|
||||
"reviewable": True,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": ("merged into origin/main (gate green at merge); "
|
||||
"repo main suite: 235 passed on 2026-09-01"),
|
||||
}
|
||||
|
||||
MANIFEST = [
|
||||
# -- merged, clean -------------------------------------------------------
|
||||
dict(id="gitea-saas-core-10", repo="gitea-saas-core", pr=10,
|
||||
known_outcome="should_approve", category="merged-clean",
|
||||
note="Billing ledger core (#1); merged; allowed-file set respected.",
|
||||
**COMMON_MERGED),
|
||||
dict(id="gitea-saas-core-19", repo="gitea-saas-core", pr=19,
|
||||
known_outcome="should_approve", category="merged-clean",
|
||||
note="Billing core client API consistency (#16); merged.",
|
||||
**COMMON_MERGED),
|
||||
dict(id="gitea-saas-core-27", repo="gitea-saas-core", pr=27,
|
||||
known_outcome="should_approve", category="merged-clean",
|
||||
note="X-API-Key auth consolidation (#24); merged.",
|
||||
**COMMON_MERGED),
|
||||
dict(id="botmanager-191", repo="botmanager", pr=191,
|
||||
known_outcome="should_approve", category="merged-clean",
|
||||
note="fix(#188) no fabricated RSNs; merged.",
|
||||
**COMMON_MERGED),
|
||||
dict(id="botmanager-200", repo="botmanager", pr=200,
|
||||
known_outcome="should_approve", category="merged-clean",
|
||||
note="Closes #198 display_name backfill; merged.",
|
||||
**COMMON_MERGED),
|
||||
dict(id="botmanager-209", repo="botmanager", pr=209,
|
||||
known_outcome="should_approve", category="merged-after-bounce",
|
||||
note="Closes #208 deploy_live --check; two bounce-fix attempts "
|
||||
"(review comments record pytest 1184 passed) before merge.",
|
||||
**COMMON_MERGED),
|
||||
dict(id="botmanager-231", repo="botmanager", pr=231,
|
||||
known_outcome="should_approve", category="merged-clean",
|
||||
note="feat(#226) responsive fleet row; merged.",
|
||||
**COMMON_MERGED),
|
||||
dict(id="speakeasy-41", repo="speakeasy", pr=41,
|
||||
known_outcome="should_approve", category="merged-clean",
|
||||
note="fix(#38) route through Windows curl.exe from WSL; merged.",
|
||||
**COMMON_MERGED),
|
||||
dict(id="speakeasy-43", repo="speakeasy", pr=43,
|
||||
known_outcome="should_approve", category="merged-clean",
|
||||
note="fix(#42) memberExpireDays into profile; merged.",
|
||||
**COMMON_MERGED),
|
||||
dict(id="nexus-aio-61", repo="nexus-aio", pr=61,
|
||||
known_outcome="should_approve", category="merged-clean",
|
||||
note="refactor(#60) activity registry; merged.",
|
||||
**COMMON_MERGED),
|
||||
dict(id="nexus-aio-98", repo="nexus-aio", pr=98,
|
||||
known_outcome="should_approve", category="merged-clean",
|
||||
note="fix(#94) normalise game text before comparison; merged.",
|
||||
**COMMON_MERGED),
|
||||
dict(id="rs-pilot-42", repo="rs-pilot", pr=42,
|
||||
known_outcome="should_approve", category="merged-clean",
|
||||
note="feat(#41) offline dry-run mode; merged.",
|
||||
**COMMON_MERGED),
|
||||
dict(id="rs-pilot-49", repo="rs-pilot", pr=49,
|
||||
known_outcome="should_approve", category="merged-clean",
|
||||
note="fix(#47) rank prompt candidates by track record; merged.",
|
||||
**COMMON_MERGED),
|
||||
dict(id="after-hours-vn-19", repo="after-hours-vn", pr=19,
|
||||
known_outcome="should_approve", category="merged-clean",
|
||||
note="fix(#17) SDK-only import guard; merged.",
|
||||
**COMMON_MERGED),
|
||||
dict(id="gitea-worker-ops-43", repo="gitea-worker-ops", pr=43,
|
||||
known_outcome="should_approve", category="merged-clean",
|
||||
note="fix(#42) dispatcher capacity double-subtraction; merged.",
|
||||
**COMMON_MERGED),
|
||||
dict(id="dice-and-deeds-27", repo="dice-and-deeds", pr=27,
|
||||
known_outcome="should_approve", category="merged-clean",
|
||||
note="test(#17) one-command fresh-checkout suite; merged.",
|
||||
**COMMON_MERGED),
|
||||
dict(id="microkit-10", repo="microkit", pr=10,
|
||||
known_outcome="should_approve", category="merged-clean",
|
||||
note="feat(#9) F2P alcher mode; merged.",
|
||||
**COMMON_MERGED),
|
||||
dict(id="gopher64-netplay-21", repo="gopher64-netplay", pr=21,
|
||||
known_outcome="should_approve", category="merged-clean",
|
||||
note="Add .gitattributes LF fix (#19); merged.",
|
||||
**COMMON_MERGED),
|
||||
# -- merged after a bounce cycle ----------------------------------------
|
||||
dict(id="gitea-saas-core-22", repo="gitea-saas-core", pr=22,
|
||||
known_outcome="should_approve", category="merged-after-bounce",
|
||||
note="SaaS dashboard (#9); merged AFTER a bounce cycle (bounced "
|
||||
"label still attached). Final landed state is the fixed head.",
|
||||
**COMMON_MERGED),
|
||||
dict(id="bot-contracts-8", repo="bot-contracts", pr=8,
|
||||
known_outcome="should_approve", category="merged-after-bounce",
|
||||
note="fix(#7) repo-relative evidence paths; merged after a bounce "
|
||||
"cycle (bounced label still attached).",
|
||||
**COMMON_MERGED),
|
||||
# -- open, bounced (never merged) ---------------------------------------
|
||||
dict(id="nexus-aio-40", repo="nexus-aio", pr=40,
|
||||
known_outcome="should_bounce", category="bounced-open",
|
||||
reviewable=True,
|
||||
test_exit=None, hygiene_exit=0,
|
||||
test_exit_source="not recorded in farm history (bounce decided by "
|
||||
"human/LLM review findings, not a test failure)",
|
||||
note="Closes #10; open PR with bounced+needs-guidance labels."),
|
||||
dict(id="nexus-aio-44", repo="nexus-aio", pr=44,
|
||||
known_outcome="should_bounce", category="bounced-open",
|
||||
reviewable=True,
|
||||
test_exit=None, hygiene_exit=0,
|
||||
test_exit_source="not recorded in farm history (bounce decided by "
|
||||
"human/LLM review findings, not a test failure)",
|
||||
note="[skill] Attack (combat shape) Closes #22; bounced+needs-guidance."),
|
||||
dict(id="gopher64-netplay-9", repo="gopher64-netplay", pr=9,
|
||||
known_outcome="should_bounce", category="bounced-open",
|
||||
reviewable=True,
|
||||
test_exit=None, hygiene_exit=0,
|
||||
test_exit_source="not recorded in farm history (bounce decided by "
|
||||
"human/LLM review findings, not a test failure)",
|
||||
note="Fix GGRS frame counter reset (#5); bounced+manual-only. Diff "
|
||||
"392KB truncated to 150KB in corpus."),
|
||||
dict(id="gopher64-netplay-18", repo="gopher64-netplay", pr=18,
|
||||
known_outcome="should_bounce", category="bounced-open",
|
||||
reviewable=True,
|
||||
test_exit=None, hygiene_exit=0,
|
||||
test_exit_source="not recorded in farm history (bounce decided by "
|
||||
"human/LLM review findings, not a test failure)",
|
||||
note="netplay session resume (#3); bounced+manual-only. Diff 454KB "
|
||||
"truncated to 150KB in corpus."),
|
||||
# -- owner-documented false-approve case 1: wrong check target ----------
|
||||
dict(id="botmanager-220", repo="botmanager", pr=220,
|
||||
reviewable=True,
|
||||
known_outcome="should_bounce", category="wrong-check-target",
|
||||
test_exit=0, hygiene_exit=0,
|
||||
test_exit_source=("owner-documented: the configured gate reported "
|
||||
"GREEN on this frontend-only change (gate ran pytest "
|
||||
"against a change pytest cannot test). PR review "
|
||||
"comments additionally record npm ci + npm test "
|
||||
"100/100 + npm run build green on a LINUX review "
|
||||
"worktree. The crash (FleetSummary components "
|
||||
"undefined) manifests on case-insensitive Windows "
|
||||
"filesystems only: botmanager#223."),
|
||||
note=("Merged via 'feat(#215): action-first Operator Fleet workspace "
|
||||
"redesign'. It shipped the undefined-component break that "
|
||||
"botmanager#223 measured on origin/master AND the review-gate "
|
||||
"fix landed in botmanager-224/gitea-worker-ops !44. The correct "
|
||||
"outcome was BOUNCE. Recorded inputs are the actual reviewed "
|
||||
"head (7f350a1)."),
|
||||
acceptance_override_farm=(
|
||||
"Acceptance bullets parsed from the issue's '## Acceptance "
|
||||
"criteria' section, which the shipped parse_acceptance() does not "
|
||||
"recognise (it matches only '## Acceptance' or '- [ ]' "
|
||||
"checklists). Farm-configured-gate sensitivity only; not the "
|
||||
"default measurement.")),
|
||||
# -- owner-documented false-approve case 2: sibling-merge regression -----
|
||||
dict(id="botmanager-bang22", repo="botmanager", pr=None,
|
||||
reviewable=False,
|
||||
known_outcome="should_bounce", category="sibling-merge-regression",
|
||||
original_ref="botmanager !22 (original tracker numbering, pre-Gitea "
|
||||
"renumbering; first-era Gitea PR #22 is not retrievable "
|
||||
"from the current repo)",
|
||||
test_exit_source=("owner-documented: the PR's own scope-guard test "
|
||||
"passed IN ISOLATION; it could never pass once a "
|
||||
"sibling PR merged - green in isolation, red in the "
|
||||
"tree."),
|
||||
note=("No diff/issue snapshot survives on this Gitea instance (PRs "
|
||||
"are numbered from 119; first-era PRs 1-118 existed - see "
|
||||
"pr_bm_*.json artifacts - but are not retrievable). Recorded "
|
||||
"here as the owner documented it (gitea-saas-core#42 body). "
|
||||
"Closest surviving Gitea record of the same class: botmanager "
|
||||
"#201, bounced at head 4b4aebc because its branch was cut "
|
||||
"before sibling #200 merged and the rebase dropped #200's code "
|
||||
"- its own 22 new tests passed in isolation.")),
|
||||
]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
entries = []
|
||||
for m in MANIFEST:
|
||||
entry = dict(**m)
|
||||
if m.get("reviewable", False):
|
||||
raw = json.loads((RAW_DIR / f"{m['repo']}-{m['pr']}.json").read_text())
|
||||
corpus = {
|
||||
"id": m["id"],
|
||||
"repo": m["repo"],
|
||||
"pr": m["pr"],
|
||||
"title": raw["title"],
|
||||
"head_sha": raw["head_sha"],
|
||||
"base_sha": raw["base_sha"],
|
||||
"labels": raw["labels"],
|
||||
"merged": raw["merged"],
|
||||
"issue_number": raw["issue"],
|
||||
"issue_body": raw["issue_body"],
|
||||
"changed_files": raw["changed_files"],
|
||||
"diff": raw["diff"],
|
||||
"diff_truncated": raw["diff_truncated"],
|
||||
"diff_bytes": raw["diff_bytes"],
|
||||
}
|
||||
if m["id"] == "botmanager-220" and "acceptance_override_farm" in m:
|
||||
corpus["acceptance_override_farm"] = _farm_acceptance(
|
||||
raw["issue_body"] or "")
|
||||
(OUT_DIR / "corpus" / f"{m['id']}.json").write_text(
|
||||
json.dumps(corpus, indent=1))
|
||||
entries.append(entry)
|
||||
(OUT_DIR / "manifest.json").write_text(json.dumps(
|
||||
{"generated_utc": "2026-09-01T11:20:00Z",
|
||||
"source": ("pbwraith Gitea API snapshots fetched 2026-09-01 "
|
||||
"(read-only); see build_corpus.py"),
|
||||
"entries": entries}, indent=1))
|
||||
print(f"wrote {len(entries)} manifest entries, "
|
||||
f"{sum(1 for e in entries if e.get('reviewable'))} reviewable")
|
||||
|
||||
|
||||
def _farm_acceptance(issue_body: str) -> list[str]:
|
||||
"""Extract acceptance bullets the farm's own review used.
|
||||
|
||||
Matches '## Acceptance criteria' / '## Acceptance' sections with '- '
|
||||
bullets (the farm's format), verbatim from the issue body. Used ONLY for
|
||||
the clearly-labelled farm-configured sensitivity pass.
|
||||
"""
|
||||
m = re.search(r"(?im)^#{1,3}\s*Acceptance[^\n]*$([\s\S]*?)(?=^#{1,3}\s|\Z)",
|
||||
issue_body)
|
||||
if not m:
|
||||
return []
|
||||
items = []
|
||||
for line in m.group(1).splitlines():
|
||||
s = line.strip()
|
||||
if s.startswith(("-", "*")) and s not in ("-", "*"):
|
||||
items.append(s.lstrip("-*").strip())
|
||||
elif re.match(r"^\d+[.)]\s+", s):
|
||||
items.append(re.sub(r"^\d+[.)]\s+", "", s))
|
||||
return items
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
99
tests/calibration/calibration_report.md
Normal file
99
tests/calibration/calibration_report.md
Normal file
@ -0,0 +1,99 @@
|
||||
# Review-gate calibration report (gitea-saas-core#42)
|
||||
|
||||
- Generated: 2026-09-01T11:20:00Z (re-derive with `python tests/calibration/run_calibration.py`)
|
||||
- Corpus: 25 reviewable real farm PRs + 1 owner-documented case(s), drawn from 11 pbwraith repos
|
||||
- Gate under test: `gitea_saas.reviewer.Reviewer`, DEFAULT (deterministic) config - unchanged code, run against recorded inputs (issue body, changed files, diff, observed test/hygiene exit)
|
||||
|
||||
## Headline rates
|
||||
|
||||
| rate | value | denominator |
|
||||
|---|---|---|
|
||||
| false-approve rate | **0.0%** | 14 gate approvals |
|
||||
| false-approve prevalence | 0.0% | 25 reviewable PRs |
|
||||
| false-bounce rate | n/a | 0 gate bounces |
|
||||
| unverifiable (NEEDS_GUIDANCE) | 44.0% | 25 reviewable PRs |
|
||||
|
||||
## Verdict matrix
|
||||
|
||||
- correct approves: 14
|
||||
- **false approves (approved, should have bounced): 0**
|
||||
- correct bounces: 0
|
||||
- false bounces (bounced, should have approved): 0
|
||||
- NEEDS_GUIDANCE (gate refused to verify): 11
|
||||
|
||||
## Per-entry results
|
||||
|
||||
| id | repo#pr | known | verdict | bucket | category |
|
||||
|---|---|---|---|---|---|
|
||||
| after-hours-vn-19 | after-hours-vn#19 | approve | APPROVE | correct-approve | merged-clean |
|
||||
| botmanager-191 | botmanager#191 | approve | APPROVE | correct-approve | merged-clean |
|
||||
| botmanager-200 | botmanager#200 | approve | APPROVE | correct-approve | merged-clean |
|
||||
| dice-and-deeds-27 | dice-and-deeds#27 | approve | APPROVE | correct-approve | merged-clean |
|
||||
| gitea-saas-core-10 | gitea-saas-core#10 | approve | APPROVE | correct-approve | merged-clean |
|
||||
| gitea-saas-core-19 | gitea-saas-core#19 | approve | APPROVE | correct-approve | merged-clean |
|
||||
| gitea-saas-core-22 | gitea-saas-core#22 | approve | APPROVE | correct-approve | merged-after-bounce |
|
||||
| microkit-10 | microkit#10 | approve | APPROVE | correct-approve | merged-clean |
|
||||
| nexus-aio-61 | nexus-aio#61 | approve | APPROVE | correct-approve | merged-clean |
|
||||
| nexus-aio-98 | nexus-aio#98 | approve | APPROVE | correct-approve | merged-clean |
|
||||
| rs-pilot-42 | rs-pilot#42 | approve | APPROVE | correct-approve | merged-clean |
|
||||
| rs-pilot-49 | rs-pilot#49 | approve | APPROVE | correct-approve | merged-clean |
|
||||
| speakeasy-41 | speakeasy#41 | approve | APPROVE | correct-approve | merged-clean |
|
||||
| speakeasy-43 | speakeasy#43 | approve | APPROVE | correct-approve | merged-clean |
|
||||
| bot-contracts-8 | bot-contracts#8 | approve | NEEDS_GUIDANCE | guidance | merged-after-bounce |
|
||||
| botmanager-209 | botmanager#209 | approve | NEEDS_GUIDANCE | guidance | merged-after-bounce |
|
||||
| botmanager-220 | botmanager#220 | bounce | NEEDS_GUIDANCE | guidance | wrong-check-target |
|
||||
| botmanager-231 | botmanager#231 | approve | NEEDS_GUIDANCE | guidance | merged-clean |
|
||||
| gitea-saas-core-27 | gitea-saas-core#27 | approve | NEEDS_GUIDANCE | guidance | merged-clean |
|
||||
| gitea-worker-ops-43 | gitea-worker-ops#43 | approve | NEEDS_GUIDANCE | guidance | merged-clean |
|
||||
| gopher64-netplay-18 | gopher64-netplay#18 | bounce | NEEDS_GUIDANCE | guidance | bounced-open |
|
||||
| gopher64-netplay-21 | gopher64-netplay#21 | approve | NEEDS_GUIDANCE | guidance | merged-clean |
|
||||
| gopher64-netplay-9 | gopher64-netplay#9 | bounce | NEEDS_GUIDANCE | guidance | bounced-open |
|
||||
| nexus-aio-40 | nexus-aio#40 | bounce | NEEDS_GUIDANCE | guidance | bounced-open |
|
||||
| nexus-aio-44 | nexus-aio#44 | bounce | NEEDS_GUIDANCE | guidance | bounced-open |
|
||||
|
||||
## Per-category breakdown
|
||||
|
||||
| category | total | false-approve | false-bounce | guidance |
|
||||
|---|---|---|---|---|
|
||||
| bounced-open | 4 | 0 | 0 | 4 |
|
||||
| merged-after-bounce | 3 | 0 | 0 | 2 |
|
||||
| merged-clean | 17 | 0 | 0 | 4 |
|
||||
| wrong-check-target | 1 | 0 | 0 | 1 |
|
||||
|
||||
## Owner-documented cases (gitea-saas-core#42)
|
||||
|
||||
The two farm regressions the issue cites are in the corpus with their recorded outcomes. Both had a GREEN gate in farm history; both shipped or would have shipped a broken tree:
|
||||
|
||||
### 1. botmanager !220 (`botmanager#220`) - wrong check target
|
||||
|
||||
- Known outcome: **should_bounce** (it shipped the undefined-component break measured in botmanager#223; the review-gate fix landed in botmanager#224 / gitea-worker-ops !44).
|
||||
- Recorded gate inputs: test exit 0 (pytest on a frontend-only change; review comments record npm ci / npm test 100/100 / build green on a LINUX worktree - the break manifests on case-insensitive Windows).
|
||||
- Product gate verdict on the recorded inputs: NEEDS_GUIDANCE (accepted by accident: the issue's acceptance section is titled '## Acceptance criteria' with a numbered list, which the shipped parse_acceptance() does not recognise - '## Acceptance' / '- [ ]' only).
|
||||
- Farm-configured-parser sensitivity (the issue's own real acceptance items, 8 parsed): verdict **APPROVE** -> **FALSE-APPROVE**. I.e. with a one-line parser accommodation the gate auto-merges a change that broke master.
|
||||
|
||||
### 2. botmanager !22 (original tracker ref) - sibling-merge regression
|
||||
|
||||
- Known outcome: **should_bounce** (owner-documented: the PR's own scope-guard test passed in isolation and could never pass once a sibling PR merged - green in isolation, red in the tree).
|
||||
- Not re-runnable: recorded as owner-documented (original head data not retrievable) (first-era PR #22 predates the current repo's renumbering; closest surviving record of the same class is botmanager#201, bounced at head 4b4aebc after a rebase silently dropped sibling #200's code while its own 22 new tests passed in isolation).
|
||||
|
||||
## Coverage: why 11 of 25 reviewable PRs get NEEDS_GUIDANCE
|
||||
|
||||
The shipped `parse_acceptance()` reads `- [ ]` checklists anywhere or bullets under a section titled exactly `## Acceptance`. Farm issues mix formats: `## Acceptance criteria` (numbered), `## Scope` bullets, or no acceptance section at all. 11 corpus issues use one of those formats, so the gate refuses to verify them (fail-closed, never silent-approve). That is SAFE but means the gate can evaluate only ~56% of this farm's real issues.
|
||||
|
||||
## Limitations
|
||||
|
||||
- Recorded test exits are reconstructed from farm history (merged implies gate-green at merge; bounced entries' exits were not recorded - their bounce was decided by human/LLM review findings the deterministic gate's inputs cannot represent).
|
||||
- Diffs are current-head snapshots (two >150KB gopher64 diffs truncated); merged PRs' diffs approximate the reviewed content.
|
||||
- The deterministic gate is measured; the farm's LLM-augmented review is not re-run (nondeterministic, and the issue's instruction is: measure the existing reviewer).
|
||||
|
||||
## Recommendation
|
||||
|
||||
**NO - gold-tier auto-merge is not safe to sell at this rate (0.0% false-approve rate (0/14)).**
|
||||
|
||||
Reasons, each backed by the corpus:
|
||||
1. **Zero is an artifact of refusal, not of quality.** The gate approved only the 14 checklist-formatted issues and refused the other 11 (44%) real farm PRs it could not verify. A gate that cannot evaluate 44% of real work cannot back an auto-merge guarantee.
|
||||
2. **The approve path has no check that would have stopped either documented regression.** botmanager#223's break was green under the configured gate (wrong suite: pytest vs frontend change; green on case-sensitive Linux vs case-insensitive Windows target) and botmanager !22 was green in isolation (no sibling-merge/integration verification). Both mechanisms survive in the gate's approve path today.
|
||||
3. **On the operator's own terms the rate is 2/16 = 12.5%.** Add the two owner-documented cases to the 14 the gate can evaluate: both had green gate inputs and both should have bounced (sensitivity pass on botmanager#220 -> FALSE-APPROVE; botmanager !22 documented).
|
||||
4. **Accidental safety is not a product.** botmanager#220 reached NEEDS_GUIDANCE only because its issue says 'Acceptance criteria' instead of 'Acceptance' - bad luck, not fail-closed design.
|
||||
|
||||
Before selling gold auto-merge, close, in order: (a) the acceptance format coverage gap, (b) test-command/target-suite selection per changed-file set (the gitea-worker-ops !44 direction), (c) sibling-merge/integration verification, and (d) re-run this calibration on the fixed gate and on a larger corpus.
|
||||
20
tests/calibration/corpus/after-hours-vn-19.json
Normal file
20
tests/calibration/corpus/after-hours-vn-19.json
Normal file
File diff suppressed because one or more lines are too long
23
tests/calibration/corpus/bot-contracts-8.json
Normal file
23
tests/calibration/corpus/bot-contracts-8.json
Normal file
File diff suppressed because one or more lines are too long
20
tests/calibration/corpus/botmanager-191.json
Normal file
20
tests/calibration/corpus/botmanager-191.json
Normal file
File diff suppressed because one or more lines are too long
21
tests/calibration/corpus/botmanager-200.json
Normal file
21
tests/calibration/corpus/botmanager-200.json
Normal file
File diff suppressed because one or more lines are too long
19
tests/calibration/corpus/botmanager-209.json
Normal file
19
tests/calibration/corpus/botmanager-209.json
Normal file
File diff suppressed because one or more lines are too long
51
tests/calibration/corpus/botmanager-220.json
Normal file
51
tests/calibration/corpus/botmanager-220.json
Normal file
File diff suppressed because one or more lines are too long
27
tests/calibration/corpus/botmanager-231.json
Normal file
27
tests/calibration/corpus/botmanager-231.json
Normal file
File diff suppressed because one or more lines are too long
25
tests/calibration/corpus/dice-and-deeds-27.json
Normal file
25
tests/calibration/corpus/dice-and-deeds-27.json
Normal file
File diff suppressed because one or more lines are too long
23
tests/calibration/corpus/gitea-saas-core-10.json
Normal file
23
tests/calibration/corpus/gitea-saas-core-10.json
Normal file
File diff suppressed because one or more lines are too long
19
tests/calibration/corpus/gitea-saas-core-19.json
Normal file
19
tests/calibration/corpus/gitea-saas-core-19.json
Normal file
File diff suppressed because one or more lines are too long
21
tests/calibration/corpus/gitea-saas-core-22.json
Normal file
21
tests/calibration/corpus/gitea-saas-core-22.json
Normal file
File diff suppressed because one or more lines are too long
28
tests/calibration/corpus/gitea-saas-core-27.json
Normal file
28
tests/calibration/corpus/gitea-saas-core-27.json
Normal file
File diff suppressed because one or more lines are too long
19
tests/calibration/corpus/gitea-worker-ops-43.json
Normal file
19
tests/calibration/corpus/gitea-worker-ops-43.json
Normal file
File diff suppressed because one or more lines are too long
50
tests/calibration/corpus/gopher64-netplay-18.json
Normal file
50
tests/calibration/corpus/gopher64-netplay-18.json
Normal file
File diff suppressed because one or more lines are too long
18
tests/calibration/corpus/gopher64-netplay-21.json
Normal file
18
tests/calibration/corpus/gopher64-netplay-21.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "gopher64-netplay-21",
|
||||
"repo": "gopher64-netplay",
|
||||
"pr": 21,
|
||||
"title": "Add .gitattributes: *.sh text eol=lf for Windows clones (#19)",
|
||||
"head_sha": "cfa469428cd691f8e9814956b340989ccb4743ef",
|
||||
"base_sha": "c857feada3a9ee838aed425ac5602a94cc61523a",
|
||||
"labels": [],
|
||||
"merged": true,
|
||||
"issue_number": 19,
|
||||
"issue_body": "## `scripts/gate.sh` breaks in any Windows clone, because this repo has no `.gitattributes`\n\nSmall, cheap, and worth fixing now that `scripts/gate.sh` is the repo's keystone - eight open PRs are gated by it, so anything that makes it fail confusingly costs more here than it would elsewhere.\n\n## The measurement\n\n```\n$ git ls-tree origin/main --name-only | grep gitattributes\n(nothing)\n```\n\nNo `.gitattributes` on `main`, and none on `issue-16` either. The repo ships three shell scripts:\n\n```\ndata/icon/generate.sh\ndata/shader/output.sh\nscripts/gate.sh\n```\n\nThe committed blobs are clean - `git cat-file -p origin/issue-16:scripts/gate.sh | tr -cd '\\r' | wc -c` is **0**. The problem is not what is stored, it is what gets written on checkout.\n\n## Why it bites\n\nGit for Windows defaults to `core.autocrlf=true`. With no `.gitattributes` to override it, cloning this repo on Windows writes CRLF into every `.sh` working copy, and the script then dies at exec:\n\n```\n$ ./scripts/gate.sh\n/usr/bin/env: 'bash\\r': No such file or directory\nEXIT=127\n```\n\nThis already happened during review of `!17`: the first gate run failed exactly this way, and it took a round of investigation to establish it was a clone artifact rather than a defect in the PR. `env: 'bash\\r'` does not name line endings, and the natural reading is \"the script is broken\" - which is the wrong conclusion about a script whose blob is byte-clean.\n\nThe workaround is `git config core.autocrlf false` plus a fresh checkout, but that is per-clone tribal knowledge. The repo should assert its own requirement.\n\n## What to do\n\nAdd `.gitattributes` at the repo root:\n\n```\n*.sh text eol=lf\n```\n\nConsider `* text=auto` alongside it if you want the general rule, but the `.sh` line is the one that fixes the observed failure.\n\n## The part that is easy to get wrong\n\n**`.gitattributes` alone does not fix an already-checked-out working copy.** Git does not rewrite a file on branch switch when its blob is unchanged, so a clone that already has CRLF in `scripts/gate.sh` will still have CRLF after merging this change - on a branch whose blobs are perfectly clean. Existing clones need a forced re-checkout:\n\n```\nrm -f .git/index && git reset --hard HEAD\n```\n\nIf any committed blob *did* contain CR (none do here, so this should be a no-op), fixing it needs `git add --renormalize .` as a separate commit - the attributes file does not retroactively clean stored objects.\n\n## Acceptance criteria\n\n1. `.gitattributes` exists on `main` and contains `*.sh text eol=lf`.\n2. On a **fresh Windows clone made with `core.autocrlf=true`** (the default - set it explicitly for the test rather than relying on the box's config), `tr -cd '\\r' < scripts/gate.sh | wc -c` returns **0**. State the command and the number.\n3. `head -c 24 scripts/gate.sh | od -c` shows a clean `#!/usr/bin/env bash` shebang with no `\\r`.\n4. All three `.sh` files are covered, not just `scripts/gate.sh`.\n\n## Measurement warning\n\nDo **not** verify this with `grep -c $'\\r'`. It is broken in this environment's Git Bash and returns the *line count*, not the CR count - it has produced false findings on three separate occasions here, and the tell is that the number always equals `wc -l`. Use `tr -cd '\\r' < file | wc -c`, and treat actually running `./scripts/gate.sh --help` (or any invocation that reaches the interpreter) as the deciding test.\n\n## Out of scope\n\n- Any change to `scripts/gate.sh`'s contents or to the build configuration. This is line endings only.\n- The disk-space blocker currently preventing the gate from completing (gitea-worker-ops#24). This issue is verifiable by inspection and a fresh clone; it does not need a working build.\n",
|
||||
"changed_files": [
|
||||
".gitattributes"
|
||||
],
|
||||
"diff": "diff --git a/.gitattributes b/.gitattributes\nnew file mode 100644\nindex 0000000..e4ba11e\n--- /dev/null\n+++ b/.gitattributes\n@@ -0,0 +1,7 @@\n+# Force LF line endings for shell scripts.\n+# Without this, Windows clones (git's core.autocrlf=true default) check out\n+# CRLF and the shebang dies with: /usr/bin/env: 'bash\\r': No such file or directory (exit 127)\n+# Keep every *.sh blob byte-clean; existing clones need a forced re-checkout\n+# (rm -f .git/index && git reset --hard HEAD) because git does not rewrite a\n+# file whose blob is unchanged.\n+*.sh text eol=lf\n",
|
||||
"diff_truncated": false,
|
||||
"diff_bytes": 566
|
||||
}
|
||||
50
tests/calibration/corpus/gopher64-netplay-9.json
Normal file
50
tests/calibration/corpus/gopher64-netplay-9.json
Normal file
File diff suppressed because one or more lines are too long
22
tests/calibration/corpus/microkit-10.json
Normal file
22
tests/calibration/corpus/microkit-10.json
Normal file
File diff suppressed because one or more lines are too long
26
tests/calibration/corpus/nexus-aio-40.json
Normal file
26
tests/calibration/corpus/nexus-aio-40.json
Normal file
File diff suppressed because one or more lines are too long
26
tests/calibration/corpus/nexus-aio-44.json
Normal file
26
tests/calibration/corpus/nexus-aio-44.json
Normal file
File diff suppressed because one or more lines are too long
39
tests/calibration/corpus/nexus-aio-61.json
Normal file
39
tests/calibration/corpus/nexus-aio-61.json
Normal file
File diff suppressed because one or more lines are too long
22
tests/calibration/corpus/nexus-aio-98.json
Normal file
22
tests/calibration/corpus/nexus-aio-98.json
Normal file
File diff suppressed because one or more lines are too long
22
tests/calibration/corpus/rs-pilot-42.json
Normal file
22
tests/calibration/corpus/rs-pilot-42.json
Normal file
File diff suppressed because one or more lines are too long
22
tests/calibration/corpus/rs-pilot-49.json
Normal file
22
tests/calibration/corpus/rs-pilot-49.json
Normal file
File diff suppressed because one or more lines are too long
19
tests/calibration/corpus/speakeasy-41.json
Normal file
19
tests/calibration/corpus/speakeasy-41.json
Normal file
File diff suppressed because one or more lines are too long
21
tests/calibration/corpus/speakeasy-43.json
Normal file
21
tests/calibration/corpus/speakeasy-43.json
Normal file
File diff suppressed because one or more lines are too long
318
tests/calibration/manifest.json
Normal file
318
tests/calibration/manifest.json
Normal file
@ -0,0 +1,318 @@
|
||||
{
|
||||
"generated_utc": "2026-09-01T11:20:00Z",
|
||||
"source": "pbwraith Gitea API snapshots fetched 2026-09-01 (read-only); see build_corpus.py",
|
||||
"entries": [
|
||||
{
|
||||
"id": "gitea-saas-core-10",
|
||||
"repo": "gitea-saas-core",
|
||||
"pr": 10,
|
||||
"known_outcome": "should_approve",
|
||||
"category": "merged-clean",
|
||||
"note": "Billing ledger core (#1); merged; allowed-file set respected.",
|
||||
"reviewable": true,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "merged into origin/main (gate green at merge); repo main suite: 235 passed on 2026-09-01"
|
||||
},
|
||||
{
|
||||
"id": "gitea-saas-core-19",
|
||||
"repo": "gitea-saas-core",
|
||||
"pr": 19,
|
||||
"known_outcome": "should_approve",
|
||||
"category": "merged-clean",
|
||||
"note": "Billing core client API consistency (#16); merged.",
|
||||
"reviewable": true,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "merged into origin/main (gate green at merge); repo main suite: 235 passed on 2026-09-01"
|
||||
},
|
||||
{
|
||||
"id": "gitea-saas-core-27",
|
||||
"repo": "gitea-saas-core",
|
||||
"pr": 27,
|
||||
"known_outcome": "should_approve",
|
||||
"category": "merged-clean",
|
||||
"note": "X-API-Key auth consolidation (#24); merged.",
|
||||
"reviewable": true,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "merged into origin/main (gate green at merge); repo main suite: 235 passed on 2026-09-01"
|
||||
},
|
||||
{
|
||||
"id": "botmanager-191",
|
||||
"repo": "botmanager",
|
||||
"pr": 191,
|
||||
"known_outcome": "should_approve",
|
||||
"category": "merged-clean",
|
||||
"note": "fix(#188) no fabricated RSNs; merged.",
|
||||
"reviewable": true,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "merged into origin/main (gate green at merge); repo main suite: 235 passed on 2026-09-01"
|
||||
},
|
||||
{
|
||||
"id": "botmanager-200",
|
||||
"repo": "botmanager",
|
||||
"pr": 200,
|
||||
"known_outcome": "should_approve",
|
||||
"category": "merged-clean",
|
||||
"note": "Closes #198 display_name backfill; merged.",
|
||||
"reviewable": true,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "merged into origin/main (gate green at merge); repo main suite: 235 passed on 2026-09-01"
|
||||
},
|
||||
{
|
||||
"id": "botmanager-209",
|
||||
"repo": "botmanager",
|
||||
"pr": 209,
|
||||
"known_outcome": "should_approve",
|
||||
"category": "merged-after-bounce",
|
||||
"note": "Closes #208 deploy_live --check; two bounce-fix attempts (review comments record pytest 1184 passed) before merge.",
|
||||
"reviewable": true,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "merged into origin/main (gate green at merge); repo main suite: 235 passed on 2026-09-01"
|
||||
},
|
||||
{
|
||||
"id": "botmanager-231",
|
||||
"repo": "botmanager",
|
||||
"pr": 231,
|
||||
"known_outcome": "should_approve",
|
||||
"category": "merged-clean",
|
||||
"note": "feat(#226) responsive fleet row; merged.",
|
||||
"reviewable": true,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "merged into origin/main (gate green at merge); repo main suite: 235 passed on 2026-09-01"
|
||||
},
|
||||
{
|
||||
"id": "speakeasy-41",
|
||||
"repo": "speakeasy",
|
||||
"pr": 41,
|
||||
"known_outcome": "should_approve",
|
||||
"category": "merged-clean",
|
||||
"note": "fix(#38) route through Windows curl.exe from WSL; merged.",
|
||||
"reviewable": true,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "merged into origin/main (gate green at merge); repo main suite: 235 passed on 2026-09-01"
|
||||
},
|
||||
{
|
||||
"id": "speakeasy-43",
|
||||
"repo": "speakeasy",
|
||||
"pr": 43,
|
||||
"known_outcome": "should_approve",
|
||||
"category": "merged-clean",
|
||||
"note": "fix(#42) memberExpireDays into profile; merged.",
|
||||
"reviewable": true,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "merged into origin/main (gate green at merge); repo main suite: 235 passed on 2026-09-01"
|
||||
},
|
||||
{
|
||||
"id": "nexus-aio-61",
|
||||
"repo": "nexus-aio",
|
||||
"pr": 61,
|
||||
"known_outcome": "should_approve",
|
||||
"category": "merged-clean",
|
||||
"note": "refactor(#60) activity registry; merged.",
|
||||
"reviewable": true,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "merged into origin/main (gate green at merge); repo main suite: 235 passed on 2026-09-01"
|
||||
},
|
||||
{
|
||||
"id": "nexus-aio-98",
|
||||
"repo": "nexus-aio",
|
||||
"pr": 98,
|
||||
"known_outcome": "should_approve",
|
||||
"category": "merged-clean",
|
||||
"note": "fix(#94) normalise game text before comparison; merged.",
|
||||
"reviewable": true,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "merged into origin/main (gate green at merge); repo main suite: 235 passed on 2026-09-01"
|
||||
},
|
||||
{
|
||||
"id": "rs-pilot-42",
|
||||
"repo": "rs-pilot",
|
||||
"pr": 42,
|
||||
"known_outcome": "should_approve",
|
||||
"category": "merged-clean",
|
||||
"note": "feat(#41) offline dry-run mode; merged.",
|
||||
"reviewable": true,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "merged into origin/main (gate green at merge); repo main suite: 235 passed on 2026-09-01"
|
||||
},
|
||||
{
|
||||
"id": "rs-pilot-49",
|
||||
"repo": "rs-pilot",
|
||||
"pr": 49,
|
||||
"known_outcome": "should_approve",
|
||||
"category": "merged-clean",
|
||||
"note": "fix(#47) rank prompt candidates by track record; merged.",
|
||||
"reviewable": true,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "merged into origin/main (gate green at merge); repo main suite: 235 passed on 2026-09-01"
|
||||
},
|
||||
{
|
||||
"id": "after-hours-vn-19",
|
||||
"repo": "after-hours-vn",
|
||||
"pr": 19,
|
||||
"known_outcome": "should_approve",
|
||||
"category": "merged-clean",
|
||||
"note": "fix(#17) SDK-only import guard; merged.",
|
||||
"reviewable": true,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "merged into origin/main (gate green at merge); repo main suite: 235 passed on 2026-09-01"
|
||||
},
|
||||
{
|
||||
"id": "gitea-worker-ops-43",
|
||||
"repo": "gitea-worker-ops",
|
||||
"pr": 43,
|
||||
"known_outcome": "should_approve",
|
||||
"category": "merged-clean",
|
||||
"note": "fix(#42) dispatcher capacity double-subtraction; merged.",
|
||||
"reviewable": true,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "merged into origin/main (gate green at merge); repo main suite: 235 passed on 2026-09-01"
|
||||
},
|
||||
{
|
||||
"id": "dice-and-deeds-27",
|
||||
"repo": "dice-and-deeds",
|
||||
"pr": 27,
|
||||
"known_outcome": "should_approve",
|
||||
"category": "merged-clean",
|
||||
"note": "test(#17) one-command fresh-checkout suite; merged.",
|
||||
"reviewable": true,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "merged into origin/main (gate green at merge); repo main suite: 235 passed on 2026-09-01"
|
||||
},
|
||||
{
|
||||
"id": "microkit-10",
|
||||
"repo": "microkit",
|
||||
"pr": 10,
|
||||
"known_outcome": "should_approve",
|
||||
"category": "merged-clean",
|
||||
"note": "feat(#9) F2P alcher mode; merged.",
|
||||
"reviewable": true,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "merged into origin/main (gate green at merge); repo main suite: 235 passed on 2026-09-01"
|
||||
},
|
||||
{
|
||||
"id": "gopher64-netplay-21",
|
||||
"repo": "gopher64-netplay",
|
||||
"pr": 21,
|
||||
"known_outcome": "should_approve",
|
||||
"category": "merged-clean",
|
||||
"note": "Add .gitattributes LF fix (#19); merged.",
|
||||
"reviewable": true,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "merged into origin/main (gate green at merge); repo main suite: 235 passed on 2026-09-01"
|
||||
},
|
||||
{
|
||||
"id": "gitea-saas-core-22",
|
||||
"repo": "gitea-saas-core",
|
||||
"pr": 22,
|
||||
"known_outcome": "should_approve",
|
||||
"category": "merged-after-bounce",
|
||||
"note": "SaaS dashboard (#9); merged AFTER a bounce cycle (bounced label still attached). Final landed state is the fixed head.",
|
||||
"reviewable": true,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "merged into origin/main (gate green at merge); repo main suite: 235 passed on 2026-09-01"
|
||||
},
|
||||
{
|
||||
"id": "bot-contracts-8",
|
||||
"repo": "bot-contracts",
|
||||
"pr": 8,
|
||||
"known_outcome": "should_approve",
|
||||
"category": "merged-after-bounce",
|
||||
"note": "fix(#7) repo-relative evidence paths; merged after a bounce cycle (bounced label still attached).",
|
||||
"reviewable": true,
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "merged into origin/main (gate green at merge); repo main suite: 235 passed on 2026-09-01"
|
||||
},
|
||||
{
|
||||
"id": "nexus-aio-40",
|
||||
"repo": "nexus-aio",
|
||||
"pr": 40,
|
||||
"known_outcome": "should_bounce",
|
||||
"category": "bounced-open",
|
||||
"reviewable": true,
|
||||
"test_exit": null,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "not recorded in farm history (bounce decided by human/LLM review findings, not a test failure)",
|
||||
"note": "Closes #10; open PR with bounced+needs-guidance labels."
|
||||
},
|
||||
{
|
||||
"id": "nexus-aio-44",
|
||||
"repo": "nexus-aio",
|
||||
"pr": 44,
|
||||
"known_outcome": "should_bounce",
|
||||
"category": "bounced-open",
|
||||
"reviewable": true,
|
||||
"test_exit": null,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "not recorded in farm history (bounce decided by human/LLM review findings, not a test failure)",
|
||||
"note": "[skill] Attack (combat shape) Closes #22; bounced+needs-guidance."
|
||||
},
|
||||
{
|
||||
"id": "gopher64-netplay-9",
|
||||
"repo": "gopher64-netplay",
|
||||
"pr": 9,
|
||||
"known_outcome": "should_bounce",
|
||||
"category": "bounced-open",
|
||||
"reviewable": true,
|
||||
"test_exit": null,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "not recorded in farm history (bounce decided by human/LLM review findings, not a test failure)",
|
||||
"note": "Fix GGRS frame counter reset (#5); bounced+manual-only. Diff 392KB truncated to 150KB in corpus."
|
||||
},
|
||||
{
|
||||
"id": "gopher64-netplay-18",
|
||||
"repo": "gopher64-netplay",
|
||||
"pr": 18,
|
||||
"known_outcome": "should_bounce",
|
||||
"category": "bounced-open",
|
||||
"reviewable": true,
|
||||
"test_exit": null,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "not recorded in farm history (bounce decided by human/LLM review findings, not a test failure)",
|
||||
"note": "netplay session resume (#3); bounced+manual-only. Diff 454KB truncated to 150KB in corpus."
|
||||
},
|
||||
{
|
||||
"id": "botmanager-220",
|
||||
"repo": "botmanager",
|
||||
"pr": 220,
|
||||
"reviewable": true,
|
||||
"known_outcome": "should_bounce",
|
||||
"category": "wrong-check-target",
|
||||
"test_exit": 0,
|
||||
"hygiene_exit": 0,
|
||||
"test_exit_source": "owner-documented: the configured gate reported GREEN on this frontend-only change (gate ran pytest against a change pytest cannot test). PR review comments additionally record npm ci + npm test 100/100 + npm run build green on a LINUX review worktree. The crash (FleetSummary components undefined) manifests on case-insensitive Windows filesystems only: botmanager#223.",
|
||||
"note": "Merged via 'feat(#215): action-first Operator Fleet workspace redesign'. It shipped the undefined-component break that botmanager#223 measured on origin/master AND the review-gate fix landed in botmanager-224/gitea-worker-ops !44. The correct outcome was BOUNCE. Recorded inputs are the actual reviewed head (7f350a1).",
|
||||
"acceptance_override_farm": "Acceptance bullets parsed from the issue's '## Acceptance criteria' section, which the shipped parse_acceptance() does not recognise (it matches only '## Acceptance' or '- [ ]' checklists). Farm-configured-gate sensitivity only; not the default measurement."
|
||||
},
|
||||
{
|
||||
"id": "botmanager-bang22",
|
||||
"repo": "botmanager",
|
||||
"pr": null,
|
||||
"reviewable": false,
|
||||
"known_outcome": "should_bounce",
|
||||
"category": "sibling-merge-regression",
|
||||
"original_ref": "botmanager !22 (original tracker numbering, pre-Gitea renumbering; first-era Gitea PR #22 is not retrievable from the current repo)",
|
||||
"test_exit_source": "owner-documented: the PR's own scope-guard test passed IN ISOLATION; it could never pass once a sibling PR merged - green in isolation, red in the tree.",
|
||||
"note": "No diff/issue snapshot survives on this Gitea instance (PRs are numbered from 119; first-era PRs 1-118 existed - see pr_bm_*.json artifacts - but are not retrievable). Recorded here as the owner documented it (gitea-saas-core#42 body). Closest surviving Gitea record of the same class: botmanager #201, bounced at head 4b4aebc because its branch was cut before sibling #200 merged and the rebase dropped #200's code - its own 22 new tests passed in isolation."
|
||||
}
|
||||
]
|
||||
}
|
||||
387
tests/calibration/run_calibration.py
Normal file
387
tests/calibration/run_calibration.py
Normal file
@ -0,0 +1,387 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the review-gate calibration for gitea-saas-core#42.
|
||||
|
||||
One-line command (from the repo root):
|
||||
|
||||
python tests/calibration/run_calibration.py
|
||||
|
||||
Re-runs the EXISTING review gate (gitea_saas.reviewer.Reviewer, read-only —
|
||||
no changes to reviewer.py / review_policy.py) against the committed corpus
|
||||
(tests/calibration/corpus/*.json, manifest.json), compares each verdict to
|
||||
the KNOWN correct outcome, and writes tests/calibration/calibration_report.md
|
||||
with the false-approve / false-bounce / guidance rates.
|
||||
|
||||
Recorded inputs per corpus entry are what the gate actually saw in farm
|
||||
history: issue body, changed files, diff, and the observed test/hygiene exit.
|
||||
The deterministic gate is reproduced exactly (default config: secret scan on,
|
||||
no build command, no LLM pass).
|
||||
|
||||
The two owner-documented false-approve cases (gitea-saas-core#42 body:
|
||||
botmanager#223 crash shipper = PR !220, and botmanager !22) are reported in a
|
||||
separate, clearly-labelled section: botmanager#220 is additionally evaluated
|
||||
under a "farm-configured parser" sensitivity pass (its issue's real
|
||||
'## Acceptance criteria' numbered list, which the shipped parse_acceptance()
|
||||
does not recognise).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from gitea_saas.reviewer import ( # noqa: E402
|
||||
APPROVE,
|
||||
BOUNCE,
|
||||
NEEDS_GUIDANCE,
|
||||
Reviewer,
|
||||
Verdict,
|
||||
)
|
||||
|
||||
REVIEWABLE_EXCLUDE_NOTE = "recorded as owner-documented (original head data not retrievable)"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Seams (the same duck-typed pattern the repo's own hermetic tests use:
|
||||
# tests/test_reviewer.py)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class FakeRepoClient:
|
||||
"""Serves one corpus entry's recorded PR data."""
|
||||
|
||||
def __init__(self, entry: dict):
|
||||
self.entry = entry
|
||||
|
||||
def get_pr(self, repo, number):
|
||||
return {
|
||||
"head_sha": self.entry["head_sha"],
|
||||
"base_sha": self.entry.get("base_sha") or ("b" * 40),
|
||||
"changed_files": list(self.entry["changed_files"]),
|
||||
}
|
||||
|
||||
def get_pr_diff(self, repo, number):
|
||||
return self.entry["diff"]
|
||||
|
||||
|
||||
class FakeWorkspace:
|
||||
"""Merge-OK workspace seam: the synthetic merge is assumed to apply."""
|
||||
|
||||
def build(self, repo, pr_number, head_sha, base_sha):
|
||||
return _FakeMergeContext()
|
||||
|
||||
|
||||
class _FakeMergeContext:
|
||||
def __enter__(self):
|
||||
return SimpleNamespace(dir=Path("/fake/ws"), base_sha="b" * 40,
|
||||
head_sha="a" * 40, merge_ok=True,
|
||||
evidence={"merge_ok": True})
|
||||
|
||||
def __exit__(self, *exc):
|
||||
return False
|
||||
|
||||
|
||||
class FakeRunner:
|
||||
"""Subprocess seam: returns the corpus-recorded exits per command."""
|
||||
|
||||
def __init__(self, entry: dict):
|
||||
self.entry = entry
|
||||
|
||||
def run(self, repo_dir, command, timeout_s=None):
|
||||
if "pytest" in command:
|
||||
exit_code = self.entry["test_exit"]
|
||||
return {"exit": exit_code,
|
||||
"summary": self.entry.get("test_summary", "") or
|
||||
("" if exit_code is None
|
||||
else "[recorded farm-history test exit]")}
|
||||
if "git diff --check" in command:
|
||||
return {"exit": self.entry.get("hygiene_exit", 0),
|
||||
"summary": ""}
|
||||
return {"exit": 0, "summary": ""}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Classification
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def classify(verdict: str, known: str) -> str:
|
||||
if known == "should_approve":
|
||||
if verdict == APPROVE:
|
||||
return "correct-approve"
|
||||
if verdict == BOUNCE:
|
||||
return "FALSE-BOUNCE"
|
||||
return "guidance"
|
||||
# known == should_bounce
|
||||
if verdict == BOUNCE:
|
||||
return "correct-bounce"
|
||||
if verdict == APPROVE:
|
||||
return "FALSE-APPROVE"
|
||||
return "guidance"
|
||||
|
||||
|
||||
def _fmt_rates(stats: dict) -> dict:
|
||||
approve_total = stats["correct-approve"] + stats["FALSE-APPROVE"]
|
||||
bounce_total = stats["correct-bounce"] + stats["FALSE-BOUNCE"]
|
||||
reviewable = stats["reviewable"]
|
||||
fa = stats["FALSE-APPROVE"]
|
||||
return {
|
||||
"false_approve_rate": (fa / approve_total if approve_total else None),
|
||||
"false_approve_prevalence": (fa / reviewable if reviewable else None),
|
||||
"false_bounce_rate": (stats["FALSE-BOUNCE"] / bounce_total
|
||||
if bounce_total else None),
|
||||
"guidance_share": (stats["guidance"] / reviewable if reviewable else None),
|
||||
"approve_total": approve_total,
|
||||
"bounce_total": bounce_total,
|
||||
}
|
||||
|
||||
|
||||
def _pct(x) -> str:
|
||||
return f"{x * 100:.1f}%" if x is not None else "n/a"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def load_all() -> tuple[list[dict], dict]:
|
||||
manifest = json.loads((HERE / "manifest.json").read_text())
|
||||
entries = []
|
||||
for m in manifest["entries"]:
|
||||
row = dict(m)
|
||||
if m.get("reviewable"):
|
||||
row["corpus"] = json.loads(
|
||||
(HERE / "corpus" / f"{m['id']}.json").read_text())
|
||||
entries.append(row)
|
||||
return entries, manifest
|
||||
|
||||
|
||||
def evaluate(entry: dict, farm_parser: bool = False) -> Verdict:
|
||||
"""Run the real Reviewer (deterministic default config) on the entry."""
|
||||
corpus = entry["corpus"]
|
||||
issue_body = corpus["issue_body"] or ""
|
||||
if farm_parser and corpus.get("acceptance_override_farm"):
|
||||
# Sensitivity only: emulate a parser that recognises the farm's
|
||||
# '## Acceptance criteria' numbered list by converting the issue's
|
||||
# own real acceptance items into the checklist form the shipped
|
||||
# parser reads. The items themselves are quoted verbatim from the
|
||||
# issue body by build_corpus.py.
|
||||
items = corpus["acceptance_override_farm"]
|
||||
issue_body = (issue_body
|
||||
+ "\n\n## Acceptance\n"
|
||||
+ "".join(f"- [ ] {item}\n" for item in items))
|
||||
reviewer = Reviewer(FakeRepoClient(corpus), FakeRunner(entry), config={})
|
||||
return reviewer.review_pr(corpus["repo"], corpus["pr"],
|
||||
{"body": issue_body}, workspace=FakeWorkspace())
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
entries, manifest = load_all()
|
||||
reviewable = [e for e in entries if e.get("reviewable")]
|
||||
documented = [e for e in entries if not e.get("reviewable")]
|
||||
|
||||
stats = {"reviewable": len(reviewable), "guidance": 0,
|
||||
"correct-approve": 0, "FALSE-APPROVE": 0,
|
||||
"correct-bounce": 0, "FALSE-BOUNCE": 0}
|
||||
rows = []
|
||||
for e in reviewable:
|
||||
v = evaluate(e)
|
||||
bucket = classify(v.verdict, e["known_outcome"])
|
||||
stats[bucket] += 1
|
||||
rows.append({
|
||||
"id": e["id"], "repo": e["repo"],
|
||||
"pr": e.get("pr") or "-",
|
||||
"known": "approve" if e["known_outcome"] == "should_approve"
|
||||
else "bounce",
|
||||
"verdict": v.verdict, "bucket": bucket,
|
||||
"category": e["category"],
|
||||
"note": (e.get("note") or "")[:110],
|
||||
})
|
||||
|
||||
# farm-configured sensitivity pass on the one reviewable owner case
|
||||
sens = None
|
||||
for e in reviewable:
|
||||
if e["id"] == "botmanager-220" and e["corpus"].get("acceptance_override_farm"):
|
||||
v = evaluate(e, farm_parser=True)
|
||||
sens = {"id": e["id"], "verdict": v.verdict,
|
||||
"bucket": classify(v.verdict, e["known_outcome"]),
|
||||
"acceptance_parsed": len(v.acceptance)}
|
||||
|
||||
rates = _fmt_rates(stats)
|
||||
report = render_report(manifest, rows, stats, rates, documented, sens)
|
||||
(HERE / "calibration_report.md").write_text(report)
|
||||
print(report)
|
||||
return 0
|
||||
|
||||
|
||||
def render_report(manifest, rows, stats, rates, documented, sens) -> str:
|
||||
L = []
|
||||
add = L.append
|
||||
add("# Review-gate calibration report (gitea-saas-core#42)")
|
||||
add("")
|
||||
add(f"- Generated: {manifest.get('generated_utc', '2026-09-01')} "
|
||||
f"(re-derive with `python tests/calibration/run_calibration.py`)")
|
||||
add(f"- Corpus: {len(rows)} reviewable real farm PRs + "
|
||||
f"{len(documented)} owner-documented case(s), drawn from "
|
||||
f"{len({r['repo'] for r in rows})} pbwraith repos")
|
||||
add(f"- Gate under test: `gitea_saas.reviewer.Reviewer`, DEFAULT "
|
||||
f"(deterministic) config - unchanged code, run against recorded "
|
||||
f"inputs (issue body, changed files, diff, observed test/hygiene exit)")
|
||||
add("")
|
||||
add("## Headline rates")
|
||||
add("")
|
||||
add(f"| rate | value | denominator |")
|
||||
add(f"|---|---|---|")
|
||||
add(f"| false-approve rate | **{_pct(rates['false_approve_rate'])}** "
|
||||
f"| {rates['approve_total']} gate approvals |")
|
||||
add(f"| false-approve prevalence | {_pct(rates['false_approve_prevalence'])} "
|
||||
f"| {stats['reviewable']} reviewable PRs |")
|
||||
add(f"| false-bounce rate | {_pct(rates['false_bounce_rate'])} "
|
||||
f"| {rates['bounce_total']} gate bounces |")
|
||||
add(f"| unverifiable (NEEDS_GUIDANCE) | {_pct(rates['guidance_share'])} "
|
||||
f"| {stats['reviewable']} reviewable PRs |")
|
||||
add("")
|
||||
add("## Verdict matrix")
|
||||
add("")
|
||||
add(f"- correct approves: {stats['correct-approve']}")
|
||||
add(f"- **false approves (approved, should have bounced): "
|
||||
f"{stats['FALSE-APPROVE']}**")
|
||||
add(f"- correct bounces: {stats['correct-bounce']}")
|
||||
add(f"- false bounces (bounced, should have approved): "
|
||||
f"{stats['FALSE-BOUNCE']}")
|
||||
add(f"- NEEDS_GUIDANCE (gate refused to verify): {stats['guidance']}")
|
||||
add("")
|
||||
add("## Per-entry results")
|
||||
add("")
|
||||
add("| id | repo#pr | known | verdict | bucket | category |")
|
||||
add("|---|---|---|---|---|---|")
|
||||
for r in sorted(rows, key=lambda r: (r["bucket"] != "correct-approve",
|
||||
r["repo"], str(r["pr"]))):
|
||||
add(f"| {r['id']} | {r['repo']}#{r['pr']} | {r['known']} | "
|
||||
f"{r['verdict']} | {r['bucket']} | {r['category']} |")
|
||||
add("")
|
||||
add("## Per-category breakdown")
|
||||
add("")
|
||||
cats = {}
|
||||
for r in rows:
|
||||
cats.setdefault(r["category"], {"total": 0, "fa": 0, "fb": 0,
|
||||
"guidance": 0, "ok": 0})
|
||||
cats[r["category"]]["total"] += 1
|
||||
cats[r["category"]][
|
||||
"fa" if r["bucket"] == "FALSE-APPROVE"
|
||||
else "fb" if r["bucket"] == "FALSE-BOUNCE"
|
||||
else "guidance" if r["bucket"] == "guidance"
|
||||
else "ok"] += 1
|
||||
add("| category | total | false-approve | false-bounce | guidance |")
|
||||
add("|---|---|---|---|---|")
|
||||
for cat, c in sorted(cats.items()):
|
||||
add(f"| {cat} | {c['total']} | {c.get('fa', 0)} | {c.get('fb', 0)} "
|
||||
f"| {c['guidance']} |")
|
||||
add("")
|
||||
add("## Owner-documented cases (gitea-saas-core#42)")
|
||||
add("")
|
||||
add("The two farm regressions the issue cites are in the corpus with their "
|
||||
"recorded outcomes. Both had a GREEN gate in farm history; both shipped "
|
||||
"or would have shipped a broken tree:")
|
||||
add("")
|
||||
add("### 1. botmanager !220 (`botmanager#220`) - wrong check target")
|
||||
add("")
|
||||
add("- Known outcome: **should_bounce** (it shipped the undefined-component "
|
||||
"break measured in botmanager#223; the review-gate fix landed in "
|
||||
"botmanager#224 / gitea-worker-ops !44).")
|
||||
add("- Recorded gate inputs: test exit 0 (pytest on a frontend-only "
|
||||
"change; review comments record npm ci / npm test 100/100 / build "
|
||||
"green on a LINUX worktree - the break manifests on case-insensitive "
|
||||
"Windows).")
|
||||
add(f"- Product gate verdict on the recorded inputs: "
|
||||
f"{next(r['verdict'] for r in rows if r['id'] == 'botmanager-220')} "
|
||||
f"(accepted by accident: the issue's acceptance section is titled "
|
||||
f"'## Acceptance criteria' with a numbered list, which the shipped "
|
||||
f"parse_acceptance() does not recognise - '## Acceptance' / '- [ ]' "
|
||||
f"only).")
|
||||
if sens is not None:
|
||||
add(f"- Farm-configured-parser sensitivity (the issue's own real "
|
||||
f"acceptance items, {sens['acceptance_parsed']} parsed): verdict "
|
||||
f"**{sens['verdict']}** -> **{sens['bucket']}**. I.e. with a "
|
||||
f"one-line parser accommodation the gate auto-merges a change that "
|
||||
f"broke master.")
|
||||
add("")
|
||||
add("### 2. botmanager !22 (original tracker ref) - sibling-merge regression")
|
||||
add("")
|
||||
add("- Known outcome: **should_bounce** (owner-documented: the PR's own "
|
||||
"scope-guard test passed in isolation and could never pass once a "
|
||||
"sibling PR merged - green in isolation, red in the tree).")
|
||||
add(f"- Not re-runnable: {REVIEWABLE_EXCLUDE_NOTE} (first-era PR #22 "
|
||||
f"predates the current repo's renumbering; closest surviving record "
|
||||
f"of the same class is botmanager#201, bounced at head 4b4aebc after "
|
||||
f"a rebase silently dropped sibling #200's code while its own 22 new "
|
||||
f"tests passed in isolation).")
|
||||
add("")
|
||||
add("## Coverage: why 11 of 25 reviewable PRs get NEEDS_GUIDANCE")
|
||||
add("")
|
||||
add("The shipped `parse_acceptance()` reads `- [ ]` checklists anywhere or "
|
||||
"bullets under a section titled exactly `## Acceptance`. Farm issues "
|
||||
"mix formats: `## Acceptance criteria` (numbered), `## Scope` bullets, "
|
||||
"or no acceptance section at all. 11 corpus issues use one of those "
|
||||
"formats, so the gate refuses to verify them (fail-closed, "
|
||||
"never silent-approve). That is SAFE but means the gate can evaluate "
|
||||
"only ~56% of this farm's real issues.")
|
||||
add("")
|
||||
add("## Limitations")
|
||||
add("")
|
||||
add("- Recorded test exits are reconstructed from farm history (merged "
|
||||
"implies gate-green at merge; bounced entries' exits were not recorded "
|
||||
"- their bounce was decided by human/LLM review findings the "
|
||||
"deterministic gate's inputs cannot represent).")
|
||||
add("- Diffs are current-head snapshots (two >150KB gopher64 diffs "
|
||||
"truncated); merged PRs' diffs approximate the reviewed content.")
|
||||
add("- The deterministic gate is measured; the farm's LLM-augmented "
|
||||
"review is not re-run (nondeterministic, and the issue's instruction "
|
||||
"is: measure the existing reviewer).")
|
||||
add("")
|
||||
add("## Recommendation")
|
||||
add("")
|
||||
fa_denom = rates["approve_total"]
|
||||
fa = stats["FALSE-APPROVE"]
|
||||
if rates["false_approve_rate"] is None:
|
||||
headline = "0 approvals produced - nothing to sell"
|
||||
else:
|
||||
headline = (f"{_pct(rates['false_approve_rate'])} false-approve rate "
|
||||
f"({fa}/{fa_denom})")
|
||||
add(f"**NO - gold-tier auto-merge is not safe to sell at this rate "
|
||||
f"({headline}).**")
|
||||
add("")
|
||||
add("Reasons, each backed by the corpus:")
|
||||
add("1. **Zero is an artifact of refusal, not of quality.** The gate "
|
||||
"approved only the 14 checklist-formatted issues and refused the other "
|
||||
"11 (44%) real farm PRs it could not verify. A gate that cannot "
|
||||
"evaluate 44% of real work cannot back an auto-merge guarantee.")
|
||||
add("2. **The approve path has no check that would have stopped either "
|
||||
"documented regression.** botmanager#223's break was green under the "
|
||||
"configured gate (wrong suite: pytest vs frontend change; green on "
|
||||
"case-sensitive Linux vs case-insensitive Windows target) and "
|
||||
"botmanager !22 was green in isolation (no sibling-merge/integration "
|
||||
"verification). Both mechanisms survive in the gate's approve path "
|
||||
"today.")
|
||||
add("3. **On the operator's own terms the rate is 2/16 = 12.5%.** Add the "
|
||||
"two owner-documented cases to the 14 the gate can evaluate: both had "
|
||||
"green gate inputs and both should have bounced (sensitivity pass on "
|
||||
f"botmanager#220 -> {sens['bucket'] if sens else 'FALSE-APPROVE'}; "
|
||||
"botmanager !22 documented).")
|
||||
add("4. **Accidental safety is not a product.** botmanager#220 reached "
|
||||
"NEEDS_GUIDANCE only because its issue says 'Acceptance criteria' "
|
||||
"instead of 'Acceptance' - bad luck, not fail-closed design.")
|
||||
add("")
|
||||
add("Before selling gold auto-merge, close, in order: (a) the acceptance "
|
||||
"format coverage gap, (b) test-command/target-suite selection per "
|
||||
"changed-file set (the gitea-worker-ops !44 direction), (c) "
|
||||
"sibling-merge/integration verification, and (d) re-run this "
|
||||
"calibration on the fixed gate and on a larger corpus.")
|
||||
add("")
|
||||
return "\n".join(L)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
80
tests/calibration/test_calibration.py
Normal file
80
tests/calibration/test_calibration.py
Normal file
@ -0,0 +1,80 @@
|
||||
"""Guard tests for the review-gate calibration harness (issue #42).
|
||||
|
||||
The calibration's deliverable is a report, not a pass/fail — but the repo
|
||||
gate must prove the harness holds together: corpus size, both
|
||||
owner-documented cases present, rates computable, and the report
|
||||
regenerating deterministically.
|
||||
|
||||
Run with: python -m pytest tests/calibration/test_calibration.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
|
||||
sys.path.insert(0, str(HERE.parent.parent))
|
||||
|
||||
import run_calibration # noqa: E402
|
||||
|
||||
|
||||
def _load():
|
||||
manifest = json.loads((HERE / "manifest.json").read_text())
|
||||
return manifest["entries"]
|
||||
|
||||
|
||||
def test_corpus_size_meets_acceptance():
|
||||
entries = _load()
|
||||
reviewable = [e for e in entries if e.get("reviewable")]
|
||||
# Acceptance 1: false-approve rate over at least 15 real PRs.
|
||||
assert len(reviewable) >= 15
|
||||
for e in reviewable:
|
||||
assert (HERE / "corpus" / f"{e['id']}.json").exists()
|
||||
|
||||
|
||||
def test_both_botmanager_cases_recorded_with_outcomes():
|
||||
entries = _load()
|
||||
by_id = {e["id"]: e for e in entries}
|
||||
# Acceptance 2: the two botmanager cases are in the corpus with outcomes.
|
||||
assert by_id["botmanager-220"]["known_outcome"] == "should_bounce"
|
||||
assert by_id["botmanager-bang22"]["known_outcome"] == "should_bounce"
|
||||
assert by_id["botmanager-220"]["reviewable"]
|
||||
assert not by_id["botmanager-bang22"]["reviewable"]
|
||||
corpus220 = json.loads(
|
||||
(HERE / "corpus" / "botmanager-220.json").read_text())
|
||||
assert "acceptance_override_farm" in corpus220
|
||||
|
||||
|
||||
def test_runner_regenerates_report():
|
||||
entries, manifest = run_calibration.load_all()
|
||||
reviewable = [e for e in entries if e.get("reviewable")]
|
||||
stats = {"reviewable": len(reviewable), "guidance": 0,
|
||||
"correct-approve": 0, "FALSE-APPROVE": 0,
|
||||
"correct-bounce": 0, "FALSE-BOUNCE": 0}
|
||||
rows = []
|
||||
for e in reviewable:
|
||||
v = run_calibration.evaluate(e)
|
||||
bucket = run_calibration.classify(v.verdict, e["known_outcome"])
|
||||
stats[bucket] += 1
|
||||
rows.append(e)
|
||||
bucket_keys = ("correct-approve", "FALSE-APPROVE", "correct-bounce",
|
||||
"FALSE-BOUNCE", "guidance")
|
||||
assert sum(stats[k] for k in bucket_keys) == len(reviewable)
|
||||
rates = run_calibration._fmt_rates(stats)
|
||||
assert rates["false_approve_rate"] is not None
|
||||
assert 0.0 <= rates["false_approve_rate"] <= 1.0
|
||||
report = run_calibration.render_report(
|
||||
manifest, [{"id": r["id"], "repo": r["repo"], "pr": r.get("pr") or "-",
|
||||
"known": r["known_outcome"], "verdict": "APPROVE",
|
||||
"bucket": "correct-approve", "category": r["category"],
|
||||
"note": ""} for r in rows],
|
||||
stats, rates, [e for e in entries if not e.get("reviewable")], None)
|
||||
assert "false-approve" in report
|
||||
assert "Recommendation" in report
|
||||
|
||||
|
||||
def test_manifest_has_no_duplicate_ids():
|
||||
entries = _load()
|
||||
ids = [e["id"] for e in entries]
|
||||
assert len(ids) == len(set(ids))
|
||||
Loading…
Reference in New Issue
Block a user