gitea-saas-core#42: review-gate calibration — measure the false-approve rate before selling auto-merge #47

Merged
pbwraith merged 1 commits from issue-42 into main 2026-09-03 11:05:36 +02:00
31 changed files with 1885 additions and 0 deletions

79
CALIBRATION.md Normal file
View 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).

View 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())

View 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.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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."
}
]
}

View 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())

View 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))