# Proof audit pass 1 — surrogate-safe truncate

## Identity and verdict

Audit target: local takeover/pr-13413 at [95c8f6b](https://github.com/pingdotgg/t3code/commit/95c8f6b2250479002501e44ac03560d1ba747a90), full diff against merge-base [7fd2e09](https://github.com/pingdotgg/t3code/commit/7fd2e09e381a61f427da09dd7a8f2c9c1558fe59). Worktree: /home/ubuntu/wt/pr-13413. Clean at start. Only packages/shared/src/String.ts and String.test.ts differ. No UI component changes.

Original [#13413](https://git.cafe/pingdotgg/t3code/pulls/13413) ([GitHub mirror](https://github.com/pingdotgg/t3code/pull/13413)) still reports ValeraZSD/fix/shared-truncate-surrogate at [ca8b37b](https://github.com/ValeraZSD/t3code/commit/ca8b37b67b6c03c7f4e62060ae58a637222e5831). Forge description discusses broader grapheme handling. Those claims/checks are not replacement-head evidence. This audit uses the dispatcher's narrower specification: prevent UTF-16 pair splits; preserve ASCII/BMP lengths. ZWJ/combining/flag clustering is not an acceptance condition.

No correctness blocker found for that specification. Proof is not complete until pass 2 supplies the authorized web still. No browser or screenshot was run in pass 1. Tests alone do not satisfy the dispatcher’s evidence requirement.

## Fixture matrix derived from the complete diff and caller graph

Affected means output can change on a formerly split pair; controls establish unchanged behavior. Counts refer to UTF-16 code units after normalization/trim; suffix remains three ASCII dots. Let A(n) mean n ASCII `a` characters.

| Row | Surface / path | Affected? | Fixture and expected replacement | Minimum proof |
|---|---|---|---|---|
| H1 | shared truncate backoff | Yes | `abcd😀 more`, limit 5 → `abcd...`; old result has lone high surrogate | focused helper test; represented visually by T1 |
| H2 | exact pair fits | Control | `abc😀 more`, 5 → `abc😀...`; `abc😀`, 5 → unchanged | tests only |
| H3 | ASCII/BMP/trim | Control | padded `hello world`, 5 → `hello...`; Japanese text, 3 → first three BMP units + dots; short/empty unchanged | tests only |
| H4 | very small budget | Yes/control | `😀 more`, 1 → `...`; budget 0 → `...` | tests only |
| H5 | already malformed input | Control | lone high followed by `X`, and lone low, remain as before | scratch checks only; helper does not repair malformed input |
| T1 | deriveThreadTitleSeed prose; web useComposerSend and useMultipleModelSend; mobile projectThreadStartTurn and outbox drain | Yes | `{text:A(49)+"😀 more",attachments:[]}` → A(49)+`...`; old A(49)+lone high+`...` | actual caller test + **one before/after web still** |
| T2 | same shared seed, file attachment branch | Yes | empty prose, non-image filename A(43)+`😀 more`; `File: ` contributes 6 → total 49 before emoji | actual caller check only; equivalent rendering to T1 |
| T3 | same shared seed, fallback label (single-send context fallback) | Yes | empty prose/attachments, fallback A(49)+`😀 more` → A(49)+`...` | actual caller check only |
| T4 | image-only and no-input branches | No | `Attached image` / `New thread` | caller controls only; no truncation in these branches |
| P1 | web useProposedPlanSend; proposedPlan title builder | Yes | heading `# `+A(39)+`😀 more`; `Implement ` contributes 10 → A(39) after prefix, then `...` | real plan builder + helper check; no extra screenshot |
| M1 | mobile threadActivity compact labels, expanded preview and command summary | Yes | normalized A(199)+`😀 more`, limit 200 → A(199)+`...` | helper at actual 200 budget; no native screenshot required for shared-only change |
| R1 | web sidebar title renderer; desktop embeds web; mobile title renderer | Yes, consumes changed seed | synthetic T1 title displayed without CSS hiding final glyph/dots | one web still covers dispatcher requirement; do not multiply by provider/model/connection mode |
| U1 | settings, palette, keybindings, provider adapters, RPC contracts, relay/tunnel | No changed logic | title string is computed client-side and transported unchanged; no protocol delta | source trace; no unrelated captures |

Source anchors at replacement head: [helper](https://github.com/pingdotgg/t3code/blob/95c8f6b2250479002501e44ac03560d1ba747a90/packages/shared/src/String.ts), [seed branches](https://github.com/pingdotgg/t3code/blob/95c8f6b2250479002501e44ac03560d1ba747a90/packages/client-runtime/src/operations/threadTitle.ts), [single send](https://github.com/pingdotgg/t3code/blob/95c8f6b2250479002501e44ac03560d1ba747a90/apps/web/src/components/chat/useComposerSend.ts#L1157), [fanout](https://github.com/pingdotgg/t3code/blob/95c8f6b2250479002501e44ac03560d1ba747a90/apps/web/src/components/chat/useMultipleModelSend.ts#L132), [plan send](https://github.com/pingdotgg/t3code/blob/95c8f6b2250479002501e44ac03560d1ba747a90/apps/web/src/components/chat/useProposedPlanSend.ts#L304), [mobile start](https://github.com/pingdotgg/t3code/blob/95c8f6b2250479002501e44ac03560d1ba747a90/apps/mobile/src/lib/projectThreadStartTurn.ts#L41), [mobile outbox](https://github.com/pingdotgg/t3code/blob/95c8f6b2250479002501e44ac03560d1ba747a90/apps/mobile/src/state/use-thread-outbox-drain.ts#L865), [mobile activity](https://github.com/pingdotgg/t3code/blob/95c8f6b2250479002501e44ac03560d1ba747a90/apps/mobile/src/lib/threadActivity.ts#L275), [sidebar render](https://github.com/pingdotgg/t3code/blob/95c8f6b2250479002501e44ac03560d1ba747a90/apps/web/src/components/sidebar/SidebarThreadRow.tsx#L989).

## Minimum capture set

One composite static still, clearly labelled **Before — base** and **After — replacement**, showing the same T1 title in a real web sidebar. Two source PNGs may be combined into this one deliverable. Before must visibly show the replacement glyph at the split; after must show no replacement glyph and the suffix. The emoji is deliberately omitted in the fixed split case. H2 separately proves an emoji that fits survives.

Use the same theme, viewport, font and sidebar width. Widen sidebar enough that the final glyph and three literal dots are visible; CSS ellipsis must not conceal them. Include enough app chrome to identify the actual sidebar. Inspect text content/code units as well as pixels: old ends in U+D83D followed by dots, new has only 49 ASCII units plus dots. Do not silently replace the old lone surrogate with literal U+FFFD in the fixture generator.

No recording is required: no motion, loading transition or interaction semantics changed. Navigation/seeding interactions are setup, not proof of a changed interaction. No provider matrix, fanout repetition, mobile/desktop captures, settings screenshots or unrelated rendering.

## Real-app seeding route (preferred; source-validated, not executed in pass 1)

Use a new isolated worktree .t3 home, initialized by the dev server. Do not read or copy ~/.t3/userdata. Do not use migrate-dev-db. Follow test-t3-app and its SQLite fixture reference. Dispatcher explicitly authorizes headed Chromium on Xvfb; use the built-in Browser workflow when available and confirm the runtime satisfies that requirement. Permission need not be requested again.

1. Start `vp run dev` in the specified checkout, capture the process/session, read actual ports and pair a browser privately. Never publish startup pairing URLs or auth cookies. Never set VITE_HTTP_URL or VITE_WS_URL.
2. In this isolated app, create a disposable synthetic project/thread through supported app commands/UI without sending an agent turn. Obtain its actual thread ID. Stop only this captured dev-server process before any direct SQL writes.
3. Run the public rebuild script below. Its fixtures.json contains before/after output from the **actual deriveThreadTitleSeed function** with only its truncate import switched to the base implementation for the before case. The replacement case uses the audited helper. The web title renderer itself is unchanged, so switching the seeded result on the same app revision is sufficient; do not mislabel this as running a base application build.
4. Generated before.sql and after.sql update both title and payload_json.title in orchestration_v2_projection_threads for `proof-13413`. Substitute the disposable app-created thread's actual ID in these scratch SQL files. Inspect schema first. Execute each file through `node apps/server/scripts/t3-sqlite-state.ts exec --base-dir <isolated-home> --file <file>`, while stopped. Confirm exactly the intended disposable row was changed. Do not insert partial rows or change the event log.
5. Restart the same isolated runtime for each state, capture the same sidebar row. Direct projection seeding proves caller-output rendering, not send/persistence behavior. Tests/source trace provide the computation proof. No provider invocation is necessary.

There is no established seeding blocker in pass 1, so a harness is not currently justified as the primary capture. If pass 2 cannot create a disposable thread or safely seed the isolated projection, record the exact failure and use the generated harness.html. It is prominently labelled “caller harness — not the T3 app” and renders actual caller outputs. Capture it in authorized headed Chromium on Xvfb; do not claim real-app integration. Lack of live-user data is not itself a harness justification.

## Public rebuild / pass-2 handoff

[Download rebuild.mjs](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/4ecb01b0cbfb3469/rebuild.mjs). Everything is outside the repository. Pass 2 must download this PUBLIC file, rather than depend on retained scratch or a committed evidence folder:

```sh
mkdir -p /tmp/ev-13413-pass2
curl -fL 'https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/4ecb01b0cbfb3469/rebuild.mjs' -o /tmp/ev-13413-pass2/rebuild.mjs
node /tmp/ev-13413-pass2/rebuild.mjs /home/ubuntu/wt/pr-13413 /tmp/ev-13413-pass2/rebuilt
```

Requires modern Node with stripTypeScriptTypes (validated on v26.12.1), installed workspace dependencies, audited HEAD and the base commit available locally. It reads Git/source only; writes solely to the explicit /tmp output directory. It checks HEAD, loads base helper via git show without checkout changes, imports the actual seed and plan builder, checks expected values, and emits fixtures.json, before.sql, after.sql, harness.html. Native TypeScript stripping does not alter the helper algorithm. Input strings are synthetic/public. Generated SQL is setup material and must never target live state.

The public script is the reproducible data/setup source, not a browser automation script. Pass 2 follows the app Browser skill to produce the actual screenshot; it must report any headed/Xvfb tooling mismatch. Do not commit scratch, generated SQL, or proof screenshots. Upload evidence through ghdrop. A fresh public download and rerun should pass without the pass-1 scratch directory.

## Executed checks

- `vp test run packages/shared/src/String.test.ts`: 1 file, 3 tests passed.
- Public rebuild source locally executed: 13 explicit helper/plan checks, 5 actual seed-caller checks, 51 cut-position checks passed. The original implementation fails split fixtures as expected; ASCII/BMP controls agree.
- Full diff inspected; grep traced all shared truncate imports/calls across apps and packages.
- No lint/typecheck/full suite or browser capture claimed. This is a proof audit, not a completed merge-readiness review.

## FRICTION

- Forge PR metadata and CI refer to original ca8b37b, not replacement 95c8f6b. Wider grapheme claims in that description must not leak into this narrower proof.
- No colocated threadTitle.test.ts exists at the inferred path; actual caller checks are provided by the public scratch script instead of adding repository tests.
- A first scratch run exposed data-URL imports retaining TypeScript syntax; fixed with Node's native stripTypeScriptTypes and reran successfully. Node emits its experimental-feature warning.
- Seeding route and headed/Xvfb availability are not runtime-verified in pass 1. SQL requires the actual disposable thread ID, initialized current schema, and a stopped isolated server. Do not call this a finished visual proof.
- No repository edits, commits, pushes, comments, watching, delegation, or live-userdata reads. Only public artifact uploads and app PR registration accompany read-only inspection.
