# Proof audit, pass 1 — fixture matrix

PR [#32424](https://git.cafe/pingdotgg/t3code/pull/32424), branch `fix/issue-32377`.

Before: main merge-base [80e62619f5](https://github.com/pingdotgg/t3code/commit/80e62619f5c01d84579bcf70ea6d566712007412). After: [db571ce260](https://github.com/pingdotgg/t3code/commit/db571ce260c43655551ab7b19109eb264fa88890). Both use react-native-keyboard-controller 1.23.0; the PR changes its patch. Read-only source audit in `/home/user/wt/pr-32424`; no app captures were produced in this pass.

## Findings first

1. **Blocking proof gap: none of the supplied captures proves this revision.** The body identifies before [257e4073c1](https://github.com/pingdotgg/t3code/commit/257e4073c1) and after [79b7925bc4](https://github.com/pingdotgg/t3code/commit/79b7925bc4), with C2 replaced at [4395a6ac](https://github.com/pingdotgg/t3code/commit/4395a6ac). They precede the library upgrade and native fingerprint change. All affected device rows below need fresh evidence at the stipulated before/after commits. Old C5/C6 are after-only; there are no corresponding before recordings. Repeated focus, already-visible input, header-overlap input away from the end, sending, reference entry, and the new frozen-transition sequence are absent from the supplied coverage.

2. **Header-overlap behavior is not established by the unit test and may not be wired into the real app.** [The test passes `adjustedStartInsetCompensation: 80`](https://github.com/pingdotgg/t3code/blob/db571ce260c43655551ab7b19109eb264fa88890/apps/mobile/src/features/keyboard/chatKeyboardFeedInput.test.tsx#L75). [The real list passes `contentInset.top`, but no compensation prop](https://github.com/pingdotgg/t3code/blob/db571ce260c43655551ab7b19109eb264fa88890/apps/mobile/src/features/threads/ThreadFeed.tsx#L3658); the installed chat-scroll-view defaults compensation to zero. LegendList forwards the scroll props without deriving compensation from `contentInset.top`. [The new delta calculation uses compensation as `visibleTop`](https://github.com/pingdotgg/t3code/blob/db571ce260c43655551ab7b19109eb264fa88890/patches/react-native-keyboard-controller@1.23.0.patch#L1447), not the static inset. For an input top 60 pt below a full-screen viewport origin but under an 80 pt header, the test requests a −20 pt shift while production options request zero (assuming the bottom already fits). **F3− and F6 must measure actual viewport/header coordinates and demonstrate header clearance; code inspection alone cannot settle the native coordinate origin.** This is a confirmed test/production-parameter mismatch and a suspected visible failure, not a claimed on-device reproduction. If reproduced, feed the actual uncovered top into the calculation and verify the same production geometry.

3. **The old unfolded comparison has a confounded focus history.** Its before composer was focused earlier; the after composer was not. The body acknowledges this and the old C2 stills show different composer positions. Replacement F1/unfolded must start from identical fresh focus history. A screenshot with the right card position but different composer state is not a controlled pair.

4. **The frozen row needs a branch witness.** The app freezes during `useKeyboardScrollToEnd`; the next unfrozen transition clears `inputShift`/`lifted` after an intervening frozen transition. An ordinary keyboard dismissal after Send is not proof of this overlap: Send explicitly awaits dismissal before starting end scrolling. F8 needs a trace showing `onStart` while frozen, followed by unfreeze and another transition. There is no persistent public UI switch or server RPC for this client shared value. The focused unit test proves the hook sequence with stubs, but not that a captured UI interaction entered it.

Focused verification run: `vp test run apps/mobile/src/features/keyboard/chatKeyboardFeedInput.test.tsx apps/mobile/src/features/keyboard/keyboardControllerState.test.tsx` — **2 files, 14 tests passed** (12 hook cases, 2 keyboard-state cases). Tests stub native measurement, focus metadata, Reanimated, and scroll application; they do not replace native proof.

## Minimum fixture matrix

All required media are **recordings** because the changed behavior includes motion, focus transitions, and restoring the prior offset. Extract rest/open/end stills from those recordings for side-by-side review; those are not additional independent fixtures. `A` means affected by the changed iOS path, including a regression control whose branch shares changed bookkeeping. `MISSING` means no evidence at the requested revisions, even where an old capture exists.

Devices: **DF** = folded Duo 466×678 pt; **DU** = unfolded Duo 951×669 pt; **P** = iPhone 17 Pro 402×874 pt. Light theme, default text size, identical software-keyboard language/settings, same simulator OS, same content and focus history on both sides. Fix device scale/status bar where available. No dark-theme duplication: the diff changes no color. Dimensions are full device bounds; record the actual feed viewport and native header boundary separately.

| ID / named fixture | Branch, data, and initial state | Required recording and observable acceptance | Minimum device × theme × row; before/after | Current status / prior row |
|---|---|---|---|---|
| **F1 — end-anchored secure entry, three layouts** | **A**. Short local thread with a pending Value secret card; fixed label `Staging deploy token`, reason `Needed to run the staging deploy script`; no secret-provider chips. Feed at its real rest/end, composer never focused in this fresh launch. Enough surrounding content to identify heading, field, composer, and a rest landmark. Run S-short below. | Rest → tap secure field → type dummy `abc` → dismiss through the app's normal control → reopen → dismiss. Field stays above keyboard/composer and below header, movement is only the required amount, and each dismissal returns to the initial landmark without cumulative drift. Record heading and lower controls too; distinguish focused-field visibility from whole-card visibility. On P, accept no change when native geometry already fits; do not infer the zero-delta branch merely because the final frame matches. | **DF/light/F1, DU/light/F1, P/light/F1**. **3 paired recordings** at before/after. | **MISSING / STALE C1, C2, C3**. C1 had a cycle; C2/C3 only keyboard-up stills. C2 pair also has mismatched focus history. |
| **F2 — composer alone at end** | **A regression control**. Same S-short data and rest position, fresh focus state, external composer target (parent scroll view differs from feed); no prior feed-input shift. | Rest → focus composer → type a dummy draft → dismiss. Normal full/absorbed lift as appropriate to existing blank end space; restored rest position, no feed-specific correction. Compare both sides, including native composer position. | **DF/light/F2**. **1 pair**. | **MISSING / STALE C4** (old keyboard-up still only). |
| **F3+ — end focus round trip after upward feed shift** | **A**. S-short or S-positioned so feed field is below its future uncovered keyboard strip; rest classified at end. Positive recorded feed shift. | Feed field → composer while keyboard remains up → feed field again → composer → dismiss. Composer receives its normal full/absorbed lift based on the original rest/end, not the displaced feed position. Returning to feed reveals only the field as needed; final rest has no drift. This covers `prevLifted=true` when returning from a lifted composer. | **DF/light/F3+**. **1 pair**. | **MISSING / STALE after-only C5**, which lacked the full round trip. |
| **F3− — end focus round trip from header overlap** | **A; header geometry unresolved**. S-positioned: add fixed trailing message content so that **at end** the field's top is slightly behind the header, with its lower edge still tappable. Keep a fixed bottom landmark. The intended correction is signed negative/downward, unlike F3+. | Tap exposed field edge → composer → feed → dismiss. Field clears the measured native header; composer still lifts from the recovered rest/end even after a downward correction. Final rest returns to its original landmark. Record actual field top, viewport origin, and header bottom; a zero shift caused by missing header compensation fails this fixture. | **DF/light/F3−**. **1 pair**. | **MISSING / PARAMETER MISMATCH**. Unit test covers a nonzero compensation that the app does not pass. Not in C1–C6. |
| **F4 — reading position, lower input, unlifted composer** | **A**. S-long: fixed numbered lines before and enough content after the card to be genuinely away from the end (>20 pt from hook's end threshold). Field is initially tappable but entirely below the *future* keyboard/composer strip. No pre-existing keyboard lift. | From recorded reading landmark focus composer (feed must stay put) → feed input → composer → feed input → dismiss. Positive feed correction occurs even though `whenAtEnd` suppressed composer lift; each focus change unwinds only the actual feed shift. Final reading landmark is restored. | **DF/light/F4**. **1 pair**. | **MISSING / STALE after-only C6** covered lower/partly obscured field and dismiss, not the full unlifted-composer sequence. |
| **F5 — reading position, already-visible input** | **A**. S-long; away from end, field fully within the strip between native header and future keyboard/composer. This must be proved from pre-open geometry, not a post-open screenshot. | Rest → feed input → dismiss → repeat once. **Zero** feed shift; neither a spurious full lift nor a later unwind of nonexistent movement. Same reading landmark throughout. | **DF/light/F5**. **1 pair**. | **MISSING**, expressly not captured in body. P's end-anchored card fitting is not evidence for an away-from-end zero shift. |
| **F6 — reading position, input partly behind header** | **A; header geometry unresolved**. S-long; away from end, scroll until the field top is under the native header but its lower portion remains tappable. This differs from F3− in end detection and the no-lift fallback on dismiss. | Focus exposed lower edge → type → dismiss. Intended signed negative shift reveals the top below the header, then restores the original reading landmark. Capture header/viewport coordinates as in F3−. If starting field is wholly above the header, a tap cannot focus it; that is not a reproducible direct-entry fixture. | **DF/light/F6**. **1 pair**. | **MISSING / PARAMETER MISMATCH**; not old C6 (which was a lower field). |
| **F7 — send while feed field owns the keyboard** | **A**. S-send: thread has a prior user message, pending feed card, no approval dialog, and a prewritten composer draft. Establish draft first, dismiss to reset, then focus the feed field so `inputShift` is live. App switches `whenAtEnd` → `persistent` only during an iOS send with visible keyboard. Use free Replay for any resulting turn. | With feed field focused and shifted, tap composer Send **without first focusing composer**. Record keyboard dismissal and post-send anchor/end scrolling, then focus composer and dismiss. Need evidence that the real send reaches `persistent` while prior feed shift still exists; no jump from treating that signed shift as a full keyboard lift. Normal composer send can be included in the same recording as a control. | **DF/light/F7**. **1 pair**. | **MISSING**, expressly not captured. A static hook test with `persistent` from mount does not exercise the app's mode switch. |
| **F8 — frozen transition, then next keyboard transition** | **A; new in port**. S-long plus feed shift already applied. Must cause `onStart` during the client's end-scroll freeze, unfreeze, then open composer while away from end. Record current reading/scroll position after freeze. | Feed focus → end-scroll/freeze and keyboard transition overlap → unfreeze → composer transition → dismiss. No stale feed shift is undone after unfreeze; preserve the current rest except the lift warranted by its current end state. Include a synchronized trace of freeze, keyboard start, recorded shift reset, and resulting offset. | **DF/light/F8**. **1 pair plus branch witness**. **Cannot deterministically seed this overlap through current public app/server APIs.** A scoped capture driver may be needed; keep any such instrumentation identical on both sides and disclose it. | **MISSING / NOT DETERMINISTIC VIA PUBLIC UI**. New unit sequence passes; old captures do not cover it. Do not label a normal send/dismiss as this row. |
| **F9 — reference-entry field variant** | **A**. S-reference: same pending card plus isolated server configuration with a dummy known secret provider; select its reference chip. Plain AppTextInput replaces secure TextInput, has a different native target/keyboard and extra chip-row height. | Value field → select reference source → type dummy `op://vault/item/field` (do not submit) → composer → dismiss. Field remains in view, focus metadata follows the replacement, and rest restores. No actual secret resolution is needed. | **DF/light/F9**. **1 pair**. | **MISSING**, expressly not captured. |

**Minimum set:** 10 named fixtures above, **12 device × light × fixture combinations**, each paired at before/after = **24 recordings**, with stills extracted from them. F8 is included in that requirement but is not presently a deterministic public-UI recipe. No multiplication of every interaction across all three devices: F1 establishes the three distinct native layout geometries; DF supplies the other branch witnesses. F3−/F6 are separate because at-end bookkeeping changes composer lifting and unwind. Do not substitute a later main for the stipulated merge-base. Each side requires the dev client for **its own native fingerprint**, not the old 1.22.8 client with a new Metro bundle.

## Branch inventory and what was collapsed

[The diff's iOS hook](https://github.com/pingdotgg/t3code/blob/db571ce260c43655551ab7b19109eb264fa88890/patches/react-native-keyboard-controller@1.23.0.patch#L1260) adds focused-input metadata, list-target capture at layout, signed `inputShift`, and `lifted`; it resets stale state after a frozen transition, judges end from the rest position, skips persistent/never shrinking fast paths when unwinding a feed shift, and substitutes that shift for inferred keyboard lift. The added geometry helper is also copied into module/commonjs and declared in library types; Metro uses source. The Android hook is unchanged by this PR.

| Branch / surface | Matrix coverage / decision | Status |
|---|---|---|
| Same-list feed input, matching event target, measured non-inverted viewport, keyboard opening | F1, F3±, F4–F6, F9. Native proof must establish parent-scroll-view target matching; tests stub it. | **MISSING native evidence** |
| External composer, no feed shift vs prior signed shift; `prevLifted` true vs suppressed lift | F2 vs F3± vs F4. These cannot be represented by one still of composer focus. | **MISSING** |
| Delta positive / zero / negative; target clamped at content min/max | F1/F4, F5, F3−/F6. Short end content and long reading content cover the useful bound differences; record resulting bounds, do not invent oversize-input fixtures (current fields are single-line). | **MISSING; header parameter mismatch** |
| End vs reading; short blank end space absorbed lift vs long content | F1/F2/F3± vs F4–F6. Preserve actual anchor/blank-space state; do not force `blankSpace=0` as the hook tests do. | **MISSING** |
| `whenAtEnd` and `persistent` | [Real app props](https://github.com/pingdotgg/t3code/blob/db571ce260c43655551ab7b19109eb264fa88890/apps/mobile/src/features/threads/ThreadFeed.tsx#L3695) select only these two. F1–F6/F9 normal; F7 app mode switch. | **MISSING** |
| `always`, `never`, inverted list | Not used by this mobile feed; avoid three more native fixture families. Four-mode hook tests provide library-level supplementary coverage. Inverted path bypasses new feed logic. | **EXCLUDED: no real app render path** |
| Keyboard hide (`e.height=0`), repeated focus, keyboard target/height changes on secure ↔ composer | Covered in required recordings, particularly F1/F3/F4/F9; signed shift must restore exactly, within native pixel rounding. | **MISSING** |
| Frozen onStart; onMove/onEnd return while frozen; next unfrozen onStart clears recorded shift | F8 + focused hook test. onEnd settling of currentHeight/padding already exists on base; don't claim this PR invented it. | **MISSING runtime witness; unit pass** |
| Null/mismatched focus, null list target or failed measurement | F2 exercises normal external-focus fallback. Transient missing measurement/tag has no distinct controllable visual state; source fallback inspection, no extra simulated native row required. | **SOURCE ONLY; intentionally no extra row** |
| Interactive drag-to-dismiss | App sets `keyboardDismissMode="none"`; the hook's interactive marker path is pre-existing. Normal dismiss controls are covered above. No requirement to force an unavailable gesture. | **EXCLUDED: unavailable gesture here** |
| Structured-question card and link sheet | Question form is outside the feed and positioned from keyboard frame; LinkRequestCard's text fields live in its sheet's own ScrollView. No new same-feed-input branch. F2 covers parent-target fallback; no additional product-flow families. | **UNAFFECTED; no minimum pair** |
| Pending Value vs reference vs answered/inherited secret card | F1/F9 cover the two actual input render variants. Answered and inherited cards have no field. Server transport/provider identity does not alter this local native geometry. | **Input variants MISSING; no-input variants excluded** |
| Feed content resizing while field focused | Different hook (`useExtraContentPadding`) / end-follow can still move content; documented pre-existing limitation in body. Keep fixture content fixed. Not evidence for or against this hook transition fix; no scope expansion. | **KNOWN LIMIT; optional exploratory row** |
| Android | PR changes no Android useChatKeyboard/index.ts hunks or Android native code relative to base; helper addition is unused by that path. No Android minimum capture or pair for this iOS-only change. | **UNAFFECTED by source audit; not device-tested** |
| Dark theme, enlarged text, iPad, physical Duo, remote/relay | No color, contract, network, or provider protocol change. Light/default and the three named simulators are the minimum here. No new matrix axes for unrelated validation. | **OUTSIDE minimum scope** |

The helper guarantees visibility of the input within its computed strip, **not the entire heading/card**: a long label/reason may put the heading above the strip, and lower privacy/Decline controls may need scrolling. Record them, but do not silently change acceptance from “focused field visible” to “all card content visible at once.” The old C1 after still already shows lower card controls partly behind composer/status UI.

## Deterministic real-app seeding

**Prefer disposable projection fixtures for geometry.** This is a native scroll/focus proof, not a test of MCP secret storage. The repository [SQLite fixture runbook](https://github.com/pingdotgg/t3code/blob/db571ce260c43655551ab7b19109eb264fa88890/.agents/skills/test-t3-app/references/sqlite-fixtures.md) explicitly permits direct projection fixtures for ephemeral visual states, with server stopped, schema-valid payloads and untouched event history. No seeded fixture was executed during this read-only audit; this is a source-derived capture recipe and needs a rendering smoke check before the pair run.

1. Each revision gets its own fresh isolated base directory (`T3CODE_DEV_SEED=0`), migrations initialized once, and a scratch git project. Never point it at the live T3 home. Use the normal CLI/app to create the project and thread. If useful, run one **free** Replay `multi-turn`/`showcase` turn to create realistic completed run/message rows and provider configuration, then stop the server. Replay produces real app/orchestration data without a paid agent.
2. Inspect current `PRAGMA table_info`, retain the server-generated thread/run/node IDs and fixed message payload shapes. Through `apps/server/scripts/t3-sqlite-state.ts exec --base-dir <isolated-base> --file <fixture.sql>`, insert a local `secret_request` turn item into `orchestration_v2_projection_turn_items`: unique id and ordinal, matching thread/run/node IDs, `status="waiting"`, `secretStatus="pending"`, fixed label/reason/placeholder, fixed ISO timestamps, nullable provider/native refs as required by the contract. Keep denormalized SQL columns consistent with `payload_json`. Set assistant filler as non-streaming messages plus matching assistant-message items, not merely unreferenced text rows. Preserve full schema shapes from real rows.
3. For stable visual-only capture, associate the pending card with an already **completed** replay run, leaving its node/run terminal. The form renderer checks local visibility and pending secret status rather than requiring an active run. Startup recovery cancels secret cards attached to a recovered live run; do **not** seed `running`/`waiting` runs and expect their pending card to survive restart. This deliberate projection-only visual state does not prove backend liveness, saving, or MCP behavior; do not submit the dummy value. The absence of a live Working timer reduces capture noise. If live-run chrome is needed for a distinct layout, use the actual request path below and disclose the difference.
4. **S-short**: fixed initial user/assistant text plus one pending card, no changing row heights. **S-long**: fixed 30 numbered lines before the card and fixed trailing lines, enough to keep the chosen card position >20 pt away from end. **S-positioned**: vary only fixed trailing filler until, at rest/end, the input straddles the measured header for F3−. Use identical content on before/after, not different filler lengths to manufacture agreement. **S-send**: same thread plus a prior real user message and a dummy composer draft; keep Replay selected for the new turn. **S-reference**: configure an isolated known provider, e.g. `one_password`, label `Fixture vault`, config `{ "account": "fixture" }`; server advertises `secretRequestReferences`. Choose its chip. No 1Password credential or actual lookup is necessary when merely typing and dismissing.
5. Scroll/tap through normal device automation to reach each row. Native focus, keyboard, header/viewport measurement, end detection, and freeze are **client state**: a SQL insert/server RPC cannot directly set them. Record visible landmarks and coordinates before and after. A wholly hidden field has no tappable surface: for direct tap rows position it just partly exposed; for “fully keyboard-covered” rows place it below the keyboard's *future* top while it is still tappable with keyboard down. Never count an unfocusable, wholly header-hidden initial field as a successful fixture setup.

**Why not stock Replay alone?** [Replay scenarios](https://github.com/pingdotgg/t3code/blob/db571ce260c43655551ab7b19109eb264fa88890/apps/server/scripts/dev-replay-agent.ts#L58) are showcase, plan/reads, approval, question, multi-turn; its replayable requests are `session/request_permission` and `elicitation/create`. None creates a `secret_request` card. A question card is not a substitute. Replay plus a visual projection fixture is viable; a new stock secret scenario would require implementation outside this read-only pass.

**Why not a public server dispatch?** [The contract makes `secret_request.record` internal](https://github.com/pingdotgg/t3code/blob/db571ce260c43655551ab7b19109eb264fa88890/packages/contracts/src/orchestrationV2.ts#L3559) and excludes it from `orchestration.dispatchCommand`. There is an answer RPC but no public create-secret-card RPC. The real [MCP requestSecret service](https://github.com/pingdotgg/t3code/blob/db571ce260c43655551ab7b19109eb264fa88890/apps/server/src/mcp/OrchestratorMcpService.ts#L2130) additionally requires the caller's active run and owning provider session. Do not recommend calling the internal command over public WebSocket.

**Actual live-path fallback:** the prior method (an isolated real agent turn instructed to call `request_secret` with fixed label/reason, then wait) does seed the genuine waiting/Working layout and can cover keyboard interactions. It costs a paid turn and yields changing wording/timers unless carefully controlled; use a cheap supported provider only if the projection fixture fails to render or live-run chrome is material. It still cannot force a frozen-transition overlap. Run the same instructions and focus sequence on both revisions and disclose dynamic pixels; no real secrets in captures.

**F8 cannot currently be guaranteed through a seed.** App `handleScrollToEnd` invokes the LegendList helper, which sets freeze true around an end-scroll promise. Arrange an actual keyboard transition in that interval and trace it. If a capture driver cannot hold/witness the interval, report this row unproved rather than inferring it from a finished scroll. A temporary test driver would need to operate the actual mounted shared value and retain real native keyboard/scroll behavior; a standalone mocked hook or fabricated video is not a real-app recapture.

## Coverage and friction

Covered: full four-file diff against supplied merge-base; installed patched source and LegendList forwarding; mobile input render paths, layout props, send/dismiss/freeze callers; existing hook tests; stock Replay capabilities, public RPC exclusion, MCP ownership requirement, fixture runbook and recovery behavior. Inspected old C1–C4 keyboard stills and sampled composited C5/C6 frames for coverage context; this was **not** a new all-frame audit. No device, lease, server, or emulator was started. No repository edits, pushes, or PR comments. Completed within the 45-minute budget.

FRICTION: tool | optional image contact-sheet helper lacked Pillow; used installed ImageMagick instead | use installed ImageMagick for capture inspection.

AI-assisted; verified with the tests and tools listed above.

VERDICT: INSUFFICIENT F1 (DF/DU/P), F2, F3+, F3−, F4, F5, F6, F7, F8, F9 need current-revision paired recordings; F3−/F6 have a header-parameter mismatch and F8 lacks a witnessed frozen transition
