# PASS1 read-only proof audit: issue 22181

Scope: `/home/ubuntu/wt/issue-22181`, branch `fix/issue-22181`, exact head `fabd3341d8d27068e86e2b80854031a880e7a727`, base `70e5e384dab96a1d1e4a253ee5a9415ed5d8ce30`. Complete diff: only `packages/shared/src/markdownLinks.ts` (classifier) and its test (six cases). No PR exists. Source was read only. No browser, capture, source edit, seed execution, server start, push, or forge comment was performed. This is a proof plan, not a claim that visual proof passed. User reports both prior reviews MERGE after the selected rule was made deliberate.

## Selected behavior and fixture data

A position suffix no longer makes an unprefixed slash-containing extensionless basename a file candidate unless that basename is in EXTENSIONLESS_FILE_NAMES. Explicit relative/absolute paths bypass this new check. Backslashes normalize before the check. Classification is syntactic, not a file-existence check.

Use deterministic assistant/user/plan/work-item text and a small dedicated workspace, identical on base and head. Do not rely on copied live data for final proof. Capturer owns committing the deterministic seed and data. Suggested message payload (actual Markdown code spans, not escaped backticks):

```markdown
The V5 file includes the `I/W217:220` fix (columns I and W, rows 217–220).

Rejected paths: `A/B12:40`, `foo/TODO:12`, `bin/run:12`, `foo/TODO:12:3`, `foo\TODO:12`.

Retained files: `packages/shared/src/markdownLinks.ts:167`, `docs/README:12`, `./bin/run:12`, `TODO:12`, `../bin/run:12`.

Already plain: `W217:W220`, `E29:E31`, `foo/TODO`, `src/main.ts/`.

Authored controls: [I/W217:220](I/W217:220), [friendly label](foo/TODO:12), [`foo/TODO:12`](foo/TODO:12).

Collision control: `foo/TODO:12` and [TODO](other/TODO:12).

Long wrapping sentence with `I/W217:220` and `foo/TODO:12` near the right edge, followed by `docs/README:12`.

~~~text
I/W217:220 foo/TODO:12 bin/run:12
~~~
```

Repeat the collision control in a separate message if necessary to isolate its parent-suffix group. Include actual `docs/README`, `bin/run`, and `other/TODO` with at least 12 lines, and a nested `docs/proof.md` containing the same payload. Existing source is the extension-bearing control. For cross-platform classification controls, add explicit POSIX and Windows absolute tokens and a backslash-relative token; these do not require files or actual host opens on Linux. No extra capture is needed for each classifier example: put examples in the same message/visible region.

Expected base/head: rejected inline tokens were chips on base when cwd resolves them; on head they retain the entire authored code text, with no file icon, shortened basename, line badge, file tooltip, or file action. `bin/run:12` is intentionally plain. `docs/README:12` and `./bin/run:12` remain candidates/chips with cwd. `TODO:12` remains a candidate despite having no slash. Authored link destinations bypass the classifier and retain behavior. Exception: a surviving authored chip's *label* can change when a removed candidate no longer contributes to basename disambiguation. In the collision example, `other/TODO` loses its parent suffix on head; its destination/action remains unchanged. Fenced code stays fenced code. Plain non-Markdown text is unaffected.

## Caller and data trace

All file paths below refer to this local checkout, not external forge files.

