# Proof audit, pass 1

PR [#32994](https://git.cafe/pingdotgg/t3code/pull/32994), branch `fix/issue-32977`, audited head [c4238adea1](https://git.cafe/pingdotgg/t3code/commit/c4238adea165b4d853298ce8b8b0d51f91863b53). Read-only audit of `/home/user/wt/pr-32994`; no source edits, comments, pushes, clients, servers, emulators, or leases started.

## Findings

1. Existing evidence is insufficient under the brief. All nine attachments in the PR body are still PNGs: four before/after alert endpoints, four image differences, and one successful image endpoint. None records tap → resolution → alert/viewer → dismissal. Pixel stability of two stills does not establish that interaction.
2. The existing iOS missing-file endpoints remain valid for the changed alert at the audited head. The main merge preserves the message selector, resolution error handling, gallery policy, iOS native viewer, and iOS `Dialog.alert`. The successful image endpoint also remains valid. They are **not stale** merely because the files moved. They still do not prove the complete flow.
3. Missing changed-message branches: no `checkedPath` but a resource `path`; no usable path at all (notably a missing tool-output image); Android; and path-length/viewport stress. The relative-path fallback can occur on the current server too: workspace resolution can construct the error without `checkedPath`. It is not exclusively an older-server fixture.
4. Gallery failures differ by platform. iOS leaves a failed nonselected image in its slot and keeps the selected image open; Android closes the gallery and alerts even when the failure is a sibling. The newly retained typed error changes that Android alert. The PR's single-image stills prove neither path.
5. The changed rejection contract also reaches iOS video resolution/reauthorization and attachment initial/stale URL resolution. Their new catches intentionally preserve the prior visible errors. They need representative regression recordings, not a cross-product of every media format. Android video uses the unchanged `useRefreshAssetUrl` hook, not the changed standalone function.

## Source and merge audit

Changed production paths:

- [FilePreviewSession.tsx](https://github.com/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/components/ui/FilePreviewSession.tsx#L42): typed not-found → checked path, otherwise resource path, otherwise error message; other failures → existing reconnect copy. Resolution occurs before choosing the image/PDF/document renderer. A custom `onOpenError` receives the original cause instead of showing the default alert.
- [assets.ts](https://github.com/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/state/assets.ts#L116): missing connection returns null; query Failure now rejects with the squashed cause; successful resolution uses the currently prepared connection. Its fresh-URL atom catches back to null and retains the prior attachment error.
- [VideoPreviewModal.tsx](https://github.com/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/components/ui/VideoPreviewModal.tsx#L50): pre-presentation resolution catches to null; post-presentation reauthorization suppresses rejection. Connected and disconnected errors have different copy; failure after native presentation has its own save/share guidance.
- [attachmentDocument.ts](https://github.com/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/lib/attachmentDocument.ts#L37): text reads refresh when authorization is null or older than five minutes; failures catch to null and retain reconnect copy.

Compared capture head [03406c6b95](https://git.cafe/pingdotgg/t3code/commit/03406c6b95) with the audited head using `git diff <capture-head> <audited-head> -- apps/mobile/src`, then compared the relocated files explicitly. The original before captures use [ade473018d](https://git.cafe/pingdotgg/t3code/commit/ade473018d). The old shared FilePreviewModal becomes FilePreviewSession plus platform wrappers; `Platform.OS === "ios"` becomes `progressiveGallery=true` on iOS and false on Android. The iOS native viewer's presentation/update/dismiss logic is preserved. The video file changes only import paths. `assets.ts`, `attachmentDocument.ts`, both Dialog implementations, previewPresentation, localAttachmentPreview, attachmentDownload, and the asset contracts/runtime helpers have no merge changes. The iOS native file presentation implementation is unchanged; additions in the native controls module concern other functionality. Gallery builders and markdown media resolution change import paths only. Android's image renderer extraction is broader, but there was no Android capture to invalidate.

Thread/feed/header/markdown rendering did change on main (including feed scrolling and timestamps). Therefore the existing full-screen images must not be presented as pixel-identical captures of the current head. Their **alert and image endpoints** remain representative; fresh recordings must exercise current navigation/presentation. No new pixel comparison was run or claimed.

## Caller inventory and collapse rules

All direct FilePreviewModal caller files in the audited source:

| Caller | Source shapes / presentation differences | Matrix rows |
|---|---|---|
| [ThreadFeed](https://github.com/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/threads/ThreadFeed.tsx) | Markdown media chips/images/PDFs; remote media-file, captured attachment, direct URI, tool-output images; message galleries; no custom error callback | F1–F10, U1–U3 |
| [FileMarkdownPreview](https://github.com/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/files/FileMarkdownPreview.tsx) | Thread or draft-workspace media; direct URI; captured mode blocks workspace resolution | Same default remote alert; F1–F8, U2–U3 |
| [ComposerContextSheet](https://github.com/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/components/ComposerContextSheet.tsx) | Remote attachment, local file-backed attachment, or inline URI; image/PDF; context galleries | F7–F11, U2–U3 |
| [ComposerContextAttachment](https://github.com/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/components/ComposerContextAttachment.tsx) | Resolved image URI; remote/local actions source affects sharing, not URL resolution | U2–U3 |
| [ThreadComposer](https://github.com/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/threads/ThreadComposer.tsx), [QuestionAttachments](https://github.com/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/threads/QuestionAttachments.tsx), [ReviewCommentComposerSheet](https://github.com/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx) | Draft local attachment / inline URI, composer galleries; ThreadComposer restores focus after close | U1–U3 |
| [PullRequestMarkdown](https://github.com/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/pull-requests/PullRequestMarkdown.tsx), [PullRequestMedia](https://github.com/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/pull-requests/PullRequestMedia.tsx) (media and attachment views) | Credentialed source-control media or direct URI; image gallery; generic server failure | F7, F9–F10, U2–U3 |
| [WorkspaceFileImagePreview](https://github.com/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/files/WorkspaceFileImagePreview.tsx) | Already resolved image URI | U2 |
| [ThreadFilesRouteScreen](https://github.com/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx) | Already resolved PDF URI; remote actions source | U2–U3 |
| [AttachmentFileScreen](https://github.com/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/files/AttachmentFileScreen.tsx) | Remote attachment/local source; custom error callback → inline native error; reader/fresh URL state | F11, D1–D2, U2–U4 |
| [ZipArchiveView](https://github.com/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/features/files/ZipArchiveView.tsx) | Extracted URI; custom error callback → inline error | U2–U4 |
| [MermaidCodeBlock](https://github.com/pingdotgg/t3code/blob/c4238adea165b4d853298ce8b8b0d51f91863b53/apps/mobile/src/native/MermaidCodeBlock.tsx) | Generated image URI; explicit empty gallery | U2 |

Video callers are ThreadFeed, ThreadComposer, QuestionAttachments, ComposerContextSheet, FileMarkdownPreview, and PullRequestMedia. They collapse into remote resource, direct media URI, and local attachment; only the iOS remote-resource branches exercise changed calls. `useFreshAssetUrl` has one production consumer, `useAttachmentDocument`, used by AttachmentFileScreen. Its stale text reader is a separate changed call.

Other branch inventory: null source closes/unmounts; gallery includes images only, deduplicates identities, inserts a missing selected item, freezes the group, and tracks selected index. Direct URIs resolve synchronously; local attachment leases resolve or fail separately; remote resolution is asynchronous. iOS images present while resolving; Android waits for every URL. Abort/unmount suppresses late errors and releases leases. Documents/PDFs wait for a URI; native-open errors differ from URL-resolution errors. Document errors can offer Cancel / Save or share; sharing may fail with a separate alert. PDF actions source is supplied or synthesized; name/MIME/sourceIdentifier/srcFragment affect successful native presentation but not the new message selector. No new app-width or theme branch exists in the session, but text length, platform dialog renderer, theme, and text scaling affect the alert layout.

## Fixtures and minimum capture set

`I` = iOS phone, 402×874 logical pixels, default text size (matches existing evidence). `A` = Android phone, 360×800 logical pixels, default text size. `IS` = iOS narrow phone, 375×667 logical pixels. `L/D` = light/dark OS appearance and corresponding app appearance; stress text scale is explicitly recorded. A listed size is a target viewport, not a claim that a device was opened during this audit.

Every row involving a tap, async completion, timeout/age, retry, page change, close, or navigation requires a **recording**. Existing endpoints are acknowledged in the requested yes/no/stale column; “yes — endpoint only” is incomplete evidence for that row. `Minimum: —` means enumerated but unaffected, so no new capture is required for this PR. Several rows may be labelled segments of one recording; no need for one file per row.

| ID / fixture and required data | Diff effect / expected visible state | Minimum capture (viewport × theme × row) / medium | Real-app seed or labelled harness | already captured in PR body: yes/no/stale | Status |
|---|---|---|---|---|---|
| F1 Selected missing image, multi-line checked path | Changed: not-found alert names absolute checkedPath, not reconnect advice; closes pending viewer | I × L,D × F1; **recording**, tap → alert → OK → same thread | Existing synthetic thread: missing `v2/extended/duo-color-3-extended-FINAL.png`, present `docs/in-project.png`; reuse actual project cwd; tap missing chip | yes — before/after endpoints only, both themes | Endpoint valid after merge; recordings missing |
| F2 Same selected missing image on Android | Changed copy through separate Compose Dialog; full path readable and OK works | A × L,D × F2; **recording** | Same thread/server/data as F1; Android app | no | Missing |
| F3 Short path / resource-path fallback | Changed: checkedPath absent → resource.path, e.g. `missing.png`; short alert body | I × L × F3; **recording** | Prefer real missing `workspace-file`/`draft-workspace-file` via file markdown; current server can omit checkedPath. If that route resolves another error first, use labelled native harness of actual session + Dialog with `AssetWorkspaceAssetNotFoundError({resource:{_tag:'media-file',threadId,path:'missing.png'}})`. Do not claim an ordinary missing media-file on current server omits checkedPath | no | Missing |
| F4 Very long unbroken/path text on constrained screen | Changed dynamic path length can fill/overflow alert; last path segment and dismissal remain reachable | IS × L at large accessibility text, A × D at 1.5 text scale × F4; **recording**, scroll if needed | Synthetic absent path near 1024-character resource limit (safe per-segment lengths) in thread chip; native harness if OS/host path limits prevent that error. Include mixed spaces/Unicode in a segment if feasible; no need for separate locale row | no | Missing |
| F5 Pathless missing tool-output image | Changed: neither checkedPath nor resource.path → `Tool output image was not found.` | I × L × F5; **recording** | Real persisted viewed-image/tool-output-image reference with missing stored image; if missing references cannot be tapped because inline UI blocks them, labelled native harness with actual tool-output-image resource and typed error | no | Missing |
| F6 No usable checked path but pathful resource (empty checkedPath boundary) | CheckedPath `''` wins nullish selection, then uses error.message; not resource.path | —; no additional capture beyond F5's short generic-message layout; logic boundary should be tested if relied on | Labelled harness only; current server normally supplies nonempty checkedPath or omits it | no | Enumerated; same visual layout as F5, no extra visual requirement |
| F7 Generic server failure / no connection | Guard: reconnect + moved/deleted advice retained; no checked path leaked; return to thread | I × L × F7; **recording**, exercise one server refusal and a disconnected attempt | On disposable environment revoke filesystem read or use missing attachment; disconnect after thread is loaded, then tap. Harness if global overlay prevents disconnected tap. Initial/no-longer-prepared connection and non-not-found errors collapse to same alert | no — before F1 has same old copy, but is a different trigger/revision | Missing current-head regression flow |
| F8 Missing remote non-image (PDF representative; document same resolver) | Changed: same checked-path alert before PDF/document viewer mounts; no native-open Save/share fallback | I × L × F8; **recording** | Add absent `missing.pdf` media chip in thread; document kind can use harness if needed. No need to capture both PDF and document for identical resolution-error rendering | no | Missing |
| F9 Three-image gallery, selected good, sibling missing | iOS: selected stays open, failed sibling slot; Android: closes and shows new checked-path alert | I × L, A × L × F9; **recording**, page through iOS gallery, close/OK | Thread message with three image embeds (A/C present, B absent); select A while B mint fails. If unavailable embed is filtered out, labelled harness passes all three actual sources as gallery | no | Missing, two platform paths |
| F10 Present remote image, selected missing/not selected variants covered above | Guard: successful mint still opens image, supports close and later re-open; fragment appended where present | I × L × F10; **recording**; Android success can share F9 control | Existing present `docs/in-project.png`; tap after dismissing F1, close, reopen. Avoid needing another paid agent turn | yes — successful iOS image endpoint only | Endpoint valid after merge; interaction proof missing |
| F11 Remote resolution failure with custom onOpenError | Original typed cause goes to caller; AttachmentFileScreen maps it to prior inline error, not default checked-path alert | I × L × F11; **recording** | Upload attachment, let URL resolve, then remove disposable captured bytes before explicit native open; otherwise labelled native harness retaining AttachmentFileScreen's callback/body and actual session. Zip URI callback shares visible error handling but bypasses remote change | no | Missing representative callback regression flow |
| V1 iOS remote video fails before native presentation | Guard: connected failure says check connection; disconnected says reconnect/open again; never file checked-path alert | I × L × V1; **recording** with both connection states | Thread has absent `.mp4` media chip; connection toggle for second attempt. Backend mint must fail before presentVideo | no | Missing |
| V2 iOS native video fails after presentation and reauthorization also rejects | Guard: playback failure guidance appears once; background remint rejection does not replace it or leave player stuck | I × L × V2; **recording** | Controlled native failure needed: labelled harness using actual VideoPreviewModal, failing presentVideo after ready, then failing mint; no reliable normal UI/RPC switch forces this two-stage failure | no | Missing; labelled harness acceptable |
| D1 Attachment fresh URL mint fails, then retry | Guard: fresh-URL catch yields `File unavailable` with reconnect body and Try again; restoration/retry recovers | I × L × D1; **recording** | Navigate real `ThreadAttachment` for disposable uploaded text/PDF attachment after deleting bytes, restore bytes, Try again. Use app-created attachment ID, name, mimeType, sizeBytes and environmentId; do not hand-edit live DB | no | Missing |
| D2 Attachment text URL is old/null-authorized and remint fails | Guard: reader's catch preserves reconnect error instead of leaking typed error; no stale body or indefinite loading | I × L × D2; **recording** | Labelled harness for readAttachmentText/attachment document with authorizedAt older than five minutes or null and controlled mint rejection. Real alternative: keep reader mounted past threshold and enter source mode after removing bytes; no sleeps needed in an automated harness | no | Missing, time-dependent branch |
| U1 Source null, explicit close, navigation/unmount during pending resolution | Unchanged: no view; abort hides late alert, holds/releases local leases; group frozen per presentation | —; **recording** if independently captured | Real navigate away during deferred request or labelled harness with controllable promise | no | Unaffected; no new capture required |
| U2 Direct URI / local attachment, good and bad; single image and gallery | Unchanged: URI bypasses refresh; local lease failure advises attach again; image zoom/page/close; Android actions header if actionsSource exists | —; **recording** if independently captured | Draft attachment strip, question attachment, review composer, generated Mermaid, direct URL in markdown, extracted ZIP; inaccessible local file for failure | no | Unaffected; no new capture required |
| U3 Successful PDF/document and native-open/share errors | Unchanged: iOS Quick Look; Android PDF modal or document-opening card; names/MIME/actions/sourceIdentifier; custom callback or default native-error buttons; Save/share success/error | —; **recording** if independently captured | Existing PDF/document on host or draft; unsupported file viewer; local and downloaded sharing. PDF/doc resolver failure itself is F8 | no | Unaffected; no new capture required |
| U4 Attachment reader success/loading/empty/truncated and render modes | Unchanged: initial loading; source/markdown/table/html/web/audio/native/unsupported body; no environment suppresses markdown; empty text versus no content; >1 MiB partial notice; sharing and retry controls | —; **recording** if independently captured | ThreadAttachment or ZIP entries with empty/large TXT, MD, CSV/TSV, HTML, audio; rendered toggle; local input bypasses remint | no | Unaffected; no new capture required |
| U5 Video source null, local/direct media, normal playback, audio ownership, Android states | Unchanged: local loading/failure; remote/direct player; Android loading/unavailable/actions; audio transfer/pause/dismiss | —; **recording** if independently captured | Local draft video, direct video URL, successful remote video; Android uses unchanged refresh hook | no | Unaffected; no new capture required |
| U6 Gallery all-success delay/count/order/name variants | Unchanged: iOS progressive slots; Android waits; selected first/middle; duplicate/nonimage filtering; pending URL updates must not reset page or zoom | —; **recording** if independently captured | Native harness with deferred URL promises or real media gallery; zero gallery items becomes selected singleton; 1 vs 3 images is the useful count boundary, no need arbitrary larger counts | no | Unaffected; F9 is required failure variant |

Minimum scope is the viewport/theme combinations explicitly listed in F1–F5, F7–F11, V1–V2 and D1–D2. F6 and U1–U6 add no independent capture requirement. Do not multiply every branch by both themes, every caller, every provider, a tablet, and every transport: those do not add new changed rendering paths. Use local/remote/relay URLs through the real prepared connection; this diff introduces no transport or provider branch, and web/desktop production code is untouched.

## Seeding and execution handoff

This audit did not seed or run the app. Reuse the lead's isolated backend and the existing synthetic project/thread. A real thread chip is the smallest representative default-alert entry point; a PDF and gallery add the relevant distinct resolution/presentation branches. Create extra messages with the free Replay provider or a synthetic projection fixture. For projection fixtures, stop the disposable server, inspect current columns, and use `apps/server/scripts/t3-sqlite-state.ts` with its isolated base directory, following the repository SQLite fixture reference. Never change live state or claim projection writes prove backend behavior.

App-native route identities are `ThreadFile` for workspace paths and `ThreadAttachment` for captured files; the latter takes attachmentId/name/mimeType/sizeBytes with environmentId/threadId and optional draftKey. Use actual app navigation rather than assuming a deep-link spelling. Settings appearance plus OS appearance supplies themes; device text settings supply stress scaling. Missing typed fallback/pathless/two-stage video cases may be labelled harness captures, but they must use the real platform dialog and native wrappers when making visual claims; mocked Dialog calls in the unit tests are not captures.

For each required recording show the setup/visible trigger, async transition, readable terminal state, and a working way out. Before/after sequences are needed for the changed alert branches; current-head regression sequences suffice for unchanged guard outcomes. Preserve useful existing PNGs and link new evidence beside its fixture IDs. Do not regenerate unaffected stills solely because the merge relocated files.

No tests were run in this read-only proof audit; existing tests were read to distinguish mocked assertions from real rendering. All nine PR attachments were downloaded and visually inspected. Worktree remained clean. Disk availability was 110 GB; nothing was started that requires teardown.

FRICTION: none
VERDICT: INSUFFICIENT F1–F5, F7–F11, V1–V2, D1–D2 lack required recordings; only F1/F10 terminal stills are covered
