# Issue 15702 — proof audit pass 1

Read-only source audit, 2026-10-25. No product edits, server launches, captures, device sessions, pushes, comments, merges, or watches. All proof below is REQUIRED/PENDING, not evidence of visual verification.

- Issue: [#15702](https://github.com/pingdotgg/t3code/issues/15702). PR: [#24317](https://github.com/pingdotgg/t3code/pull/24317).
- Audited local HEAD: [08f47fada7](https://github.com/pingdotgg/t3code/commit/08f47fada71c02c17e5d6efc8ded0b49c2c59664), full SHA `08f47fada71c02c17e5d6efc8ded0b49c2c59664`.
- Requested before/base: [b3d382e100](https://github.com/pingdotgg/t3code/commit/b3d382e1009d5430de8c528aef99a39854a7cbc4), full SHA `b3d382e1009d5430de8c528aef99a39854a7cbc4`.
- Entire comparison read: five files, 64 additions / 9 deletions. Three product files: shared resolver/type, web presentation/settled hover, native presentation. Two test files.
- Local `main` currently resolves to `07d38b8d17167c8d17fba1184dbe13a307fada74`, not the requested before commit. Use explicit SHAs for comparisons. GitCafe PR metadata reports published head `debf6ea643ea8348ca2bd07bc1708834617e4676`; this audit does not claim the local revision is published. No fetch/rebase performed.
- Historical screenshots/recordings and checks on earlier SHAs do not satisfy any current-head row.

## Minimal fixture basis

Twelve logical fixtures, created by transforming the downloaded JSON outside the repository. IDs, titles and lineage root IDs must be unique. Use the same fixture bytes on both revisions. Preserve independent branches (`fixture-N` against `main`) unless a stack is explicitly intended. Keep provider turns stopped. **A** means changed behavior, **C** unchanged control, **B** semantic boundary. Each parameter listed below is part of the fixture, not permission to omit it.

| ID | Affected | Input / minimum transformation | Expected current behavior | Why retained |
|---|---|---|---|---|
| F | A | Supplied six unrelated links: three closed, three ready open; active, not selected row available | Neutral ordinary PR glyph, `+6`, accessible description `6 linked pull requests in different states`; native `state:null`, not draft | Exact reported defect; open-containing mixed group |
| FS | A | F clone with `settledOverride:'settled'`, fixed `settledAt`; show unselected/unhighlighted search result | Settled search remains neutral on hover; no green/red/violet hover state | Only newly added switch arm; active F cannot exercise it |
| HC | C | Two unrelated closed links | `+2`, overall closed; web red; native aggregate rose icon/text | Homogeneous closed branch |
| HO | C | Two unrelated ready open links | `+2`, overall open/green | Homogeneous open branch |
| HM | C | Two unrelated merged links | `+2`, overall merged/violet | Homogeneous merged branch |
| HD | C | Two unrelated open links, both `isDraft:true` | `+2`, overall draft/muted; draft icon, not mixed | All-draft branch |
| S | C | Two related links, lower head `stack-base`, upper base `stack-base`; lower closed, upper open | Stack glyph and count `2`, overall open/green, list action; never mixed | A single actual chain bypasses new mixed rule |
| PC | A/B | Two unrelated links: one `snapshot:null`, one closed | Raw missing snapshot defaults to open for resolver, so mixed neutral `+2`; individual list distinguishes pending from closed | New mixed branch with incomplete data |
| PP | C/B | Two unrelated links, both `snapshot:null` | Resolver overall open `+2`; individual entries pending. This is existing behavior, not a promised pending aggregate fix | Distinguishes all-unknown from PC |
| DR | C/B | Two unrelated open links, one draft and one ready | Overall open/green, not mixed, because comparison uses snapshot.state only | Prevents assuming draft is a fifth distinct comparison state |
| ONE | C | One closed link | Individual number/status, link/open action rather than aggregate list action | Single-PR render/action branch |
| NONE | C | Only source `stack-dismissed` links (or empty array) | No linked aggregate; ensure branch/legacy fallback is absent for this fixture | Visible filtering and empty branch |

Additional reducer-only permutations: unrelated closed+merged => mixed despite no open; derived stack plus one unrelated link => mixed when states differ; reorder F links => same aggregate/count; GitLab links => merge-request terminology; mixed source hosts => terminology comes from current selected link. They reuse the mixed presentation branch, so no extra full capture set is needed. Do not infer a stack from count alone: validate chain construction.

## Capture expansion and minimum scene matrix

Web cell set **W = 1440/light, 1440/dark, 390/light, 390/dark**, CSS pixels, with fixed height/DPR/zoom recorded by parent. Every W row marked W runs all four cells at both explicit SHAs. Native **N = EAS iOS native phone portrait, light and dark**, fixed device/OS/size, both SHAs. A 390-wide web capture is not native evidence. Native static fixture feasibility is discussed below.

| Row | Affected | Caller and state | Fixture | Cells | Required proof |
|---|---|---|---|---|---|
| W1 | A | Normal sidebar row plus composer branch-toolbar badge | F | W | Before/after stills; 1440 can share whole view; 390 needs separate real sidebar drawer and composer views; neutral `+6` in both |
| W2 | A | Sidebar and composer tooltip mini-list; click badge into full PR list; close and reopen through other entry point | F | W | **Recording** from visible trigger through hover/focus, list navigation and return; assert three open and three closed, no navigation to arbitrary PR on aggregate click |
| W3 | A | Sidebar search PR-number match, active and settled result | F + FS | W | Before/after still of both results; **recording** search entry, pointer enter/leave on settled row and keyboard focus/selection, then activate result |
| W4 | A | Command palette thread search, matching PR number, active and settled result | F + FS | W | Same proof as W3; palette is a separate caller, not covered by sidebar search. Search by a linked PR number, e.g. 15694; title-only match does not guarantee badge |
| W5 | C | Sidebar homogeneous badges grouped in one viewport; composer selected control(s) | HC/HO/HM/HD | W | Compact control still per cell; verify all four icon/tone/count meanings, selected composer cases can share a short sequence |
| W6 | C | Sidebar/composer stack badge and list | S | W | Still for stack glyph/count; list navigation can join W2 recording |
| W7 | A/B | Snapshot fallback and draft/ready boundary in sidebar/composer | PC/PP/DR | W | Compact state stills; PC mixed; PP/DR remain open; tooltip pending states visible. No artificial timers required |
| W8 | C | Single and no-visible-link control | ONE/NONE | W | Compact still plus link target/accessibility inspection; ordinary number vs absence; no aggregate list for ONE |
| W9 | A | Data refresh changes same unrelated group mixed → all closed → mixed | F then HC-equivalent six-link state then F | One 1440 cell, one 390 cell | **Recording** plus state milestones; preserve identities/count so stale cache/color changes are observable. Use app command/sync for genuine behavior; remounting fixture variants proves only rendering |
| M1 | A | Native home/thread-list row normal, selected, settled and search result | F + FS | N | Before/after stills; neutral PR SF symbol, `+6`, muted text; accessibility tree includes different-states label; use actual native home search row |
| M2 | A | Native badge tap and accessibility action from list/search → ThreadPullRequests → back; long-press row then release | F | N | **Recording**; shows six individual statuses; no accidental badge navigation after long press; action label begins `Show` |
| M3 | C/B | Native row controls for homogeneous, stack, missing snapshots, draft/ready, single and none | HC/HO/HM/HD/S/PC/PP/DR/ONE/NONE | N | Group in compact native list stills; selected/receded appearance checked with F in M1; ONE uses open action, aggregates use list action |
| M4 | C / not aggregate caller | Native composer PR suggestions (`#` trigger), git controls, and linked-PR list | F | N | One inspection and short **recording** opening suggestions/list and returning; individual PR states remain accurate; do not expect a native composer `+6` aggregate |

Minimal capture organization: W1–W8 can be batched into four viewport/theme sessions per SHA, with static control states co-located in a fixture project. W2/W3/W4/W6/W8 actions may share one indexed recording per cell; do not replace action proof with stills. W9 requires actual same-thread update instrumentation; label pending if unavailable. Native needs two theme sessions per SHA and indexed recordings, after fixture-mode blocker is solved. Counts of image files are not the acceptance criterion: every row/cell must be identifiable. No settings-only or unrelated provider fixture is needed; no provider adapter/contract/server logic changed. Desktop uses the same web renderers: one Electron shell smoke through badge/list is sufficient if parent includes desktop; it does not replace W.

## Source trace / why these rows exist

Links pinned to the audited head:

- [Resolver and badge discriminated union](https://github.com/pingdotgg/t3code/blob/08f47fada71c02c17e5d6efc8ded0b49c2c59664/packages/source-control-core/src/shared/threadPullRequests.ts#L278): visible snapshots map null to open; all-draft and homogeneous state rules precede chain detection; only unrelated multi-chain output can become mixed. Client runtime re-exports configured source-control-core helper.
- [Web presentation, control, tooltip and action](https://github.com/pingdotgg/t3code/blob/08f47fada71c02c17e5d6efc8ded0b49c2c59664/apps/web/src/components/ThreadStatusIndicators.tsx#L168): mixed branch ordinary glyph/muted `+N`; buttons open list for stack/multiple; single uses link; tooltip retains individual snapshots.
- [Sidebar caller](https://github.com/pingdotgg/t3code/blob/08f47fada71c02c17e5d6efc8ded0b49c2c59664/apps/web/src/components/sidebar/SidebarThreadRow.tsx#L1001) and [composer toolbar caller](https://github.com/pingdotgg/t3code/blob/08f47fada71c02c17e5d6efc8ded0b49c2c59664/apps/web/src/components/BranchToolbarBranchSelector.tsx#L682). Branch-selector panel mode omits this badge; capture toolbar mode rather than claiming panel proof.
- [Search presentation and settled hover](https://github.com/pingdotgg/t3code/blob/08f47fada71c02c17e5d6efc8ded0b49c2c59664/apps/web/src/components/ThreadStatusIndicators.tsx#L1029), [sidebar search caller](https://github.com/pingdotgg/t3code/blob/08f47fada71c02c17e5d6efc8ded0b49c2c59664/apps/web/src/components/sidebar/SidebarThreadRow.tsx#L1646), [command palette caller](https://github.com/pingdotgg/t3code/blob/08f47fada71c02c17e5d6efc8ded0b49c2c59664/apps/web/src/components/CommandPaletteThreadItems.tsx#L111). Settled uses secondary-label base and mixed hover returns no colored override. Search is a span inside a result button, not a badge list control. Leading per-PR status is separate and may retain current individual status; do not demand every icon become neutral.
- [Native presentation](https://github.com/pingdotgg/t3code/blob/08f47fada71c02c17e5d6efc8ded0b49c2c59664/apps/mobile/src/state/thread-pr-presentation.ts#L60), [capability-gated hook](https://github.com/pingdotgg/t3code/blob/08f47fada71c02c17e5d6efc8ded0b49c2c59664/apps/mobile/src/state/use-thread-pr.ts#L41), [native thread row rendering](https://github.com/pingdotgg/t3code/blob/08f47fada71c02c17e5d6efc8ded0b49c2c59664/apps/mobile/src/features/threads/thread-list-v2-items.tsx#L1173), [navigation](https://github.com/pingdotgg/t3code/blob/08f47fada71c02c17e5d6efc8ded0b49c2c59664/apps/mobile/src/features/threads/use-open-thread-pull-requests.ts#L13). Search reuses the row. Mixed maps to null, yielding ordinary muted glyph; long press is explicitly consumed.
- Native composer [passes individual links to suggestions](https://github.com/pingdotgg/t3code/blob/08f47fada71c02c17e5d6efc8ded0b49c2c59664/apps/mobile/src/features/threads/ThreadComposer.tsx#L563), [query maps individual snapshots](https://github.com/pingdotgg/t3code/blob/08f47fada71c02c17e5d6efc8ded0b49c2c59664/apps/mobile/src/state/queries.ts#L87). It does not call the changed aggregate resolver/presenter. ThreadGitControls reads gitStatus.pr independently. Native composer aggregate screenshots would prove an invented surface.

## Seeding on the new base

The supplied [fixture JSON](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/d0a70f2b2f4f2377/issue-15702-fixture.json) and [seed script](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/7edb67f160300de0/issue-15702-seed.py) were downloaded/read, not executed. Script expects sibling **fixture.json**, not the uploaded basename issue-15702-fixture.json. Rename the downloaded JSON in scratch.

Path remains `<worktree>/.t3/userdata/statev2.sqlite`. For explicit home, it is `<home>/userdata/statev2.sqlite`, but this script is hard-coded to worktree/.t3; it cannot target arbitrary explicit homes without a scratch adaptation. Never point it at live ~/.t3. Use distinct isolated before/after state copies and stop their servers before writes.

Read-only PRAGMA inspection of the existing local sandbox confirmed both table names and required columns used by the script, including provider_instance_id. It has one project and one V2 thread row. The full diff contains no schema/seeding changes. Current AppThread schema still accepts the supplied fields; payload_json stores the AppThread object itself, not `{thread:...}`. ProjectionStore reads that JSON and constructs full projections. Existing real template row supplies any additional SQL columns.

Parent sequence, not executed here:

1. On each explicit revision, initialize/migrate isolated .t3 once (or use `vp run migrate-dev-db` with server stopped for safe stopped data). Confirm migrations complete and at least one stopped V2 thread exists. Copy data, never symlink live state.
2. Keep files together outside repo as seed.py and fixture.json. Stop isolated server, inspect PRAGMA, back up its sandbox DB. Run `python3 /tmp/<scratch>/seed.py /absolute/worktree --servers-stopped` only after confirming the real server is stopped. Flag is an assertion, not process detection.
3. Extend the fixture population in a scratch-only seeder for remaining matrix IDs, cloning a current AppThread template. Use current projection tables, unique IDs and lineage roots, consistent project IDs/ISO timestamps. Clear active provider thread and do not copy running sessions/tasks. Set F explicitly active if automatic age-based settling would hide it; keep FS explicitly settled. Use fixed time for comparable relative labels.
4. Reopen isolated runtime and verify capability `threadPullRequests:true`; otherwise callers use legacy fallback and never hit changed path. Confirm snapshots remain stable and no external refresh overwrites synthetic fixture states. Fake repository is not real forge proof. No event-history/backend behavior claims from projection writes.
5. Current helper `node apps/server/scripts/t3-sqlite-state.ts query --base-dir <isolated-home> --sql 'PRAGMA table_info(orchestration_v2_projection_threads)'` can inspect. For new SQL fixtures use that helper's exec backup path; one statement per invocation. Existing downloaded Python does not create backups itself.

The original seed creates **only F and FS**, not the entire matrix. It is compatible by inspected structure, not proven by executing a restarted app. It reuses a fixture project ID and writes projection_projects workspace_root to `<worktree>/apps/server`; choose an isolated actual Git workspace if a caller requires repository identity for composer suggestions. Its generic thread template read is unfiltered LIMIT 1; parent must confirm template is stopped. Direct fixtures are ephemeral and can be overwritten by event reprojection or a real mutation.

## Native EAS iOS static fixture feasibility

**Not available as an existing offline fixture mode at either requested revision.** This is a source finding, not an EAS availability/auth check.

`EXPO_PUBLIC_SHOWCASE=1` does not replace App with static fixtures. [App still loads ordinary runtime](https://github.com/pingdotgg/t3code/blob/08f47fada71c02c17e5d6efc8ded0b49c2c59664/apps/mobile/src/App.tsx#L51). [Showcase coordinator](https://github.com/pingdotgg/t3code/blob/08f47fada71c02c17e5d6efc8ded0b49c2c59664/apps/mobile/src/features/showcase/ShowcaseCaptureCoordinator.tsx#L72) calls connectPairingUrl and waits for actual projects/threads. Native launch controls accept scene/theme/pairing values; no JSON fixture import is exposed. [Host showcase script](https://github.com/pingdotgg/t3code/blob/08f47fada71c02c17e5d6efc8ded0b49c2c59664/scripts/mobile-showcase.ts#L1492) starts isolated backends, seeds legacy rows and restarts for V2 import, then runs Metro and native capture. It is not an offline EAS script, and built-in scene IDs target fixed showcase thread IDs, not this fixture.

Two meanings of “static” must be separated:

- Static native binary with embedded current JS: feasible in principle as an iOS simulator build uploaded to EAS Simulator, **but still needs a reachable paired isolated backend** to obtain these fixtures. No Metro needed for a Release binary; before and after require separate builds (or exact provenance matched artifacts). Current eas.json has no dedicated ios.simulator fixture profile; an ordinary device archive is not installable on simulator.
- Offline static fixture dataset with no backend: requires a new temporary harness/bundle entry or runtime mock injection at atom/connection boundaries. No such supported entry point exists here. Product editing is prohibited in this pass, so do not claim M rows completed by showcase flag or web emulation. Parent must resolve this scope or use a paired isolated backend. EAS transport/build access was not checked and no session started.

For an eventual offline harness, feed the real hook capability plus shell/row data into actual native components; simply rendering the pure presentation output misses native symbol selection, selected/receded styling, navigation and accessibility actions. It would prove renderer output only; M2/W9 behavior still needs a working navigation/runtime route. Keep any harness outside product commits and disclose it as fixture proof.

## FRICTION / handoff

1. Published PR metadata head differs from audited local head; historical evidence cannot be relabeled. Parent must retain explicit revision provenance and refresh metadata before publishing evidence.
2. Requested base differs from moving local main. Before build must pin b3d382e100, not the branch name main.
3. Seed filename mismatch, hard-coded home path, only two fixture rows, no automatic backup or real stopped-process check. These are test-tool limitations, not product defects.
4. Native offline static fixture mode is absent; EXPO_PUBLIC_SHOWCASE requires backend pairing and its scene coordinator uses fixed showcase IDs. Resolve before promising EAS static-fixture proof.
5. Composer PR suggestions need project repository identity/capability; original seed project may not supply it. Native composer is an individual-state control, not an aggregate badge caller.
6. W9 transition proof cannot be inferred from two static seeded screenshots. Record a same-thread persisted update or leave it pending. No capture or test execution occurred in this pass.

Minimum handoff: 12 fixture datasets; four web viewport/theme cells per revision across sidebar, composer, sidebar search and command palette; two native iOS theme cells per revision across normal/search/settled rows and list navigation. Record every interaction/time row. Parent owns integrated verification and all captures.
