# Issue 8640: read-only proof audit, pass 1

Verdict: FIX_FIRST — clean matched before/after runtime evidence is missing. FRICTION: current GitCafe status response reports merge.conflicts=clean, but the installed schema accepts unknown|conflicting. This prevents a clean detail render before reaching the bug. This is evidence-environment friction, not a new code blocker in the reviewed diff.

Scope: base ed9b7ca9c53843ea1e6b466bf7d52928d10e7db0; head 6daf1fbf36; branch fix/issue-8640. No PR exists for this fix. Read AGENTS.md. No edits to repository, capture, browser, server, forge operation, or delegation performed. This report is a capture specification, not proof of execution.

## Existing proof and limits

Read /tmp/8640-evidence/reproduction.md and /tmp/8640-evidence/red-test.log. Preliminary Linux Chromium reproduction was 1280x800 and involved browser disconnection under memory pressure plus a server restart. It later required explicit Refresh to show the changed title. It is not clean same-process proof. Raw red test reports expected 'changed title', actual undefined at the refresh subscriber, while the service learned the changed title. Do not upload provider/startup logs: the reproduction warns they may contain pairing credentials. No clean before/after capture is currently established by this audit. Prior reviewers' no-code-blocker verdicts are supplied context, not independently repeated reviews.

## Change and caller inventory

Only PullRequestService.ts and its existing test file changed. sameReading short-circuits identical objects and compares JSON payloads excluding top-level observedAt. serveHeld answers the old detail, performs background read, and emits the existing environment-wide refresh signal only for changed content. Only lastGoodDetail.serveHeld currently calls this path; lastGoodSummary uses peek/record, not serveHeld. Strict allowStale=false and first/cold reads bypass this new notification branch. Transient failed/rate-limited reads can return the identical held object and remain quiet. No renderer, contract, layout, theme, or provider adapter changed; there IS visible behavior.

Trace: detail -> serveHeld -> pullRequestRefreshes -> ws pullRequestsSubscribeRefreshes -> client-runtime refreshTrigger -> web query atoms. Direct detail consumers: usePullRequestDetail (full page and chat right panel), PullRequestChecksPopover LazyChecksBody, PullRequestLinkPreview, ThreadDetailsPrRow, RightPanelTabs PR glyph, useOpenPanelPullRequestUrl, chat/useComposerMenuItems. ThreadStatusIndicators derives PR status through shared summary. usePullRequestDetail merges live/cache/localStorage snapshot/list/shared summary and overlays activity. A newer seed/list timestamp can mask the staged title, as happened in preliminary proof.

Signal fanout additionally reaches linked-summary, stack, activity, preview, reviewGuide, linkedThreads, list (first page only; cursor pages excluded), listStats, checks, changeBreakdown. Panel uses turnRefresh as Code tab backgroundRefreshToken. Diff/filesViewed/labelCandidates/reviewerCandidates are not direct refreshTrigger subscribers. No new action, setting, palette command, keybinding, MCP capability, or scheduled-task entry is added. Existing open-PR/menu/keybinding paths reuse those consumers; provider runtimes Codex/Claude/Cursor/Grok/OpenCode/Antigravity are unrelated to this source-control cache fix.

## Collapsed fixture matrix

Affected = can receive changed data or extra refetch from this fix. 'Control' means no new visual behavior is intended. All rows below are specified, not captured. Time or interaction rows REQUIRE recordings; stills cannot close them.

