# Rebuild the synthetic citation evidence

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

Use isolated checkouts at the two pinned revisions. Run `vp install`, then `T3CODE_DEV_SEED=0 vp run dev` in each. Never seed from live T3 userdata. Round 2 used before web port 8113/server 16153 and after web port 8396/server 16436. Round 3 used before web port 8478/server 16518 and after web port 8035/server 16075. If those differ on another machine, adjust the driver origins and the `init.js` development import path. All imports resolve the real checkout source; no production file was changed.

Run `seed.mjs` once per fresh server, supplying its private pairing URL in `PAIR_URL`. The file is a standalone Playwright seed driver. It creates the scratch git project, creates a parent through the scoped project/startTurn commands, forks a used side chat with the real forkSideChat command, runs only the free Replay (dev) Showcase scenario, and selects a sentence from the rendered source to build valid UTF-16 citation selectors. Copy the returned synthetic `citations.json` to `before-citations.json` or `after-citations.json` beside the scripts. Do not copy the private session file into evidence.

The interactive `driver.mjs` opens Chromium and accepts JSON lines containing an async JavaScript body in `code`. Put fresh, private pair-command output into `before-pair-private.log` and `after-pair-private.log` for `api.login`. Those logs are private and are deliberately absent from uploaded evidence. Authenticate with `await api.login('before'); await api.login('after');`. Load `runtime-extensions.js` in the same `code` body, then call the row module functions with `api.forSide("before")` or `api.forSide("after")`, except R01/R02 which take the side explicitly. It fixes the export caption margin and closes prior contexts between fixtures. End with `{"exit":true}`. The driver creates a timestamped output directory; raw recordings remain there for inspecting rejected exports.

Row calls:

- R01: `await (await import(api.ROOT+'/R01.mjs')).default(api,'before')`, then `after`.
- R02: same signature in R02.mjs; perform it before adding extra fixture turns.
- R03: default export in R03.mjs; real selection/Cite/Save, unsent link click and Enter.
- R04 and R18: named functions in panels.mjs; final R20 uses panels-r2.mjs. R04 creates S2 only if absent. R18 writes the uploaded synthetic source-quote.md content into the scratch project through the filesystem and opens the real Files preview.
- R05/R06: responsive.mjs default with `api,'R05'` or `api,'R06'`.
- R07: default in R07.mjs; uses the current valid S1 quote created on each side by `prepare-native.mjs`, then records separate real Control, middle, Shift, and Alt clicks. Parent click and popup video are concatenated because Playwright records each page separately. `repair-popups.mjs` cuts from the native click to the final six settled seconds of the actual destination tab; startup loading is omitted and the cut is labelled. The captions identify the cut and destination URL.
- R08: default in R08.mjs, after R07 creates its current-source quote. It reuses the actual href recorded in R07-native-manifest.json rather than relying on the holder quote remaining in its initial virtualized page. A newly opened authenticated page has no existing source panel, then reload, navigation, Back/Forward run through the real browser history.
- R09: default in R09.mjs. **HARNESS** holds only the real quiet source-shell atom's read function at null, then restores its original implementation. The actual mounted ChatView hook/router/store/pane handle navigation. The state JSON records the null source, fallback route, replacement route, unchanged hash/activation/history length, and resulting parent panel. No source files are edited.
- R10/R14: `await (await import(api.ROOT+'/history.mjs')).seed(api)` before either capture. It adds one completed Replay turn at a time and checks a freshly loaded real history cursor and source absence. This run reached 11 total S1 turns. Then `capture(api,'R10')` or `capture(api,'R14') in history-r2.mjs for the repaired warning still`. R14 is a **HARNESS** which returns HTTP503 only for the first target S1 history request; it leaves the environment online, restores the next request, and explicitly activates the quote again. History state includes the real request URLs and rendered source bounds. `sourcePresent` refers only to the bounded projection's message array; paged timeline content is separately proven by `renderedSourceRects`.
- R11: default in R11-final.mjs after pagination seeding. It expands a real work group, selects an intermediate assistant response, saves a citation, collapses the group through its disclosure, then activates the saved quote. This demonstrates a source actually hidden by a fold rather than a final reply that never folds.
- R12/R13/R15/R16/R17: named exports in controls.mjs. Missing-message and changed-text inputs are serialized valid synthetic citations. R13 chooses the current S1-owned message ID so the stale-text warning is isolated from history paging. The ordinary fork is made through the real Fork from this response UI and owns its quoted Replay reply.
- R19: default in R19.mjs. This was a **real two-environment pass**, not a state harness. Each revision uses its own server as E1 and the other isolated server as E2; both contain synthetic data only and speak the same unchanged protocol. Mint a fresh private pair token for E2 into `secondary-pair-private.log`, update the environment ID to that server, and run. It creates distinct e2-parent/e2-side IDs, selects an actual E2 reply, saves its citation in E1, and compares E1's real panel state before and after navigation. No external machine or real account was used.

