# Paste normalization proof matrix — pass 1

Issue: [#27080](https://git.cafe/pingdotgg/t3code/issues/27080). No PR exists yet.

Read-only source audit of `fix/issue-27080`, head [7ce42465](https://git.cafe/pingdotgg/t3code/commit/7ce42465ebc3e253d8347544e914dca49f479e8e), against supplied merge-base [dd148481](https://git.cafe/pingdotgg/t3code/commit/dd148481531a0a090780a2ade20588566db099a2). No browser, server, test runner, edit, push, or forge comment was used for this audit. The lead's earlier reproductions are context, not audited captures.

## Findings

1. **Visual proof is pending.** No capture has been supplied for any required row. The added browser test checks paragraph text and serialization for CRLF and standalone CR in a focused, rich editor; it does not establish paragraph height, rendered blank-line visibility, caret visibility, Up/Down behavior, focus redirect, or the existing-code-block branch. Its reported red result comes from the brief; this auditor did not rerun it.
2. **The diff covers both real paste entry paths.** Focused paste reads normalized text before the code-block / ordinary-document split. Ordinary paste with an importer and blurred paste-to-focus both reach the changed `importPastedComposerText`. The latter is reached through `usePasteToFocusComposer → insertTextAtEnd → insertComposerText`. The short fixtures below must avoid automatic text-attachment folding.
3. **Three visual fixtures are sufficient for the changed behavior, once captured.** Rich and plain ordinary paragraphs share the same `x / empty paragraph / y` shape; assign rich to R1 and plain to R2 to exercise both modes without a cross-product. Code-block insertion is a separate render/insertion shape and is R3. The other callers/branches below either map to these same shapes or bypass the changed code. There is no new theme-, viewport-, provider-, platform-, count-, loading-, or error-dependent presentation in the diff.

## Required fixture matrix

All rows are **recording**: they involve paste, focus, keyboard movement, or a blinking caret. A still of the final text cannot prove the interaction. Capture the same viewport, theme, fonts, zoom, and input on baseline and head. Label baseline and fixed recordings explicitly.

Minimum capture set: **1280 × 900 CSS px × one fixed dark theme × R1–R3 × baseline/head = six recording segments**. They may be assembled into one labelled before video and one labelled after video, with row markers. No light/narrow/Electron duplicate is required for this text-normalization change. Record the actual theme identifier and browser/OS in the evidence manifest. Use native keyboard automation for Up/Down, not synthetic keydown-only events.

| Row / status | Diff affected / named fixture | Data and state | Interaction and visible acceptance | Real-app seed / route | Capture |
| --- | --- | --- | --- | --- | --- |
| **R1 — NOT CAPTURED** | **Yes. Focused rich paragraphs; CRLF vs LF control.** Covers the focused reader plus the importer-present ordinary-paragraph path. | Existing idle thread; empty enabled composer; rich text ON; no files, context records, pending questions, or open overlays. Clipboard `text/plain` is exactly `x\r\n\r\ny`. Repeat after resetting with `x\n\ny` (control) and `x\r\ry` (standalone-CR boundary). | Focus composer, paste, show all three lines. From the end of `y`, press Up onto the empty middle line, Up onto `x`, then Down through the middle line to `y`. Pause on the middle line long enough to show a visible caret blink. On head all three newline encodings must have the same blank-line height and navigable middle line. LF must already work on baseline. Inspect the document: paragraph text `['x', '', 'y']`, no CR text nodes. A short select-all/re-paste reset is enough; no send is needed. | Navigate to `/<environmentId>/<threadId>` for an idle thread in the lead's isolated dev environment. Set **Settings → Chat → Rich text composer ON**, return to thread. Use the real app's editor `[data-testid="composer-editor"]`; clear with normal select-all/delete. Paste with the event recipe below. | **recording**, baseline + head, 1280×900, dark. Required. |
| **R2 — NOT CAPTURED** | **Yes. Blurred plain paragraphs; redirect appends and restores focus.** Covers the second changed call site in `importPastedComposerText`. | Same thread; rich text OFF. Start empty for the exact CRLF reproduction. Also run a short second segment with existing text `prefix\n` and the same CRLF clipboard, showing that redirect appends at the end. Clipboard has no files or custom fragment. | Click a noninteractive timeline/background area so the editor is blurred. Dispatch a bubbling, cancelable paste there. Show focus returning to the composer and `x`, a full blank line, and `y`; exercise Up/Down through the empty line. Repeat an empty reset with LF as a baseline control. In the append segment preserve `prefix` and verify insertion occurs at the end. Document must contain no CR. | `/<environmentId>/<threadId>`; **Settings → Chat → Rich text composer OFF**. Keep terminal, command palette, model picker, modal/popover, and interactive controls unfocused/closed. Use a confirmed noninteractive timeline/background target, not a textbox or button. Three-line text is below text-folding thresholds. | **recording**, baseline + head, 1280×900, dark. Required. |
| **R3 — NOT CAPTURED** | **Yes. Focused existing code fence; verbatim insertion after newline normalization.** Covers `isInCodeBlock → tr.insertText`, which bypasses paragraph parsing and context import. | Rich text ON. Create a fenced code block through the real composer: type three backticks and Enter, leaving the caret inside the block. Paste exactly `x\r\n\r\ny` there. Optional same-clip trailing segment: paste a short Markdown-looking string such as `**literal**\r\n$HOME` inside the same block to show it remains literal. | Show the code-block frame and separate lines, including a visible middle-line caret while moving Up/Down. Read the editor document after paste: the code-block text must be `x\n\ny`, with no `\r`; it must remain one code block, with no parsed chip/paragraph insertion inside it. **Baseline may look the same inside a code block**; show/cross-link an escaped document-text transcript for baseline/head so the raw-CR defect is actually discriminated. Do not claim a visual before/after difference that is absent. | Same thread route; turn rich text ON. Use the existing fence-on-Enter input rule to create the code block and put the real selection inside its content before dispatching paste. No provider turn, server mutation, or direct DB write is needed. | **recording**, baseline + head, 1280×900, dark, plus escaped document transcript. Required. |

## Fixture driver contract

Use a driver through the running real app, not a component harness, for R1–R3. An existing seeded idle thread is enough; no new fixture RPC is necessary. Use only isolated dev state. Do not start any server against live T3 userdata. Do not send the prompt to a provider.

For exact bytes, use `new DataTransfer()`, `clipboardData.setData('text/plain', 'x\r\n\r\ny')`, then dispatch `new ClipboardEvent('paste', { clipboardData, bubbles: true, cancelable: true })` on the focused editor (R1/R3) or noninteractive timeline/background (R2). Browser/OS clipboard services can normalize newlines before the app sees them, so an ordinary native paste without a byte check is not enough to prove this input. These are synthetic **clipboard inputs in the real app**, and the manifest should say so. The user interaction and keyboard navigation still run against the integrated client.

Use actual keyboard actions for focus/selection and Up/Down. Show the unmodified native caret; do not draw an overlay caret or alter CSS to make a missing caret appear. Include a brief visible key trace or row narration in the recording. A cursor-model value alone cannot establish caret visibility. If the recorder omits native caret pixels, fix the recording method; a DOM rectangle is supporting evidence only.

Read-only supporting inspection may use the Tiptap instance exposed on the editor element (`element.editor`) and its `state.doc.toJSON()` / text nodes. Record escaped text, node types, paragraph bounding heights, and current selection after each milestone. For R1/R2, the empty paragraph should be a real empty paragraph with positive layout height, not a paragraph containing CR. For R3, inspect the code block's own text, not `doc.textContent` alone, which loses block boundaries. Do not expose unrelated seeded thread contents or credentials in an uploaded transcript.

Store each driver run in a new timestamped directory below the capturer's own assigned scratch subdirectory. Keep the raw recordings/logs until uploaded.

## Caller and render-branch inventory / equivalence reduction

Source scope: changed editor `apps/web/src/components/ComposerPromptEditorTiptap.tsx`, changed text-import helper `apps/web/src/components/composerInlineTokenPaste.ts`, and the added input browser test. The helper does not render anything. No CSS, code-block node view, contract, transport, native mobile component, or provider adapter is changed.

| Caller / branch / shape | Layout or interaction significance | Coverage status / capture decision |
| --- | --- | --- |
| Production caller **ChatComposer**, rendered by **ChatView** | Shared editor for existing thread, new/draft thread, edit/queued draft, and pending custom answer; `draftKey` remounts on target/mode change. Value/cursor/selection determine insertion location. | **Inventoried; R1/R2 representative.** Idle existing thread avoids unrelated orchestration. Focus redirect requires an active thread, so use an existing thread for R2. No additional new/draft/edit recording: same editor, short text, and paste handler. |
| Production caller **PromptFontPreview**, used by **TypographySettings** in both simple interface-font and advanced composer-font rows | `/settings/appearance`; editable preview, plain mode by default, no importer, no context records, seeded sample text, `max-h-42` instead of normal `max-h-52`. Font family and size inherit typography settings. | **Inventoried; equivalent to R2's plain paragraph shape and R1's direct reader.** No independent mandatory capture for three short lines. If a caller smoke is desired, select-all and replace in the real **Prompt font preview** textbox with CRLF; this is a real-app capture, not a harness. |
| Test-only callers: input and citation-draft browser tests | Controlled values/cursors, rich/plain props, skills/context maps, mock callbacks. The new case sets focus programmatically and asserts paragraph content + serialization. | **Not integrated visual evidence.** No test rerun or harness capture claimed by this audit. |
| Rich text ON vs OFF / `literalText=true` | Rich uses Markdown blocks/styling, plain still supports chips, literal builds paragraphs directly and skips chip/import processing. Literal mode is selected for an active pending question with `initialAnswer !== undefined`. | **R1 rich, R2 plain. Literal ordinary paragraphs are visually equivalent for this exact `x / blank / y` input**, and use the same normalized focused read before splitting. No separate required capture. For a literal-specific Markdown/token regression exercise, seed a real pending editable question with `initialAnswer` through an existing authorized replay fixture if available; this audit did not establish a replay driver. Otherwise explicitly label it **component harness**, with `literalText=true`, enabled editor, and no-op importer. Never present that as a real-app capture. |
| Focused `handlePaste`: missing clipboard / files present / empty text | Returns false before custom insertion. The parent capture handler may ingest folders/files or fold a large paste into an attachment. | **Bypassed/unaffected; no new capture.** R1–R3 use nonempty, file-free, very short text. The diff does not normalize attachment content. |
| Focused inside code fence vs ordinary text | Code fence inserts normalized text verbatim; ordinary text imports/rewrites context, tokenizes, then parses/splits blocks. | **R3 vs R1.** This is the one visually distinct insertion branch requiring its own row. |
| `importContextFragment` absent / literal / fragment absent / records empty / records present; ID map empty or nonempty | Present ordinary importer can rewrite context IDs before parsing; custom MIME is tried before HTML fallback. Citation/attachment/preview chips depend on record kinds and maps. | **Inventoried; R1 and R2 traverse importer with no fragment; preview/literal skip it.** Fragment decode/filter/map logic is unchanged, and normalization precedes its existing ID substitution. No extra chip capture required for the newline bug. If checking separately, copy one existing context chip through the app into a plain-text clip with CRLF around it; label that an optional context regression, not a missing core row. |
| Empty document / nonempty / selection replacement / paste at end | Empty text + empty context + nonempty placeholder renders the placeholder; paste removes it. Selection determines replacement; redirect always appends. A single nonempty paragraph inserts inline; multiline content inserts blocks; a lone empty inline result is a no-op. | **R1 starts empty and can use select-all reset; R2 includes append.** Single-line/empty no-op is unchanged; newline-only inputs collapse to the same normalized empty-paragraph shape. No separate capture. |
| Rich nested listItem/taskItem/blockquote vs root; headings/rules/fenced Markdown in pasted text | Nested ancestry disables new block syntax; root rich text can create block types. Pasted rules can leave a node selection and need a trailing paragraph. | **Inventoried; ordinary parser unchanged.** CRLF becomes exactly the existing LF input before any of these branches. Root ordinary paragraphs are R1; existing code is R3. No demand to retest every Markdown feature. |
| Known skills, mentions, citations, context references; absent skills / stale undo chip; token at start/end; active path query | Token recognition adds boundary spaces; path-query length can keep an initial prefix literal. Record and skill data determine chip labels/previews and overlays. | **Inventoried; parsing/spacing/undo code unchanged.** Use no tokens or active suggestions in required fixtures. These features do not create another newline-normalization shape for the selected input. |
| Disabled editor / project required / choice-only question / responding state | `disabled` updates Tiptap editability; alternative placeholder text depends on approval, pending question, plan follow-up, project/provider availability. Literal pending answer is above. | **Inventoried; no new capture.** Required rows use enabled idle state. No loading or error branch was added or modified. Environment banners and attachment preparation are parent UI outside this diff. |
| Suggestion IDs, aria labels, placeholder classes, container/editor classes, context/skill overlays | Accessibility props and parent classes do not change normalization; editor classes determine sizing, typography, overflow. Citation interaction can display a comment editor and previews. | **Inventoried; unchanged.** No suggestion/citation overlay in required fixtures. Normal short paragraphs fit both caller height caps. |
| Theme / width / text length / font / platform | Native caret inherits foreground/theme; rich code has theme-dependent highlighting. Editor uses wrapping, minimum two-line height, max-height scrolling. Small coarse-pointer web screens use touch font sizing; mobile web can collapse/reveal composer and add pending-answer bottom padding. | **One fixed dark desktop viewport required.** Short `x/y` lines do not wrap or reach a cap, and normalization has no branch on these axes. Long, scrollable, narrow, custom-font, light-theme, code-language, and collapsed mobile-web combinations add no distinct changed path. If a later capture exposes a branch-specific discrepancy, expand the matrix then. |
| Clipboard shortcuts, focus exclusions, Undo/Redo, mode switch, send | Focus redirect excludes editable/interactive targets, modal layers, secret requests, terminal ownership, command palette and model picker. Paste-as-text arms a one-second attachment-fold bypass. Undo/Redo and rich/plain switching use existing history/remount behavior. | **R1 normal paste, R2 blur→paste→focus, R3 fence; all recording.** No separate timed shortcut recording needed for short text: no attachment-fold threshold is reached. No send/undo/mode-switch regression claimed or required by this diff. |
| Surfaces / connection modes / providers / entry points | Desktop Electron wraps web; hosted web, local web and remote/relay/tunnel share client-side paste logic. Native iOS/Android have a separate React Native composer. No provider, RPC, server persistence, MCP, schedule, settings action or command-palette mutation is changed. | **Shared web coverage; no duplicate Electron/mobile/provider/connection capture.** Desktop native clipboard delivery is not claimed tested. Settings preview is listed above. |

## Handoff and pass-1 status

The capturer should return six labelled segments for R1–R3 (or two row-labelled composite videos), with R1/R2 LF controls, the R1 standalone-CR segment, and R3 escaped document evidence. Include the exact commit, viewport, theme, client, clipboard injection method, and row IDs. A later proof audit must watch the recordings; this matrix is a plan, not a claim that the fix has passed visually.

No code findings were established in this proof-scope audit. Diff whitespace check passed. No worktree files were modified. Available root disk space was 19 GB at the start.

FRICTION: none

VERDICT: INSUFFICIENT R1, R2, R3 not yet captured