| Named fixture | Affected paths and conditional inventory | Required states / expected proof | Medium / capture scope |
| --- | --- | --- | --- |
| F1 Held title updates | Page detail heading, right-panel heading, shared/list title, thread PR row, preview card; live vs cached/list/snapshot precedence | Open, non-draft, held detail; ORIGINAL -> CHANGED with newer updatedAt; before remains original after provider read, after updates automatically without Refresh/navigation. Use a long changed title to exercise wrapping/truncation at 390; stable identity/reference | Recording; all four viewport/theme pairs; page is representative, other compositions remain explicitly unverified unless included |
| F2 CI completes without revision timestamp | Panel check badge/summary, expanded Summary Checks, action availability, checksStale mismatch; lazy checks popup and thread PR row | One pending -> one success, SAME updatedAt/head/base; include frozen successful companion check so popup running/completed grouping changes and Show all/less can appear/disappear. Keep one long name and one details URL to expose truncation/details column. Before stale; after success, matching counts and action state; no manual refresh during wait | Recording; all four pairs; open Checks before staging; popup/thread variants are distinct compositions, not proved by panel recording |
| F3 Unchanged refresh stays quiet | All signal subscribers; identity fast path, observedAt-only changes, retained detail/loading flags | At least two completed unchanged background reads after warm cache. No refresh feedback loop, no repeated signal, no blank/loading flash. Include same-reference transient fallback in focused test/runtime transcript if claimed | Recording plus timestamped RPC/provider/signal counters; all four pairs can share F1/F2 run. Quietness cannot be inferred from stills or absence of visible change alone |
| F4 Lazy/compact consumers | LazyChecksBody: error -> loading/no-data -> checks; LinkPreview: closed/open, detail card vs error fallback/URL tooltip; ThreadDetailsPrRow: collapsed/expanded; RightPanelTabs glyph | Mount/open while detail is already held; replay F1/F2 independently if claiming each path. Check updated title/status after signal, close/reopen, collapse/expand. Grouping: attention/running/completed; zero vs nonzero, mixed vs all completed; details URL null/non-null | Recording required for each claimed distinct composition; not part of minimal primary-panel bug proof; unverified until captured |
| F5 Empty/count/length boundaries | Panel changedFiles 0/1/many; additions/deletions zero/nonzero; checks 0/1/many; reviewers/labels empty/nonempty; body whitespace/long; comments 0/1/many/truncated and no filtered results; commits 0/1/many; popup showAll | Potentially affected data via full-detail comparison or activity fanout, but those transitions are not the two regression triggers. Freeze these values in minimal proof. No claim of boundary coverage. Counts/empty branches have no changed renderer logic | Distinct supplemental fixtures only if data-boundary coverage claimed; transitions require recording; excluded from minimum |
| F6 Loading/error/recovery | Cold detail spinner/no snapshot; snapshot while pending; no-data not-found vs error/retry; activity loading/unavailable; checks stale; transient provider failure with recent held value vs expired/auth failure | Cold/strict reads unchanged by new branch. Background read error should preserve held data and not emit a change notice; recovery to changed content can emit. Do not mask schema error by a UI-only mock | Recording plus transcript if claimed; cold/error variants unverified, excluded from minimal F1/F2 success proof |
| F7 State/capability branches | Open/draft/closed/merged; conflicting/unknown mergeability; auto-merge; workflow approval; permissions; merge method counts 0/1/many; stack present/absent/pending/error; code/guide tab supported/disabled | Full payload comparison can notify for these changes, but fixture holds them fixed. Merged uses longer cache TTL. Strict action preflight bypasses new branch; do not click merge/update/draft or mutate public PR | Supplemental transitions require recording; unverified |
| F8 Broadcast neighbors | List/filter/query empty/loading/error, first page vs cursor page; listStats; stack; activity/timeline; guide; linkedThreads; changeBreakdown; code-tab refresh token | Environment-wide signal can refetch unrelated mounted consumers. Freeze provider responses and check no disruptive loading/selection loss. Code/diff renderer unchanged; direct diff/filesViewed subscribers absent | Recording if claiming this fanout; unverified beyond visible neighbors included in minimum |
| F9 Theme/width | Desktop-width page and narrow responsive header/actions, long title/check label, dropdown/popover placement | 1440x900 light, 1440x900 dark, 390x844 light, 390x844 dark; fixed heights/DPR/zoom per paired run. Theme/width style code unchanged but four explicit combinations required | Four paired recording sessions for primary panel; no diagonal theme/width substitution |
| F10 Platform/connection | Local web; desktop web wrapper/IPC; native mobile useThreadPr summary/cache/link precedence; hosted web, remote/relay/tunnel/multi-environment | Linux local web is planned target. Desktop and native mobile unavailable/unverified. 390 web is not native mobile proof. Mobile summary is indirect signal consumer; it does not call new detail branch itself. GitHub/GitCafe shared service, only GitCafe fixture planned | Explicit scope limits; no claim of desktop/native/remote/provider parity |

## Minimum capture set

