feat(capture): record the Capture a link Capture came from (ADR-0040 writer)#1995

opened1d agobyMMatt Donovan

Written by claude-opus-5-5 via Claude Code on behalf of Matt.

Part of the MHTML viewer slice 2 plan, Part B step 3, under ADR-0040 (accepted at schema 6 on 2026-10-26). The maintainer approved this step's plan under ADR-0016 on 2026-10-26.

A Capture made with Capture link records, in its signed entry, which Capture held the link, that Capture's Content Hash, and the link's address. The verifier learns the fields in #1976 (pull request #1981). This step writes them: the viewer sends the source and the address, the main process checks the source before signing, and the database, Case Archive, viewer and report carry the relationship. It merges only after the release that carries schema 6.

Agent brief: plan sections, gates, scope, acceptance criteria, verification and starting files

Plan. docs/plans/2026-10-04-mhtml-viewer-slice-2-click-through-and-provenance.md, B1, B2, B4 step 3, B5 and B6; docs/adr/0040-a-link-capture-records-the-capture-it-came-from.md, Decision and Consequences.

Waits for #1981 and the schema-6 release tag before merging. Build it stacked on the open schema-6 pull requests.

Scope. As B4 step 3: RecaptureJob, RecaptureEnqueuePayload and useRecaptureMutations gain linkedFrom (source id and link address, never the hash); recapture.ts accepts a link job only when urls holds exactly one URL equal to the link address, the source exists, is in the job's Case and its capture entry verifies against the chain, and takes linkedFromContentHash from that verified entry; a migration adds linked_from_capture_id, linked_from_content_hash and link_href; CAPTURE_COLUMNS; Case Archive import remaps linked_from_capture_id and the archive version rises; useLinkMenuTarget sends the source and address; CaptureViewer.tsx shows "From a link in <title>"; reportHtml.ts adds the rows. A Recapture of a link Capture does not inherit the fields.

Three requirements from the review of #1981:

  1. The writer signs without a schema check, so it refuses a source whose Content Hash is not lower-case SHA-256 hex rather than copying it into a signed entry the verifier would reject.
  2. The Shared Case receive path (src/main/services/sharing/receivedEvidence.ts) builds capture rows without the three fields; it carries them, so a teammate's viewer shows "From a link in".
  3. The step settles, and documents in the plan, whether linkHref is the address before or after canonicalizeUrl, and the single-URL check compares like with like.

Acceptance criteria (B5)

  • tests/main/services/recapture.test.ts covers a link job that writes the three fields; a source in another Case, deleted, or failing verification (rejected); urls empty, holding two addresses, or differing from the link address (rejected); an ordinary job that writes none of them; and a source with a non-hex Content Hash (rejected).
  • A lifecycle test pins the entry body with and without the fields, and parses it with the strict schema.
  • The migration test and the CAPTURE_COLUMNS drift test pass.
  • tests/main/services/caseArchiveRoundTrip.test.ts exports and imports a link Capture and shows linked_from_capture_id remapped, not copied; the archive version pin moves and the newer-version refusal cases still pass.
  • A Shared Case receive test shows the three fields on the received row.
  • MhtmlViewer.test.tsx pins the enqueue payload from the menu; the report test pins the new rows.
  • The e2e spec captures a link the Case does not hold and reads the new Capture's "From a link in" header.

Verification

The tests above, pnpm preflight, and xvfb-run -a pnpm test:e2e e2e/mhtml-link-interactivity.spec.ts.

Starting files. src/main/services/recapture.ts, src/main/services/captureLifecycle.ts, src/main/services/db/migrations.ts, src/main/services/db/captureRepo.ts, src/main/services/caseArchive.ts, src/main/services/sharing/receivedEvidence.ts, src/main/services/reportHtml.ts, src/renderer/components/captures/CaptureViewer.tsx, src/renderer/components/captures/useLinkMenuTarget.ts.

Evidence-affecting: yes (acquisition, manifests, storage, reporting; blocking tier).