1. Shared classifier → `apps/web/src/markdown-links.ts:resolveInlineCodeFileLinkMeta(codeText,cwd,baseDir=cwd)` → `ChatMarkdown` precomputed `inlineCodeFileLinkMetaByText` (around line 623) and inline code renderer (around line 1311). Only HAST code with `dataInlineCode` becomes a chip. Fenced code/math take other branches. File metadata contains filePath, targetPath, displayPath, workspaceRelativePath, workspaceLookupPath, basename, optional line/column. Relative tokens require cwd/baseDir to resolve. Unprefixed slash paths in a nested workspace preview resolve from workspace root; explicit `./` paths resolve from the preview base directory.
2. `ChatMarkdown` builds parent suffixes from BOTH authored destinations and inline candidates, then renders `MarkdownFileLink` with resolved theme, threadRef, environmentId and action availability. Thus a false candidate can also alter another chip's label. `MarkdownFileLink` renders an anchor when a primary action exists, otherwise an options button. Hover tooltip, context menu, file preview/editor actions, and Markdown clipboard metadata disappear for rejected tokens; ordinary code selection remains.
3. Block Markdown callers: `TimelineAssistantRows` (message text, markdownCwd, threadRef, streaming, lineBreaks); `TimelineUserMessageRow/UserMessageBody` (same cwd, threadRef, lineBreaks, raw HTML disabled); `ProposedPlanCard` (displayed plan vs collapsedPreview, folded region); `TimelineWorkEntryRow/ReasoningTraceContent` (expanded entry.detail, streaming, lineBreaks); `TimelineV2EventRow` (presentation.detail); `V2ItemInspector` (assistant item.text); `ComposerPendingUserInputPanel` (activeQuestion.question); `UserMessageContextChips` (review comment text, cwd, no threadRef); `PullRequestMarkdown` (body/comment segments, cwd/threadRef/environment, autolink plugin); `FileMarkdownPreview` (text,cwd,threadRef, imageBaseDir derived from relativePath); `AttachmentFilePreview` (text, cwd undefined); `composerContextPresentation` (comment text, cwd undefined); `WorkflowOpeningBlock` (scriptMarkdown, cwd undefined).
4. Compact thought callers in `TimelineWorkEntryRow` and `TimelineWorkGroupRows` use `inline`, raw HTML disabled, and `remarkThoughtPreview`. Trace `TimelineWorkEntryRow.logic.ts:192`: that plugin flattens inlineCode/link nodes into plain text. They do NOT expose the changed chip rendering; do not demand a separate compact-thought capture. Expanded reasoning still does. Review-comment diff fences and workflow shell fences likewise do not gain an inline-code path from this diff.
5. Shared classifier → `packages/shared/src/threadFindText.ts:markdownThreadFindText`: both path collection (parent suffixes) and code-node conversion. `searchableMessageSegments` indexes assistant/user; `searchablePlanSegments` indexes displayed plan text and title. Server `ThreadFind.ts:searchableRows` selects user_message, assistant_message, proposed_plan, excluding folded duplicate async answers. Work detail/question/PR/file-preview text is not silently assumed indexed. Server resolves cwd from worktreePath/project workspace root. Visible web path is `useThreadFind` → timeline positioning → `threadFindHighlights.ts` DOM text ranges / CSS Highlight API, including active/inactive highlights, folds and mutations. Search must agree with displayed full code after this fix, not an old shortened chip label.
6. Mobile classifier helper `apps/mobile/src/lib/markdownLinks.ts:resolveMarkdownInlineCodePresentation` is imported by ThreadFeed but has no invocation there (repository search found only definition/import). Active feed: `AssistantMarkdownContent` / user content → `SelectableMarkdownText` → `MobileEnrichedMarkdownText` → `markdownWithInlineFileLinks(..., ENRICHED_INLINE_FILE_LINK_REGEX)`. The regex in enrichedLinkPresentation.ts is independent of the changed classifier. FileMarkdownPreview/pending-input native renderers use the same native component. No native mobile render row is affected by this diff; 390px web evidence is not native evidence. No native mobile capture is required.

## Matrix and collapse

Every row below uses web at **1440×900 light, 1440×900 dark, 390×844 light, 390×844 dark**, on **both actual base and actual head**. Pin the same built-in light/dark theme and default font size in Settings → Appearance / Colors & themes; disable OS-following theme selection. Heights are prescribed here for repeatability; requested widths are 1440 and 390. Use the same sidebar/panel state per paired frame. At 390, close sidebar; never substitute a clipped desktop viewport for responsive layout. If a fixture is too tall, shorten surrounding prose rather than omit an example; additional scroll frames expand the count below.