Eight recordings: base and head for each of 1440x900 light/dark and 390x844 light/dark. Each continuous run may contain F1, F2 and F3 with a clearly marked stage/reset between them. Expand Summary Checks before starting the no-interaction interval. Frame title, check rollup, expanded check list and available action state; use recorded scroll segments at 390 where they cannot fit. Start from original/pending and unchanged reads, then title change, then CI success with unchanged updatedAt, then unchanged reads. Preserve each no-interaction interval through provider completion and the resulting client re-read; do not reload or restart. Save matched still endpoints only as supplemental overview, never as the timing proof.

This is the minimum for the primary panel defect, not exhaustive consumer validation. If claiming full affected-surface verification, add paired F4 recordings (hover card, lazy popup, compact/expanded thread row) in all four combinations, and include F5-F8 branches actually claimed. Do not silently mark those rows passed because page detail passed. Opening a popup or manual refresh after the observation window must be visibly labeled; a manual refresh cannot establish automatic propagation. Before and after need the same fixture sequence, credentials, normalization and timing criteria, on separately attested revisions. Annotate provider response times and client detail/signal counts in a sanitized sidecar; a held cached reread alone is not proof the host was fetched.

## Rebuildable seed plan for the capture agent

Use isolated worktree .t3 state only. Existing environment f2d2a621-e7a6-4134-9fa0-a4b8086363c3 and project abaff30b-8067-4cbc-aecf-6f2598d1d925 are supplied inputs, not verified database state by this auditor. Route:

`/pull-requests?repository=pingdotgg%2Ft3code&number=23210&projectId=abaff30b-8067-4cbc-aecf-6f2598d1d925&host=git.cafe&q=Claude%20sessions`

Live app source-control identity: GitCafe [#23210](https://git.cafe/pingdotgg/t3code/pulls/23210). Read it through the real app service/RPC/query/render path, with a deterministic local provider-response wrapper. Never mutate that public PR. Existing /tmp/issue-8640-fetch-v2.mjs intercepts only the base PR GET and stages title/updatedAt using /tmp/issue-8640-stage.json; /tmp/issue-8640-pull-shape.json holds public PR data. These are starting material, not a complete rebuildable fixture: v2 still reads real status/check/activity and cannot normalize current schema incompatibility or freeze their drift.

Capture agent must commit a narrowly scoped seed script plus public-safe fixture (not recordings, scratch report, auth, or database) before calling evidence rebuildable. Script should recreate isolated project/repository linkage and wrapper stages for BOTH revisions, provide documented invocation/reset, and verify the route resolves this project. Retain the existing auth unchanged. Copy existing isolated state if needed; never start against live ~/.t3/userdata. Keep auth/startup URLs out of source, recordings, and uploads. No VITE_HTTP_URL/VITE_WS_URL overrides.

Wrapper must intercept the provider response(s) actually consumed by detail, checks, summary/list and activity as necessary, not fake browser DOM or client RPC results. Freeze all unrelated public data, timestamps, check ordering, permissions, head/base and list/snapshot timestamps. Normalize unsupported merge.conflicts=clean to accepted unknown at the exact raw status response boundary identically BEFORE and AFTER; explicitly label this as fixture compatibility normalization, not production-host validation or proof of clean mergeability. Derive the correct provider response shape from adapter code; do not guess a downstream detail field. Seed title timestamps so list/shared/localStorage snapshots cannot dominate the intended staged title. Use a dedicated browser storage profile or isolate only this test reference's persisted snapshots; no global auth reset.

Title stage changes title and updatedAt only. CI stage changes status pending->success only with updatedAt unchanged. Quiet stage changes no semantic field (observedAt may advance). Include deterministic stage logs and a manifest with full runtime SHA, wrapper/fixture hash, state seed revision, UTC times, browser, viewport, DPR, theme and normalization note. Freeze fixture inputs, not the real application clock or cache scheduler. Do not lower production refresh intervals; record the real automatic interval and provider fetch milestone. The actual local dev URL/ports must come from capture agent's own runner, and are intentionally not invented here.

## Completion gate

FIX_FIRST remains until reproducible seed/fixture exists and clean same-process paired recordings close F1-F3/F9. FRICTION remains until status-shape normalization is applied consistently and disclosed. Scope must continue to say desktop/native mobile/hosted/remote paths and supplemental F4-F8 variants unverified unless separately exercised. This pass uploads only this authored public-safe matrix, not raw logs, fixtures, credentials, internal database contents, or provider response dumps.
