<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
### 1. Summary
- Adds a new, purely git-history-derived outcome-quality metric: post-merge defect attribution. A later fix(...)-prefixed commit that touches a line a merged PR introduced, within a 14-day window, is an "escape." No human review, no second model, no runs/ receipts, no GitHub API.
- Ships as scripts/defect_attribution.py (the engine + CLI), src/defect-attribution.ts (the weekly-report loader/renderer), and a new ## Post-Merge Defect Attribution (AI-303) markdown section wired into eval-weekly's report additively — the existing BLIND escape metric is untouched and keeps rendering right above it.
- Backfilled across all available history (12 UTC weeks, 2026-05-17 → 2026-08-02) into reports/defect-attribution-<date>.json, plus a new trend chart (reports/defect-attribution-trend.png / -audit.json, mirroring the AI-243 classification-trend shape in scripts/eval_weekly_charts.py).
- Records the decision and all four rejected alternatives in a new docs/decisions/ entry.
### 2. Why it's needed
The escape metric's denominator (prs_with_human_after_drone) has read 0 for two weeks running at 96% drone share — the operator is now a merge gate, not a code reviewer, so the metric's raw material has stopped being produced. Escape rate is the loop's only outcome-quality signal; everything else measured (thrash, verification density, productive turns) describes *how* the work was done, not whether it was *right*. Post-merge defect attribution needs no human and no second model, so it can't be killed by the same success that killed the old metric, and it *improves* with volume instead of degrading — the opposite of every other candidate considered.
### 3. Changes
- scripts/defect_attribution.py (new) — the whole engine: commit classification (squash (#N) / old-style Merge pull request #N), diff-hunk parsing, git blame -C-based attribution, 14-day window, Sunday-anchored weekly bucketing, --week-start/--week-end/--date-tag (per-week, matches the existing issue_learning_loop.py/finding_classification.py calling convention) and --backfill (full-history) CLI modes. Full design rationale — including two traps discovered while validating the first backfill run, beyond what the ticket spelled out — is in the module docstring.
- scripts/test_defect_attribution.py (new) — 26 tests: pure-function coverage (classification regexes, diff-hunk parsing, week windowing, weekly bucketing) plus git-fixture integration tests (real git init temp repos) pinning: pre-merge commits never count (trap 1), a refactor never counts as a fix nor as a valid escape origin (trap 3), a fix beyond the window horizon is excluded (trap 6's window), and the newest week is provisional while an older, window-closed week is not (trap 6's contract).
- src/defect-attribution.ts (new) — thin, self-contained loader (loadDefectAttributionTrend, mirroring loadClassificationTrend's shape) and renderer (renderDefectAttributionSection). Deliberately does not import from src/eval-weekly.ts (duplicates the small subprocess-runner helper) to keep the dependency one-directional and avoid growing that already-large file's surface.
- src/eval-weekly.ts — minimal, narrowly-scoped wiring: one new import, one new optional loadDefectAttribution test-injection option, one new defect_attribution artifact field, one loader call, one render call. No existing logic touched.
- scripts/eval_weekly_charts.py — load_defect_attribution_weekly_snapshots / draw_defect_attribution_trend / write_defect_attribution_trend_audit, wired into main() the same additive, non-fatal way AI-243's classification trend is (skips cleanly on an empty backfill, never crashes the weekly dashboard run).
- reports/defect-attribution-<date>.json (12 files, new) — the backfilled weekly artifacts. reports/defect-attribution-trend.png / -audit.json (new) — the trend dashboard.
- docs/decisions/20260806T060059.814Z-ai-303-...md (new) — the decision + all four rejected alternatives + the two implementation traps.
- ARCHITECTURE.md — one line documenting the new module (required by arch-drift.test.ts).
Contract surface affected: none — WeeklyEvalArtifact gains one new, additive field (defect_attribution); nothing existing changed shape.
### 4. Breaking changes
None. The existing escape metric (escape_metric_status, prs_with_human_after_drone, the BLIND rendering in src/eval-weekly.ts) is byte-unchanged — pinned by the full existing eval-weekly.test.ts suite still passing unmodified (63/63), including every literal "BLIND" / "blind" assertion.
### 5. Test plan
- [x] pnpm typecheck → clean, 0 errors.
- [x] pnpm test → 3631/3631 vitest tests passed (116 files), 514/514 Python tests passed. (One transient timeout in dispatcher.test.ts/spec-author.test.ts on a prior parallel run reproduced as a pass in isolation — pre-existing resource-contention flakiness, unrelated to this change; confirmed by an immediate clean re-run of the full suite.)
- [x] python3 -m unittest scripts.test_defect_attribution -v → 26/26 passed, including the git-fixture integration tests for traps 1, 3, and 6.
- [x] Pre-merge exclusion pinned: TestPreMergeExclusion builds a real repo with implementer + pre-merge addresser-round commits on a feature branch, squash-merges, and asserts zero escapes before any post-merge fix exists, then confirms a genuine post-merge fix IS attributed.
- [x] Refactor-is-not-a-fix pinned: TestRefactorIsNotAFix asserts a refactor(...) commit touching a PR's introduced lines never creates an attribution, and — going further than the acceptance criterion literally asked — that a refactor is also never treated as a valid escape *origin* for a later real fix (undercounts rather than mis-attributes to a cosmetic rename).
- [x] Provisional/lagged newest week pinned: TestNewestWeekProvisional asserts a week whose fix window has fully elapsed reads provisional=False/measured, while a week 2 days old reads provisional=True with pending_prs=1.
- [x] Ran the real backfill (python scripts/defect_attribution.py --backfill) and the real chart regeneration (python scripts/eval_weekly_charts.py) against this repo's actual history — see the distribution below and the attached chart.
### 6. Verification artifact
Backfilled distribution (12 UTC weeks, window_days=14, min_sample_for_trend=5):
| Week (Sunday) | cohort | attributable | escaped | clean | pending | escape rate | status |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |
| 2026-05-17 | 3 | 3 | 0 | 3 | 0 | — | insufficient |
| 2026-05-24 | 5 | 5 | 0 | 5 | 0 | 0% | measured |
| 2026-05-31 | 2 | 2 | 0 | 2 | 0 | — | insufficient |
| 2026-06-07 | 28 | 28 | 7 | 21 | 0 | 25% | measured |
| 2026-06-14 | 15 | 15 | 3 | 12 | 0 | 20% | measured |
| 2026-06-21 | 1 | 1 | 0 | 1 | 0 | — | insufficient |
| 2026-06-28 | 2 | 2 | 1 | 1 | 0 | — | insufficient |
| 2026-07-05 | 15 | 15 | 2 | 13 | 0 | 13.3% | measured |
| 2026-07-12 | 1 | 1 | 1 | 0 | 0 | — | insufficient |
| 2026-07-19 | 19 | 16 | 10 | 6 | 3 | 62.5% | measured, provisional |
| 2026-07-26 | 38 | 16 | 16 | 0 | 22 | 100% | measured, provisional |
| 2026-08-02 | 24 | 3 | 3 | 0 | 21 | — | insufficient, provisional |
Is the trend interpretable yet, or merely present? Merely present at the right-hand edge, interpretable further back. The four fully-resolved measured weeks (05-24 through 07-05) show a real, moderate signal (13–25%) — a believable trend, not noise. The two most recent weeks (07-19, 07-26) show a sharp rise, but both are explicitly provisional with the majority of their cohort still pending (58% of 07-26's 38 PRs haven't had their window close) — reading that rise as "quality is collapsing" would be exactly the right-hand-edge artifact the AI-303 ticket warned about. The correct read today is: June/early-July give an interpretable ~15–25% baseline; the last two weeks are too fresh to trust and should be re-read once their windows close.
Known blind spots (stated, not hidden):
1. Survivorship — the metric's main one. An escape nobody has fixed yet (or ever) is invisible; this measures *found-and-fixed*, not *wrong*.
2. Fix-classifier undercounts. Only literal fix(...)/fix:/fix! prefixes count; a bare AI-244: doc-accuracy... title that is substantively a fix is never counted, whatever it did.
3. Attribution is one-hop git blame -C, not full SZZ. If a non-fix, non-move commit sits between the true origin and the eventual fix, the escape is missed rather than mis-attributed forward.
4. refactor(...)/chore(...) commits can't be escape origins, discovered live: without this, git blame's legitimate "last real-content toucher" semantics would pin a later genuine fix on a cosmetic rename instead of undercounting. Same direction of error as every other gap here.
5. Attribution is source-code-only (src//*.{ts,tsx,js,mjs}, scripts//*.{py,mjs,js}) — discovered necessary, not stylistic: an unfiltered run measured escape_rate=1.0 for a whole week purely from AGENTS.md/docs churn every PR touches. A genuine bug confined to a non-source file is invisible by construction.
6. Pure-insertion fixes are unattributable — git blame needs an existing line to anchor on; a fix that only adds new lines (no deletion/modification) can't be traced back to an origin.
[Post-merge defect attribution trend (12 backfilled weeks)](https://cursor.com/agents/bc-fa9639b8-9db5-44d9-9b76-eaea225a3090/artifacts?path=%2Fworkspace%2Freports%2Fdefect-attribution-trend.png)
<sub>To show artifacts inline, <a href="https://cursor.com/dashboard/cloud-agents#team-pull-requests">enable</a> in settings.</sub>
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a href="https://cursor.com/agents/bc-fa9639b8-9db5-44d9-9b76-eaea225a3090?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-fa9639b8-9db5-44d9-9b76-eaea225a3090&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div>