“Affected” includes indirect/search effects. “Minimum” means the smallest representative set after collapsing identical renderer/data branches, not every caller duplicated. Stills cannot satisfy recording rows.

| ID | Affected / minimum | Distinct renderer/data and conditional state | Real app route / settings / seed status | Evidence and acceptance |
|---|---|---|---|---|
| S1 | Yes / required | Completed assistant Markdown with cwd/threadRef; full mixed payload, authored controls, duplicate basename collision and narrow wrapping. | Existing seeded assistant on lead web at localhost:7798 (server 15838), thread route `/$environmentId/$threadId`. Existing seed available; deterministic expanded fixture not yet supplied. | **Still**. Whole literals retained on head; accepted controls still chips; authored targets stable; collision suffix change intentional; no overflow at 390; contrast in both themes. |
| S2 | Yes / required | User-message bubble, lineBreaks=true, parseRawHtml=false; same tokens including multiline and authored/code-link control. | Same deterministic thread, stopped user_message with text/context consistent. Not seeded by current seed.py. | **Still**. Bubble layout and wrapping are visually distinct from assistant; full rejected code survives. Collapse expanded reasoning, V2 assistant inspector/event details, pending question and review-comment block into S1/S2 renderer coverage, subject to reachability list below. |
| S3 | Yes / required | Workspace Markdown preview; cwd plus nested imageBaseDir; root-relative versus explicit `./` controls. | Open committed `docs/proof.md` in Files panel, choose rendered Markdown. Seed file not present in supplied seed.py. Keep panel width identical per revision; at 390 use the app's actual panel/sheet presentation. | **Still**. Rejected inline code plain; accepted chip labels and `./`/workspace-relative resolution retained. Capture preview rather than raw editor. PR body/comment Markdown uses same block renderer and candidate logic, so no extra PR-dependent capture. |
| S4 | No direct delta for relative tokens / required negative branch | Markdown without cwd/threadRef; relative candidates cannot resolve on either revision; explicit absolute path can still become an options chip, and exercises no-primary-action branch. | Attachment Markdown preview (`AttachmentFilePreview`) with fixture attachment, or composer review-comment preview (`composerContextPresentation`). Needs deterministic attachment/comment seed; not available in current seed.py. Include one absolute source-shaped token with no host-open assertion. | **Still**. No relative chips on either revision; absolute chip remains. Do not claim the issue reproduces in this branch. This covers cwd undefined callers; fenced workflow shell text needs no new row. |
| R1 | Yes / required | Interaction transition from false chip to ordinary code, surviving true chip, authored chip; selection/copy; primary action and options-only context menu. | S1 thread and S4 no-thread preview. Normal host permissions for local file panel; use existing control files. Fixture additions required. | **recording**. Hover rejected token, dismiss, right-click, select/copy into a disposable visible text field without sending; show full literal rather than old file action. Hover/open accepted `./bin/run:12` and authored control, show line-12 file panel, close it; show options-only absolute chip menu in S4 and dismiss. At 390 use touch/tap/long-press/context-menu affordance actually supported; do not simulate hover as native mobile. Editor modifier action requires configured host editor; if unavailable report it rather than launch arbitrary apps. Stills insufficient. |
| R2 | Yes / required | Thread find counts + active/inactive visible highlights, next/previous, query replacement/clear, user/assistant/plan text and collapsed plan reveal. | Same thread with ≥2 occurrences and a long proposed_plan whose occurrence lies below collapsed preview; populate projected plan item and message items consistently. Open “Find in current thread” via command palette (`chat.find`) or configured shortcut, never browser Ctrl+F. Server threadFind capability must be enabled. Current seed has only assistant text; plan/user/duplicate fixture unavailable. | **recording**. Query `I/W217:220`, `foo/TODO:12`, then collision text/retained `README` chip label. Show count and matching painted code, next/previous across assistant/user/plan, reveal folded plan, clear query and Escape close. Base may have zero full-literal match because a chip displayed basename/line; head must find visible full code and retain accepted-chip search. Show full and partial queries, not only count. Plan card before/after reveal provides its distinct card layout proof in this recording; no redundant plan still. CSS Highlight API support required; missing highlight support is an obstacle, not visual success. |
| R3 | Yes / required | Streaming partial-to-complete Markdown and settled render; classifier runs as token text changes, mutation invalidates find highlights. | Deterministic replay of an assistant streaming item on isolated seed, with chunks through an open backtick, completed `I/W217:220`, additional text, then streaming=false. Expanded reasoning entry can be a supplementary replay using same renderer. No replay mechanism supplied by seed.py; fixture/replay work required by capturer. | **recording**. Start before code closes, show completed code and final settlement, no stale chip or duplicate text; keep find query open for one repeated occurrence and show highlight settling correctly. Actual base/head runs required; do not fake stream with CSS or detached DOM. Stills insufficient. |
| R4 | Yes inherited web renderer / required platform proof, unavailable here | Desktop/Electron shell, same completed/interactive thread; shell capabilities can add editor/reveal actions, platform modifier differs on macOS. | Desktop dev client connected to the same deterministic isolated server; real Electron runtime and platform-specific host editor setup needed. **Desktop/Electron unverified on Linux in this audit**; no desktop session was exercised. Capturer must supply it or explicitly leave this row unverified. | **recording**. Repeat S1 appearance and R1 primary/modifier action/menu behavior in Electron; include both requested widths/themes if desktop window can attain 390 CSS px, otherwise report minimum-window-width obstacle and keep responsive proof web-only. Linux desktop proof does not verify macOS Cmd or Windows Ctrl/native reveal. Stills insufficient. |