`render-video.mjs` adds a caption margin above the unchanged whole app frame. The actual current route is overlaid there; only the long base64 hash payload is abbreviated. Millisecond timing and full synthetic URLs are retained in each steps JSON. On-screen elapsed time is measured from capture start. Some R01/R02 action labels also contain nominal script times; the numeric elapsed timestamp is authoritative.

Stills use reduced motion, network idle, loaded fonts, and two pixel-identical PNGs taken 500ms apart. Recordings retain normal motion. `diffs.mjs` produces max-RGB-channel differences: exact >0 with 32x amplification and a visible minimum, plus >16/255. `contact-sheets.mjs` samples eight frames from each full video. `comparison-sheets.py` adds revision labels above whole Before/After frames without cropping. The contact sheets are indexes, not substitutes for videos.

The optional characterization groups C01–C06, Windows Electron, and native mobile were not run. R06 is desktop Chromium at a 390px viewport, using click-equivalent input, with no device or touch emulation.

Round 2 control recapture: after controls.mjs creates the ordinary source/fork fixtures, use controls-recapture.mjs for R15/R16/R17. Completed work is expanded and the real final reply centered; end-frame Diff0 is zero for these controls. R20 omits the optional PR page and captures settled Settings/Usage returns. R19 returns to E1 and collapses the sidebar using its real UI before recording. Mask coordinates come from DOM ranges over only server-measured duration digits; the app remains unmodified.

Final export repair: `repair-before-captions.mjs` changes only external caption labels inherited from the after driver, so before recordings accurately describe S1 route takeover. Run it after capture, then rebuild contact sheets. It does not modify app footage, CSS or DOM text. `restore-render.mjs` uses saved explicit render inputs; `render-video.mjs` CLI inference is for initial exports only.

## Round 3 repair and deterministic rebuild

Use `/home/user/wt/pr-33160-cap3-before` at `4c361199f47f6ea32a2c14835df392640d0f7c63` and `/home/user/wt/pr-33160-cap3-after` at `fe3ba61b9a9b166b351087add6e07a7373d075b3`. Run `vp install` in each, then start each with `T3CODE_DEV_SEED=0 vp run dev`. Track the PID/process group you spawn. Read server/web ports from `[dev-runner]`, and wait for the API `Listening` line before minting tokens with `node apps/server/src/bin.ts pair --base-dir <worktree>/.t3`. Use private token logs only for authentication. Wait for navigation to leave `/pair` before continuing.

The scripts in this directory import Playwright 1.60.0 and pngjs from the after checkout. Chromium is `/home/user/.cache/ms-playwright/chromium-1223/chrome-linux64/chrome`, version 148.0.7778.96, Linux x86_64, headless, DPR 1, zoom and fonts 100%, no device/touch emulation. The sole launch argument is `--no-sandbox`, as in round 2. All UI changes are through the real app controls or its real state/RPC commands. App source, CSS, DOM text and screenshot pixels are never edited.

Create `config.json` with the new web/server ports. Set each side's `seedTime` to the current UTC time before seeding, and initially set its `clock` to that same value. Run `SIDE=before PAIR_URL=<private fresh pairing URL> node seed.mjs`, then the corresponding after command. Both servers must start with their own fresh database; never copy a database from another build or from live userdata. Seeding creates a synthetic scratch git project, parent and side chat, and Replay turns through the app's real commands. The private session files stay local.

Launch `node driver.mjs` in an interactive stdin session. Allow 60 seconds for initial Vite/app initialization when the host is busy; a slow empty page is never a capture. Each driver invocation creates a timestamped `run-*` output directory. Commands are JSON lines with a `code` property. This is the exact extension/clock initialization (the async wrapper is required):

