feat(merge-retrospective): compute Gate-Preventable Repair Rate (GPRR) from retrospective issues - #731
Merged
Merged
Conversation
…) from retrospective issues Adds .github/scripts/compute_gprr.py: parses each label:retrospective issue's Status: tag(s), buckets by ISO week, and reports the missing-deterministic-gate share of classified repairs and of merged PRs per week, as a stateless recomputation (no persisted/committed snapshot -- every run reproduces the full trend from source). Refactors scan_retrospective_gate_drift.py to expose list_labelled_issue_records (full issue records) and a renamed public fetch_json_page, so compute_gprr.py reuses the existing paginated GitHub API client instead of a second hand-rolled one, per the issue's own constraint. list_labelled_issues becomes a thin wrapper; its existing behavior and tests are unchanged. Wires the new script into retrospective-gate-drift.yml as an additional step (same cadence, piped to $GITHUB_STEP_SUMMARY, no continue-on-error), adding pull-requests: read for the merged-PR query. Design: docs/superpowers/specs/2026-08-04-gprr-design.md Refs #726.
Verified live against this repository (2026-08-04): a busy week's retrospective issues can classify more gate-preventable repairs than PRs merged that same week, since merge-retrospective files one issue per merged PR but a single issue can enumerate several repairs, and a week's classified repairs can trace back to a PR merged earlier. Recorded so a future reader does not mistake a >100% share for a bug. Refs #726.
Extract the generic paginated-fetch-with-retry GitHub client (fetch_json_page, GitHubApiError, default_opener) into a new shared .github/scripts/_github_http.py, so compute_gprr.py no longer imports scan_retrospective_gate_drift.py directly for it -- the first version broke this repo's .github/scripts/*.py independence convention. compute_gprr.py still imports scan_retrospective_gate_drift for list_labelled_issue_records specifically, the issue-specific reuse issue #726 itself asked for. Also fixes, found by the same review: - retrospective-gate-drift.yml: the new GPRR step was silently skipped whenever the preceding citation-drift step failed on threshold; added if: ${{ !cancelled() }} so the trend still reports on exactly the days it matters most. - _github_http.fetch_json_page: an HTTP 200 response with an unparseable body raised an uncaught json.JSONDecodeError instead of the documented GitHubApiError/exit-1 path; now caught and re-raised, covered by tests/test_github_http.py. - compute_gprr.py: replaced positional _REPAIR_SLUGS[0]/[1]/[2] indexing (silently wrong if the tuple's order ever changed) with named slug constants. Full suite re-verified after: 2487 passed, 47 in-scope files still meet the 90% coverage floor, live run against this repository reproduces identical output to before the refactor. Refs #726.
This was referenced Aug 4, 2026
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
.github/scripts/compute_gprr.py, which parses everylabel:retrospectiveissue'sStatus:tag(s), buckets by ISO week, and reports the missing-deterministic-gate share of classified repairs and of merged PRs per week -- turning the narrative Status classificationskills/merge-retrospective/SKILL.mdalready mandates into an inspectable numeric time series, per issue #726.Facts
scan_retrospective_gate_drift.pyis refactored to exposelist_labelled_issue_records(full issue records) and a renamed publicfetch_json_page;list_labelled_issuesbecomes a thin wrapper over it.compute_gprr.pyreuses both instead of a second hand-rolled GitHub client, per the issue's own constraint. All 51 pre-existing/added tests for that file pass; measured coverage 97.6% (floor: 90%).compute_gprr.pyis new, stdlib-only, 130 statements, 32 tests, 99.2% measured coverage.uv run pytest-- 2487 passed.uv run ruff check .-- clean.uv run mypy(thetests + pythonpath-linked rootsgroup, which covers.github/scripts) -- clean, 113 source files.gate_evals_scripts_coverage.py-- all 47 in-scope files pass the 90% floor, including the three changed/added scripts.GITHUB_TOKENalready present in this session) rather than only against fixtures:python3 .github/scripts/compute_gprr.py --owner tvna --repo gitapexreturned, as of 2026-08-04:gate_share_of_merged_prslegitimately exceeds 100% in some weeks (2026-W31, 2026-W32 above) -- a single retrospective issue can enumerate several repairs, and merge-retrospective files one issue per merged PR, not one repair per PR.compute_gprr.pyimportscan_retrospective_gate_drift.pydirectly for the low-level HTTP client, breaking this repo's.github/scripts/*.pyindependence convention. Fixed by extracting the generic paginated-fetch-with-retry client (fetch_json_page,GitHubApiError,default_opener) into a new shared.github/scripts/_github_http.py; both scripts depend on it, neither on the other, for that layer (compute_gprr.pystill importsscan_retrospective_gate_driftforlist_labelled_issue_recordsspecifically, which is issue-specific business logic the issue's own constraint named as the thing to reuse). Same pass also found and fixed: the new GPRR workflow step was silently skipped whenever the citation-drift step above it failed on threshold (addedif: ${{ !cancelled() }}, since GPRR is meant to report regardless of that check's own outcome); an HTTP-200-with-unparseable-body response would have raised an uncaughtjson.JSONDecodeErrorinstead of the documentedGitHubApiError/exit-1 path (fixed in_github_http.fetch_json_page, now covered bytests/test_github_http.py); and positional_REPAIR_SLUGS[0]/[1]/[2]indexing that could silently mis-map if the tuple's order ever changed (replaced with named slug constants). Re-ran the full suite after: 2487 passed, 47 in-scope files still meet the 90% coverage floor (new_github_http.py: 97.8%), and the live run against this repository reproduced the identical numbers reported above, confirming the refactor is behavior-preserving.Assumptions
label:retrospectiveissues' owncreated_atevery run, so a run a week from now naturally shows one more week-bucket -- the growth the issue's proof method asks for -- with no new write scope, no commit-back step, and nocontents: write. Speculative: if step-summary-only visibility (bounded by Actions' own log retention) proves insufficient in practice, a persisted series is a follow-on, not built here.Risk / blast radius
pull-requests: readtoretrospective-gate-drift.yml's existingcontents: read/issues: readpermissions (needed for the merged-PR query); no new secret, no write scope, no commit-back step.continue-on-error, matching its sibling step, but nothing in it can trip CI red except a genuine GitHub API error -- the metric itself never fails the run.list_labelled_issuestests pass unchanged, and a malformed-item input was constructed by hand to confirm the refactor still fails loudly with the sameKeyError, not a new silent fail-open).Rollback
Revert this PR's merge commit (or drop the new "Compute Gate-Preventable Repair Rate (GPRR)" step from
retrospective-gate-drift.ymland thepull-requests: readpermission lines) to fully disable;scan_retrospective_gate_drift.py's own citation-drift check is untouched in behavior and keeps running either way.Verification
Acceptance Criteria Map from issue #726, restated:
tests/test_compute_gprr.py'sbuild_weekly_series/parse_status_tagstests; reusesscan_retrospective_gate_drift.py'sfetch_json_page/list_labelled_issue_records, no second GitHub client addedChecklist
docs/superpowers/specs/2026-08-04-gprr-design.mdadded.github/scripts/*.pyand adocs/superpowers/specs/*.mddesign doc)evals/*/split.mdKept-edit-log entry addedskills/*/SKILL.mdStop-boundary/dispatch-branch changeSkill audit evidence
compute_gprr.py,_github_http.py, and thescan_retrospective_gate_drift.pyrefactor: constructed a malformed GitHub API response (an issue item missing"number") and confirmedlist_labelled_issuesstill raisesKeyErroruncaught, identical to pre-refactor behavior (no new fail-open path); confirmed a zero denominator reports"n/a"rather than a misleading"0.0%"; confirmed theStatus:line regex is anchored to the start/end of its own line so a hostile string merely mentioned in a repair's free-prose clause cannot forge a fake field, perSKILL.md's own stated injection concern (tests/test_compute_gprr.py::test_parse_status_tags_ignores_mid_sentence_mention); a separate, independent/code-reviewpass (8 finder agents) additionally found and this PR fixed: the cross-import independence-convention break, the workflow step being skipped after a threshold failure, an uncaughtjson.JSONDecodeErroron an HTTP-200-with-bad-body response (now a testedGitHubApiError,tests/test_github_http.py), and positional slug-tuple indexing -- see Facts for the full list; ran the full test/lint/mypy/coverage suite after each fix (see Facts).skills/evaluating-deterministic-gate-quality/references/dimensions.md; dimension 15 ("fail-closed default on incomplete or malformed input") specifically re-verified by hand against the refactoredlist_labelled_issue_records/list_labelled_issues, per the dimension's own instruction not to credit it from happy-path fixtures alone.docs/superpowers/specs/2026-08-04-gprr-design.md's Decision 2 (stateless recompute instead of a persisted/committed time series) was checked directly against the issue's own "storage decision -- unknown" residual risk, Decision 4's merged-PR ratio was checked against the live run above (which is what surfaced and then documented the >100% case), and Decision 1 was itself revised after review to the shared_github_http.pymodule shape described in Facts.Related Issue
Closes #726