Minimum set: **8 rows: 4 still rows + 4 recording rows**. For available web coverage S1–S4 and R1–R3: **32 still images + 24 recordings** (each row × four width/theme combinations × two revisions). R4 adds **8 desktop recordings**, making **32 stills + 32 recordings = 64 artifacts** for this defined full representative set, contingent on native window sizing. One recording may contain several interactions/states in its row; do not multiply by individual tokens/callers. Combined recordings may cover multiple rows if the complete sequences and viewport/theme/revision provenance remain clear; the number of required row-condition observations stays 64. S4 is an explicit negative/control branch, so the smallest affected-only web set omits its 8 stills, but that is not the full branch audit requested here.

For bare issue-repro evidence alone, S1 at four width/theme combinations on actual base/head is eight stills; that narrower set does not prove interactions, find, previews, streaming, or Electron. No additional native mobile artifacts are in this minimum.

## Reachability, fixture availability, and limits

- User supplied a running lead `vp run dev` app on port 7798 with server 15838 and a T3 browser tab. This audit did not inspect that tab, consume a pairing URL, read a browser snapshot, restart the app, or validate server state. The deterministic capturer should record actual environment/thread IDs and full route after seeding; do not publish credentials or startup pairing URLs.
- `/tmp/issue-22181/seed.py` exists and was read only. It selects the most recently updated assistant message, replaces message payload text and matching assistant turn-item payload text in the worktree DB. It supplies the primary spreadsheet repro, A/B12:40, foo/TODO:12, W217:W220, E29:E31 and three retained files. It does NOT supply bin/run negative, authored links, collision control, user/plan, preview attachment, streaming replay, or stable IDs; it is not a complete deterministic fixture. Its selected thread ID cannot be derived from source alone. Do not run it against a live/shared home. No copied live data is required for final fixture.
- Expanded reasoning: create a stopped reasoning work entry with detail=fixture; expand work log in the thread. V2 inspector: inspect a projected assistant item with text=fixture. V2 event details: use presentation.detail carrying fixture. Pending-input: seed a request with activeQuestion.question=fixture and inspect the composer question panel. These callers are source-reachable but not currently seeded; shared block renderer collapses their token classification proof into S1/S2. No claim that actual routing/fixtures for each was exercised. Any request for per-entry-point visual certification expands the minimum.
- Review comments: thread context chip comment text with cwd reaches changed block renderer; composer preview of the same comment uses cwd undefined (S4). A comment diff rendered as a fence is unaffected. Seed context/comment structures, not plain assistant text pretending to be a comment.
- PR Markdown: body/comment fixture with cwd uses S1/S3-equivalent block logic; no PR exists and no forge mutation/read is needed for this audit. A deterministic local PR panel fixture is unavailable; don't create a PR solely for evidence. Missing PR-specific UI proof remains disclosed, even though renderer coverage is collapsed.
- Plan: seed a genuine proposed_plan turn item; collapsedPreview and displayedPlanMarkdown are distinct sources. R2 must place target below truncation and record reveal. Empty/nonassistant messages do not introduce new affected classifier branches. Deleted/hidden/folded inherited items need no separate classifier fixture unless used to prove search navigation.
- Workflow opening: scriptMarkdown is shell-fenced and cwd undefined, so not an affected inline render route. Compact thought preview strips code identity before rendering, unlike expanded reasoning. Native mobile helper is inactive; do not list native iOS/Android as affected solely because of its import.
- Web and desktop share ChatMarkdown, but shell/editor/reveal and OS modifiers are not established by web recordings. Explicitly **desktop/Electron unverified on Linux**; macOS/Windows host actions also unverified. No native mobile changed render path was found. A narrow browser is only responsive web.
- Local/remote/tunnel use the same typed text/cwd renderer, so no extra layout combination is justified by this diff. Remote editor/shell permissions can change action availability; options-only branch is represented in R1/S4. No remote/relay/tunnel execution was verified. Providers all supply the same Markdown string path; no adapter changed, so no six-provider capture multiplication.
- Existing repro filenames supplied by user: `browser-screenshot-localhost-mvipazfm-718dec9e.png` and `browser-screenshot-localhost-mvipb0j3-9aa85e0a.png`, under `/home/ubuntu/.t3/userdata/browser-artifacts/`. Neither was opened or captured. They are prior repro references, not verified actual-base/actual-head proof and not sufficient for recording rows.
- Capturer should record revision, deterministic fixture version, route, viewport CSS dimensions, theme, client/platform, and entry point for every artifact. Label base/head explicitly. Capture actual source revisions; don't use DOM edits to simulate the change. This audit did not execute tests; existing diff adds classifier cases but no search/renderer integration test.

## FRICTION

- Current seed chooses “latest assistant” rather than stable fixture IDs and only mutates projection payloads; it cannot prove deterministic replay/search/plan state by itself. Capturer's planned committed seed+data resolves this; copying live data is unnecessary.
- Seed gaps block S2–S4 and full R1–R3 today: authored controls, collisions, line-12 files, user/plan items, attachment/comment context, and stream replay must be supplied.
- Exact route IDs, theme configuration, threadFind capability, Highlight API support, panel sizing at 390 and actual base/head server provenance were not browser-verified under the explicit no-browser instruction.
- R4 requires a real desktop session and shell/editor setup. Desktop/Electron remains unverified on Linux; macOS/Windows-specific actions cannot be certified here. A 390px Electron window may be prevented by minimum size.
- No PR exists; no PR body/comment fixture is currently available. Its shared renderer is traced and collapsed, not claimed visually exercised.
- This matrix is the only scratch output authorized for upload. It contains synthetic fixture text and source-derived observations, no live conversation payload, browser credential, pairing link, or secret. Upload is public through ghdrop as explicitly authorized.