```js
await api.login('before');
await api.login('after');
await eval('(async()=>{'+fs.readFileSync(api.ROOT+'/runtime-extensions.js','utf8')+'})()');
await (await import(api.ROOT+'/derive-clock.mjs')).default(api,cfg);
(await import(api.ROOT+'/capture-metadata.mjs')).default(api,cfg);
```

`derive-clock.mjs` reads `ev.entities.readThreadShell(ev.ref('evidence-parent')).createdAt` from each server's own app state, writes it as `parentCreatedAt`, and computes `clock = new Date(Date.parse(parentCreatedAt) + 17_400_000).toISOString()`. The fixed offset is **4h50m**. Always recompute it after seeding; copying this delivery's absolute clock into a new database is incorrect. All new sidebar ages are 4h. The initial R10 pass used the same data-derived method with a 4h20m offset, and was subsequently replaced by the uniform 4h50m pass. Server duration measurement remains live.

Fixture/capture order for this repair: seed history on both sides with `history.mjs.seed(api.forSide(side))`; capture R10; run R11-final.mjs on both sides; run panels-r2.mjs.R20 on both sides; retake R01 and R02 (each module accepts `api.forSide(side), side`). Set `api.currentRow` before every row. The revised R01/R02 explicitly load the real earlier S1 history through the command before centering their original saved quotes. They continue to test the same initial source messages after pagination exists.

A fresh-context pass in `raster-probe.mjs` then recaptured the R11 starting UI with both timelines at their real end positions, no Scroll to end control, and no tooltip. Its ordinary `Page.screenshot` outputs `probe-before-start` and `probe-after-start` are delivered as R11 starts; `R11-start-recapture-provenance.json` records those aliases. The CDP view captures remain diagnostic only. R02 and R18 use the standard screenshot after a one-pixel viewport reflow and return to the declared viewport, followed by the same two-identical-PNG settling requirement. start-recapture-provenance.json records these aliases; all app pixels are untouched. The recording already starts in the same UI/scroll state; the fresh pass removes incidental one-level composer/code-edge rasterization from the still pair.

`start-plan.mjs` refreshes the age-bearing starts without replacing their existing recordings. It uses `start-only.mjs` to run each real fixture setup, capture a settled start, then close its context before the action. For R07 the modifier actions share one pre-action UI state; its canonical fresh start and per-image DOM JSON are copied to each modifier's start label. R08 uses a fresh page in an authenticated context. This avoids treating an old absolute clock as a deterministic seed.

Run the repaired R01/R02 full modules again after the expanded fixture setup, then `history-r2.mjs.capture` for R10 and `panels-r2.mjs.R20` for R20 on both sides. R20 repeatedly checks actual scroller endpoints after virtualization; a single assignment to scrollTop can be superseded by a subsequent virtual-list size correction. The timeline minimap dashes are driven by `data-in-view`, derived from those row/viewport positions, rather than provider turn status. The metadata captures scroller positions and minimap states.

For final start cleanup use `recapture-starts.mjs`: R04 uses the existing S2 fixture, R14 uses its real history fixture before the first request/action, and R15/R16 use controls-recapture.mjs to reuse their existing ordinary source/fork quotes. The start helper centers the quote repeatedly until layout settles; it does not create duplicate fixture turns. Capture-state mismatches outside measured duration boxes must be recaptured, never added to a mask.

Every final still waits for reduced motion, network idle, document.fonts.ready, pointer at a neutral location, focus blur, and no tooltip, then requires two identical PNG byte streams 500ms apart. The per-image `*-duration-masks.json` is a DOM Range around `Worked for N.Ns`; it includes the text and source button HTML. Per-image `*-capture.json` records clock, seed reference, viewport, DPR, theme, scrollers and minimap state. Retained round-2 ordinary control ends and R19's start use their original narrower DOM Ranges over the duration token; their unmasked images are retained unchanged.

Run `finalize-stills.py`, then:

```sh
PYTHONPATH=/tmp/lead-33160-r1/evidence-capture-3/python-deps python3 per-image-diffs.py /tmp/lead-33160-r1/evidence-capture-3/delivery-final
```

