# PR #22987 proof audit (pass 2)

**Verdict: SUFFICIENT**

Code under test is `6a346ccad1`. The evidence commit is `b12b6c393e`. Before is `8d3b061121`. Every forge response is STUBBED: an HTTPS GitHub Enterprise stub on `github.localtest.me`, `[::1]:443`, inside `unshare -rnm`.

## SHAs and scope
- `git diff 6a346ccad1 b12b6c393e --stat` touches only `apps/server/scripts/evidence/pr-22987/` (6 files). Nothing else changed.
- `6a346ccad1` is the merge of the PR head `afd9b4262f` with `8d3b061121`. `git merge-base 6a346ccad1 origin/main` = `8d3b061121`.
- The 7 swapped source files are in `/tmp/ev-22987/swapped-files.txt`. They are exactly the non-test files touched between `8d3b061121` and `6a346ccad1`. `git diff 8acbefefc5 8d3b061121 -- <those 7>` is empty.
- `recorded/{before,after}/results.json` carry `label` before/after. The transcript names `8d3b061121` and `6a346ccad1` in every row header. `recorded/*` is JSON-equal to `/tmp/ev-22987/{before,after}/results.json`; the only differences are byte-level formatting.

## Fixtures
- `node driver.ts --emit-fixtures`, run on a copy in scratch, produces JSON identical to the committed `fixtures.json`. The builders and the data agree.
- The shapes are realistic GitHub:
  - `type:"FORBIDDEN"`, with messages that are exactly "Resource not accessible by personal access token" and "Resource not accessible by integration".
  - `locations`, and paths `repository.pullRequest.commits.nodes.0.commit.statusCheckRollup.contexts`, `…contexts.nodes.<i>` and `…reviewRequests.nodes.0.requestedReviewer`.
  - 403 bodies are `{errors}` only. `NOT_FOUND` comes with `pullRequest:null`.
- The stubbing is labelled in the driver header ("Every forge answer is STUBBED") and in the transcript environment line.

## Per row (client-facing `pullRequests.detail` result; request sequence `[detailedChecks,requestedReviewers]`)
| Row | Before | After | Matches matrix / code? |
|---|---|---|---|
| F1 | 2 real checks, reviewer; 1 req `[unsent,100]` | 2 real checks, no `checksUnavailable`; 1 req `[true,100]` | yes |
| F2 | `PullRequestOperationError` detail "…by personal access token"; 1 req | degraded: `checks=[Overall check status/success]`, `checksUnavailable` set, reviewers/labels/body/headSha kept; 2 req `[true,100]`,`[false,100]` | yes (key row) |
| F2b | error (PAT); 1 req | degraded, status `failure`; 2 req | yes |
| F3 (403, integration msg) | error "…by integration"; 1 req (403) | degraded success; 2 req (403 then 200) | yes, and it fills the unit-test gap |
| F3b (path len 10, partial data) | error (2 PAT msgs); 1 req | degraded, status `pending`; 2 req | yes |
| F4 (reviewers+contexts, 200) | error; 1 req | degraded, `reviewers:[]`; 2 req `[true,100]`,`[false,0]` | yes (`isRefusedDetailFieldsOnly`) |
| F4b (reviewers only) | detail, real checks, `reviewers:[]`; 2 req `[unsent,100]`,`[unsent,0]` | same; 2 req `[true,100]`,`[true,0]` | yes (regression guard) |
| F4c (combined at 403) | error | error, 1 req | yes (documented edge) |
| F5a generic "Forbidden" | error | error, 1 req | yes |
| F5b 500 INTERNAL | error | error, 1 req | yes |
| F5c PAT at `contexts.totalCount` | error | error, 1 req | yes |
| F5d contexts refusal + INTERNAL | error | error, 1 req | yes |
| F5t socket destroyed | "Could not reach GitHub…" | same, 1 req | yes |
| F6 >100 checks | 105 checks; Core + 2 walk pages | 105 checks; same 3 req | yes |
| F6b walk refused | error; 2 req | degraded `failure`; 3 req Core`[true]`, walk, Core`[false]` | yes |
| F7 head changed | error "unreadable getPullRequestDetail response" | same | yes, with a note (below) |
| F8 not found | `reason:"not-found"` | same, 1 req | yes |
| F5 rate-limited | not captured | not captured | gap, accepted: the matrix flags it as poisoning the host limiter; negative paths are covered by F5a-d |
| F9 REST check-runs | not captured | not captured | gap, accepted: it is not reachable via `pullRequests.detail` and is unit-tested at L3576 |

No error has a `reason` except F8's `not-found`. The matrix's "reason failed" for F2-before is really an absent `reason`. That is harmless.

**F7 note:** the client sees the generic `GitHubPullRequestReadError.message` ("GitHub returned an unreadable getPullRequestDetail response."), not "head changed". The matrix's `detail:~"head changed"` is inaccurate. The "Pull request head changed while reading checks." cause appears only in the server trace, in both runs: at `GitHubPullRequestApi.ts:1520` after, and at the old `filterOrFail` before. Behaviour is unchanged, so this is not a regression.

The request counts agree with `getPullRequestDetail` at `6a346ccad1` (L1475-1540): `read(true)` then `catchIf(isRefusedCheckContextsOnly, read(false))`; reviewer-only retry keeps `detailedChecks`; the combined refusal forces `false,0`; the walk runs only when `checksTruncated && detailedChecks`.

## Rebuild
I ran `node apps/server/scripts/evidence/pr-22987/driver.ts --label after --out /tmp/ev-22987/audit2/rerun-F2 --rows F2,F5a` from the worktree (Node v26.12.1). The run exited 0.
- F2: the result is identical to `recorded/after` (ignoring projectId/workspaceRoot/observedAt), and the GraphQL request sequence is identical. The one extra log line is the one-time `GET /api/v3/user` credential check. It is attributed to the first row run, which is F1 in the full recorded run.
- F5a: the result and the requests are identical.

## Hygiene
- The driver sets `T3CODE_HOME` and bootstrap `t3Home` to `<out>/home`, and nothing references `~/.t3/userdata`. The rerun's home was created under `--out`. The worktree has no `.t3`.
- The server is spawned by the driver and stopped by its handle with SIGTERM, then SIGKILL after 10s. PID 694063 is gone, and port 35521 is free.
- The stub and `/etc/hosts` exist only inside the namespace, which dies with the process.
- `git status` in the worktree is clean after the rerun.
- The `~/.t3/userdata/statev2.sqlite*` mtimes moved during the audit. That is the live T3 instance hosting this session; the driver does not write there.
