# Evidence matrix — issue 16447 (final committed revision)

Scope: [#16447](https://git.cafe/pingdotgg/t3code/issues/16447), no PR. Worktree `/home/ubuntu/wt/issue-16447`, branch `fix/issue-16447`. Final AFTER: [366ec74cf8](https://git.cafe/pingdotgg/t3code/commit/366ec74cf818a8b0ccd032d115e2f6d353f61245). Comparison BEFORE: [3b95e7831b](https://git.cafe/pingdotgg/t3code/commit/3b95e7831b), the final merge-base. Fixed synthetic fixture is committed at the final SHA in `scripts/fixtures/issue-16447.py` and `.json`; the same sandbox projections and copied fixture files were used on BEFORE. No implementation edits were made during capture. The old base a12d6ed942 captures are historical repro only.

This document specifies planned coverage. Delivered coverage, timestamps, exact artifact URLs, limitations, and observed outcomes are in `/tmp/issue16447-evidence.md`; planned expectations below are not evidence or a complete pass claim. Literal Wide is primary evidence; Comfortable is a supplement. Logic tests are not proof of browser interactions.

## Change and render-path inventory

`TimelineMinimap.tsx`: active preview previously expanded the interactive rail to 22rem; now rail always uses measured `hitStripWidth`. Preview changes from pointer-events-auto/cursor-text/select-text to pointer-events-none; preview-target click/mousedown exemptions and preview mousemove stopPropagation removed. Preview appearance/content/placement, strip rendering, navigation, keyboard logic and selection callback otherwise unchanged. `MessagesTimeline.logic.ts` removes only the now-unused expanded-width helper. No other caller of that helper remains in current source search.

Path: ThreadRouteView (draft with session OR ready server thread/loading server shell) → ChatView's sole MessagesTimeline invocation → MessagesTimeline wrapper (find overrides/live follow) → ConversationTimeline → sole TimelineMinimap invocation. Shared web UI also runs in Electron; no Electron shell/IPC file changed, so browser proof covers this scope. Native mobile does not consume this component. Local, remote and tunnel receive the same projection and use local DOM hit testing; no separate transport/provider fixture warranted.

Data: deriveTimelineMinimapItems takes only user-message rows; rowIndex is the real list index; preview uses last assistant message before next user, ignoring work/meta/event rows. Markdown is flattened and capped to 1500 input characters, user text compacted, empty user uses “User message”, absent/empty assistant omits second block. Items <2 return null. >=2 share changed hit path; height min((n-1)*8px,100vh-18rem). Preview placement is first 0%, middle -50%, last -100%; fisheye bands distance 0/1/2/other, visible strips separate in-view overlay. Active null/out-of-range hides preview. Current null/out-of-range disables both neighbors; valid first/last disables one. All can be covered with two users plus a long thread; no Cartesian product needed.

Geometry: g=max(0,(viewport-min(viewport,content))/2); hit width h=clamp(floor(g)-12,0,40). Persistent iff g>=48. Navigation pointer interaction iff h>=14 and not passThrough. Thus named equivalence classes: h=0; 0<h<14; 14<=h<36 (nonpersistent); 36<=h<=40 (persistent). The persistent boundary at g=48 is not the navigation boundary g=26. Measure actual timeline viewport/content, not whole browser width.

## Smallest sufficient fixture matrix

Every interaction/time row requires a **recording**, not a still. Recordings may share one continuous file if named time ranges identify rows. All screenshots/recordings show the whole chat surface, sidebar and composer; extra crops only supplemental.

| ID / name | Seed and geometry | Affected? / exercise and expected proof | Capture |
|---|---|---|---|
| P — persistent long conversation | 1440x900, fine pointer, Comfortable; >=80 user turns with intervening assistant/work rows; long selectable paragraph and link near left content edge, long user title + multiline markdown assistant; g>=48, h>=36. Freeze scroll mid-thread. | YES. Idle → hover first/middle/last strips (all transforms, truncation and capped rail height); move from rail into preview's former 22rem footprint and underlying text/link: preview must not retain pointer ownership or cause a jump. Select underlying text across that area, click link, wheel there and on rail. Record browser selection and unchanged navigation until an intentional rail click. Before may demonstrate failure. | **Recording**, 1440 light AND dark; matching idle/active/released keyframes for pixel diffs. |
| D — drag and keyboard in same long seed | P, 1440 fine pointer; current first/middle/last by scrolling. | YES regression guard. Drag starts in message and crosses rail → passThrough; release outside window and return buttons=0 → recovery. Drag starts on rail then exits into preview footprint → no accidental selection/jump on release outside; rail still usable afterward. Tab focus → ArrowUp/Down, Home/End, Enter/Space jump; blur clears preview; previous/next including disabled endpoint controls; do not assume pointer transparency removes keyboard descendants. Explicitly exercise keyboard-open preview then pointer entry into underlying text. | **Recording**, combined with P in each theme; stills insufficient. |
| N — nonpersistent bounded hit area | Same long seed, Wide at 1440; use sidebar/panel sizing to set g=32, h=20, then g=20,h=8 (or measured equivalent). | YES. Hover/focus reveals hidden rail, pointer exit fades in 150ms rather than remaining caught by preview. At h=20 neighbor buttons pointer-active; h=8 pointer-inactive, rail works. Repeat text drag across former preview area and release/re-entry. Capture actual g/h; width setting alone proves nothing. | **Recording**, light at 1440; dark covered by P because theme does not branch hit logic. Keyframes before/after fade. |
| Z — Full / zero gutter | Same long seed, Full at 1440, measure h=0. | Unaffected control: rail pointer-inert even if keyboard focus can reveal preview; message text selection/link/wheel remain usable. Tab/arrow preview guard because button tabIndex is unchanged and CSS pointer-events does not remove keyboard focus. | **Recording**, can append to N; light sufficient, no changed theme branch. |
| C — minimum/absent counts | Reuse P thread truncated in sandbox: 2 users (one whitespace/attachment-only with no assistant, other short text + short final assistant), then 1, then 0. Use Comfortable 1440. | >=2 YES (8px rail, first/last preview, fallback and absent assistant); 0/1 unaffected no minimap. In 2-item state hover/click each, keyboard navigation and text drag beneath preview. 0/1 need only representative absence keyframes, not separate videos. | **Recording** for 2 items in light, with 0/1 keyframes appended. |
| M — narrow / coarse-pointer control | Same P seed at 390x844, light AND dark; state actual pointer emulation. Use mobile touch/coarse profile; additionally inspect fine-pointer 390 in same run if h>0. | Unaffected when coarse: media query hides minimap regardless of count. Fine-pointer may be rendered but h=0 when content fills viewport; verify measured h rather than claim all 390 hidden. If h>0 unexpectedly, run P interaction here too. No native iOS build required for web-only files. | Full-surface **stills** for 390 light/dark absence; recording only if interactive rail exists. |

Width decision: Comfortable (46rem) is the persistent/overlap proof; Wide (72rem) supplies constrained/nonpersistent paths; Full (100%) supplies zero hit width. Do not multiply every fixture across all three: classify by measured g/h. At 1440 total browser width Wide may already have zero gutter due to sidebar; collapse sidebar/panels or resize within app to reach the N ranges while keeping capture viewport 1440. If impossible, supplement with a labelled projection/geometry fixture retaining the real ConversationTimeline, CSS and list. Do not substitute an isolated minimap for P.

Loading/errors: no new loading/error minimap branch. Empty initial loading can return the timeline-loading placeholder; empty ordinary thread returns placeholder; errors/history loading are other rows, not minimap items. Existing loaded users keep the same hit path during history load/error, streaming assistant, worktree preparation and provider failure. Collapse these into P's mixed rows; capture one assistant update while preview active only if final diff adds temporal behavior. No distinct provider/account fixtures: item derivation is provider-neutral. Invalid current index and removed active item can be checked in the projection fixture by replacing data during focus (clear preview, disabled neighbors), but unchanged guards do not warrant separate captures for this tiny fix.

## Seed and reproduction route

Use isolated worktree `.t3` state only. With dev server stopped, `vp run migrate-dev-db` can copy recent stopped real threads read-only; choose a long thread containing user/assistant messages and selectable prose. Do not run agents or use live userdata read-write. A deterministic synthetic projection fixture is permitted here: seed a sandbox thread's actual client projection/timelineEntries with fixed ids/timestamps and realistic alternating user/assistant/work entries (shape examples in MessagesTimeline.browser.test.tsx). Render it through real ChatView/ConversationTimeline in the running app. This supplies long, 2/1/0, blank-user, no-assistant, fixed content and exact row counts without provider calls. Preserve app selection toolbar, LegendList and layout; a standalone fake preview cannot prove text selection through the overlay. Geometry-only impossible states may use supplemental fixture with explicit label. Link fixture data/seeding script in eventual PR; keep synthetic data free of credentials/customer text. No fixture or seed was created in this pass.

Set chat width through Settings → Appearance → Chat width; theme through normal appearance controls. Hold sidebar/panel state, scroll target, font loading, clock and content fixed. For D test with reduced motion off; optional reduced-motion repetition is not a separate required fixture because only existing scroll animation changes, not hit testing.

## Minimum capture set and evidence rules

1. P+D: two paired whole-surface recordings, 1440 light and dark; named segments and matched idle/first/middle/last/selection/focus keyframes.
2. N+Z+C: one paired 1440 light recording with named geometry/count segments, including 0/1 absence keyframes. Reuse P seed plus deterministic truncations.
3. M: two paired whole-surface stills, 390 light and dark; note coarse pointer and measured/fine-pointer behavior separately.

Minimum: **3 before/after recording pairs + 2 before/after still pairs**, with sufficient synchronized keyframes for each named segment. Interaction rows cannot be satisfied by still-only evidence. N cannot be dropped just because default Wide is h=0; supplement its unavailable geometry or explicitly mark unverified.

Before = requested base a12d6ed942 (verify actual main merge-base before eventual PR); after = final committed implementation SHA, never the same HEAD label for uncommitted edits. Recheck scope after commit. For every captured/keyframe pair provide Before | After | zero-threshold amplified diff | stated-threshold diff, SHA labels and viewport/theme. Record browser/tool, device emulation, DPR, font scaling (normal 100% unless declared), fixed viewport height and pointer type. Written “what else changed” must enumerate all visible differences, including intended narrower focus ring when preview open, preview disappearance on pointer exit, removal of preview text-selection cursor/selection, and any unintended changes. These are predictions from code, not observed results. Timing/selection outcomes require matching video segments; pixel diffs alone cannot prove them.

## FRICTION (delivered status)

- Implementation is committed at final SHA 366ec74cf8; comparison is current merge-base 3b95e7831b. Earlier uncommitted/base labels are superseded.
- The same isolated `.t3` synthetic projections were retained across serialized checkout/server switches; no live database writes or provider turns were requested.
- Literal Wide measured g=48/h=36. N20 and N8 use labelled CSS padding supplements, measured g=32/h=20 and g=20/h=8. Z0 means h=0: measured g=6 due to the stable scrollbar gutter, not literal zero gutter.
- Preview viewport is 1440x900 CSS pixels, DPR 2; videos are 2880x1800. Saved desktop snapshot PNGs are tool-downscaled to 1280x800. Narrow captures are 390x844 CSS, PNG 780x1688. Artifact pixel dimensions must not be confused with CSS viewport dimensions.
- Coarse-pointer emulation and release outside the window are unavailable in exposed tools. Narrow evidence uses a measured fine pointer. These requirements remain unverified.
- D endpoints, previous/next click recovery, exact 150ms fade timing, and cross-drag no-jump outcomes are incomplete; recordings show exercised actions but do not establish all assertions.
- Last optional setup failed because virtualized paragraph 41 was absent; no retry was made. Existing N/Z/C pair has differing scroll/content states and is geometry evidence, not a clean pixel invariance proof.
- Browser/server use was explicitly authorized. Captures are packaged using ghdrop. No PR, push, merge, implementation edit, or new commit was made.