The copied Pillow dependency is needed because the system Python lacks Pillow. For an independent machine, install Pillow into a scratch dependency directory and put that directory on PYTHONPATH. `per-image-diffs.py` applies only an image's own recorded duration boxes to that image. It publishes masked start diffs and R15/R16/R17/R20 control-end diffs only. Affected end frames retain only unmasked Diff0/DiffT because the route and panel layout differ by design. Counts come from original RGB differences outside the union of the two independently measured duration regions.

Use `visual-sheets.py` to index every PNG without cropping, inspect each original/corresponding diff, and decode/sample each replacement recording with ffmpeg before upload. `upload-reviewed.py` uses only `ghdrop upload -m` and preserves stdout verbatim. `build-index-round3.py` combines retained round-2 URLs and replacements, with per-image mask tables, pixel counts, complete R01–R20 order and the unchanged supplementary note. No public upload includes pairing tokens, sessions, private server logs or runtime model attribution.

Remaining provenance limitation: retained R19 starts and R15/R16/R17 ends use the original per-image duration-token DOM ranges recorded at capture. A new full Worked-for text/element snapshot was not collected for those retained frames. All other newly captured starts and repaired control ends have full-text ranges and source-element HTML. R15/R16/R17 end originals are pixel-identical even without masking.


## Round 4: mask validity and R20 return preconditions

All reused round-3 inputs were copied to this round's scratch directory before changing anything. Remove exactly the 33 regions in the independent audit's `invalid-mask-regions.json`; they cover occluded content, off-viewport rows, headers or composer controls. Retain an existing DOM text range only where the saved still visibly establishes duration text at that location. All 113 remaining regions were visually inspected in labelled crops of their original PNGs. Empty mask lists are intentional. The retained token ranges on R19 starts remain token-only provenance; R15/R16/R17 ends now have no masks and are pixel-identical unmasked.

Run per-image-diffs.py on the copied delivery after correcting every per-image JSON. Each image receives only its own regions. Every derivative was regenerated; counts measure original RGB pixels outside the union. No original pixels changed.

Create `/home/user/wt/pr-33160-cap4-before` and `/home/user/wt/pr-33160-cap4-after` at the same pinned revisions listed above, wait for checkout completion, run `vp install`, and start with `T3CODE_DEV_SEED=0 vp run dev`. This round's ports are before web 7349/API 15389 and after web 8048/API 16088. Pair after the API Listening line. Use only fresh synthetic Replay data, `seed.mjs`, updated init.js filesystem imports and the data-derived +4h50m clock from round 3.

R20-round4.mjs uses `assistantCitationNavigation(validCitation, sourceThread).hash` on both revisions and explicitly arrives at the parent route with this live hash, then opens the real side-chat pane. This deliberately starts from the same parent/hash precondition on both revisions, rather than retesting Go to source routing. The URL is never stripped in setup. It records the live-hash/open-tab state, closes the tab while retaining the hash, opens Settings with the real sidebar control, and closes Settings with its real Back to app control. It asserts a parent return URL without hash and no reopened side-chat tab. It then re-arrives with the live hash and repeats for Usage. Full URL and panel state are saved in each side's state JSON. External captions show elapsed timestamps and actual routes; only the long base64 citation payload is abbreviated.

Load runtime-extensions.js inside its async evaluation wrapper, capture-metadata.mjs, then visible-masks.js before calling R20-round4.mjs with api.forSide(side). visible-masks.js records viewport-clipped DOM text ranges only when checkVisibility passes and elementFromPoint at the clipped box centre belongs to the source button. App source, CSS and DOM text remain unchanged. Stills retain reduced motion, network idle, loaded fonts and two identical screenshots 500 ms apart.

The Browser panel remained on its splash screen and timed out on two waits. Per the browser fallback rule, this round used the supplied Playwright capture harness. agent-browser was not installed. The first token was consumed by the Browser panel; the seed driver used fresh tokens. Private pairing/session/server files are never uploaded.

R20 final starts were recaptured with R20-start-recapture.mjs in a fresh context at the same parent/hash/open-source/end-of-timelines state. A temporary 1441×900 viewport was returned to 1440×900 before settling. This removed four one-level RGB code-edge rasterization pixels, without editing pixels. R20-start-recapture-provenance.json records the source directories. Final R20 start differs only inside visible duration text, and its end is exactly identical unmasked.
