Skip to content

ID-117 {117.4} PLAN — Unified document/revision diff surface (decomposition of the ratified PRODUCT+TECH into implementation Subtasks across clusters A–E)

ID-117 {117.4} PLAN — Unified document/revision diff surface

Section titled “ID-117 {117.4} PLAN — Unified document/revision diff surface”

Inputs (read in full): ratified {117.2} PRODUCT.md (20 Behavior invariants, clusters A–F), ratified {117.3} TECH.md (1:1 Proposed-change-per-invariant, clusters A–E + F, RETIRE migration plan), and {117.1} RESEARCH.md (CORRECTED S384). This PLAN decomposes all of clusters A–E (cluster F wiring folds into cluster A’s container slice) into 9 implementation Subtasks for the Executor wave. Authored by a FRESH Planner (Q-PLANNER-2) running planning-and-task-breakdown.

REV S391 (this revision) — 5 staff-review edits + 2 Liam decisions, applied WITHOUT touching the APPROVED spine (UnifiedRevision/UnifiedDiff type+adapter, option-C binary leg, gated source_document_diffs DROP, api-view regen + task-view re-vendor). Summary of what changed vs the 8-Subtask draft (full rationale in §8 Revision log):

  • item 3 absorbs jscpd CMP-2 — additionally extracts lib/diff/line-diff.ts (the shared computeLineDiff/buildLcsTable/OP_CLASS/OP_PREFIX) and re-points PromptDiffView. ID-117 is the single owner of lib/diff/. jscpd Wave-3 S2 (CMP-2) is struck in favour of this (parent records that roadmap edit on main).
  • item 7 WIDENED — retirement reach now enumerates the full diff_availablereupload-banner feature chain (a SECOND dangling affordance INV-15 did not name) and is sequenced AFTER a new gated investigation Subtask.
  • NEW item 8 (investigation) — Liam decision: investigate whether the re-ingest notification path (sendSourceDocumentUpdateNotifications, independent of the diff display) is live/used, BEFORE the DROP. Retire-vs-rehome of that path is DEFERRED to this finding; the DROP must not proceed past it unresolved.
  • item 9 (was item 8) deletion list WIDENED — adds e2e/tests/document-diff.spec.ts + the unit tests referencing source_document_diffs so item 9’s own acceptance grep passes.
  • ID-121 sequencing note added (§7) — CodeQL must defer/dismiss source-document/* diff-file findings as “retired by ID-117”.
  • UnifiedDiff type nit noted in item 1 — derive renderMode from viewDepth, don’t store both free (avoids illegal combos).

Resolved gates carried into this PLAN (no re-litigation):

  • OQ-117-1 → RETIRE (Liam Decision C / S388): source_document_diffs is 0 rows in the CLIENT DB too; the re-ingest review workflow is RETIRED, not re-homed. The drop gate is CLEARED — cluster E proceeds; no RE-HOME review table, no apply/dismiss affordance. The RE-HOME addendum in TECH §6 is moot. NOTE (REV S391): OQ-117-1 retired the diff review workflow; the re-ingest notification path is a SEPARATE behaviour the new item 8 investigation resolves (see §8).
  • OQ-117-2′ → option C (visual-compare-only + alongside text summary for v1; change-overlay → v1.1). Honoured in the binary-pane slice; not re-opened.
  • OQ-117-3 → content_items.content via content_history canonical for the markdown depth; extracted_text legacy (binary-leg text fallback only). Resolved in TECH §4.
  • OQ-117-4 → synthesised changeType:'reingest' / 'initial_ingest', editIntent:null. TECH §4.
  • OQ-117-5 → TECH default (binary at the rebuilt /documents/[id]/diff; markdown/user-edit inline) — adopted as this PLAN’s IA, folded into the container slice (item 6).
  • OQ-117-6 → reference page OUT of scope (no reference_history to diff).

1. Context + code-intelligence orientation (cited verbatim)

Section titled “1. Context + code-intelligence orientation (cited verbatim)”

ID-117 builds ONE unified diff/revision surface over a common revision abstractionUnifiedRevision / UnifiedDiff (a TYPE + adapter layer generalising the landed RevisionBlob, NOT a DB schema change) — at three view-depths (binary / canonical-markdown / user-edit), and retires the 0-row source_document_diffs engine. The architectural spine: establish the type+adapter spine first (items 1–2), extend the shared RevisionDiffView engine with render modes and extract the shared LCS primitive into lib/diff/line-diff.ts (item 3, REV S391: absorbs CMP-2), install + compose the binary leg (items 4–5), wire the container + entry points (item 6), investigate the re-ingest notification path’s fate (item 7, gated), then retire source_document_diffs code couplings (item 8) and drop the table + re-vendor (item 9).

Orientation tooling note: gitnexus MCP tools were not exposed in this Planner’s tool surface, so per the binding code-intel fallback (gitnexus → ccc → grep) the citations below were INDEPENDENTLY re-verified by grep against the worktree (source_document_diffs is a DB table + several couplings are in .ts/.sql outside the TS graph). The PRODUCT/TECH gitnexus citations (S384/S385) are reproduced and confirmed; the REV S391 widen citations were freshly grep-verified:

  • RevisionDiffView (gitnexus_context, TECH §1): components/item-detail/revision-diff-view.tsxexport function RevisionDiffView( confirmed @229 (grep); export interface RevisionBlob confirmed @31 (grep). Incoming calls: exactly TWOCompareVersionsPanel (components/item-detail/version-history.tsx, function CompareVersionsPanel confirmed @92) and QARevisionHistory (components/qa/qa-revision-history.tsx). grep for RevisionDiffView across components/ returns exactly these two callers + the definition file. gitnexus_impact (TECH §1): risk LOW, direct 2, processes 0, modules 1. The two-caller seam is the central extension point.
  • Shared LCS primitive (REV S391 — CMP-2, grep-verified): computeLineDiff/buildLcsTable/ OP_CLASS/OP_PREFIX are duplicated verbatim in exactly two files — components/item-detail/revision-diff-view.tsx (buildLcsTable@70, computeLineDiff@94, OP_CLASS@139, OP_PREFIX@146) and components/intelligence/prompt-refinement/prompt-diff-view.tsx (buildLcsTable@40, computeLineDiff@65, OP_CLASS@117, OP_PREFIX@123). grep for computeLineDiff/ buildLcsTable across components/+lib/ returns ONLY these two files → ID-117 is the correct single owner of the extracted lib/diff/line-diff.ts.
  • CompareVersionsPanel — confirmed components/item-detail/version-history.tsx:92 (definition), rendered @387. Reads content_history only (header comment :88–90 confirmed).
  • source_document_diffs (DB table — grep sweep, TS+SQL+Py): couplings confirmed at lib/source-documents/source-document-impact.ts (@69 read, @141 write), lib/mcp/tools/content.ts (@1994/@2118, get_document_diff def @1962), app/api/source-documents/[id]/diff/route.ts (@75/125/183/412/490/552/586/607), app/documents/[id]/diff/page.tsx (@56–266), scripts/export-user-data.ts (type @496, read @647, assign @691), scripts/generate-api-views.ts:**118** (the mirrored-table array — confirmed), components/source-document/source-document-history.tsx:**256** (compare link → /documents/{id}/diff), hooks/use-diff-review.ts (review hook). SQL: supabase/migrations/20260617130000_squash_baseline.sql.
    • ⚠ FULL upload→banner feature chain (REV S391 — widened item-8 retire scope; freshly grep-verified): The upload re-ingest block app/api/upload/route.ts:903–969 dynamically imports computeDocumentDiff (@906) + analyseDocumentImpact (@908), INSERTs into source_document_diffs @943, sets diffAvailable=true @945, runs analyseDocumentImpact @948, conditionally sends notifications via sendSourceDocumentUpdateNotifications (dynamic import @955, call @957), and the response sets diff_available: true @1057. That flag flows: route.ts:1057hooks/use-file-upload-pipeline.ts:62 (diffAvailable?) / :354 (diffAvailable: data.diff_available ?? false) → components/create-content/upload-tab-content.tsx:677–683 (<ReuploadBanner … diffAvailable … diffDocumentId={…newDocumentId} />) → components/source-document/reupload-banner.tsx:20/45 (props)
      • :93–101 renders a live <Link href={/documents/${diffDocumentId}/diff}> “Review Q&A changes”. This is a SECOND dangling affordance to the deleted page that INV-15 does NOT name (INV-15 names only source-document-history.tsx:256). Item 8 (RETIRE) must clean up this whole chain so neither entry point 404-gaps. This is a LIVE WRITE path: dropping the table while the INSERT @943 + the document-diff.ts import remain breaks the upload flow at runtime AND breaks bun run build.
    • ⚠ Notification path (REV S391 — gated, item 7): the SAME block drives analyseDocumentImpact
      • sendSourceDocumentUpdateNotifications — a re-ingest notification feature (lib/source-documents/ source-document-notifications.ts, export … sendSourceDocumentUpdateNotifications @25) that is INDEPENDENT of the diff display. Its only non-test caller is the upload route. The DROP would silently kill this notification behaviour, which no invariant acknowledges. Liam decision: investigate live usage FIRST (item 7) — retire-vs-rehome of the notification path is DEFERRED to that finding.
  • generate-api-views.ts'source_document_diffs' confirmed in the mirrored-table array at scripts/generate-api-views.ts:**118** (between signup_policy and source_documents). Dropping the table → this entry is removed → the api. view-set regenerates → task-view-vendor-drift / schema-parity fire → re-vendor REQUIRED (item 9).
  • RevisionBlobinterface RevisionBlob @31; becomes a structural subset of UnifiedRevision (TECH §1.1); the widen is non-breaking (additive optionals binary/recordKind).

Substrate facts (grep-confirmed): storage bucket = documents (app/api/upload/route.ts:378 .from('documents'), storage_path @430); signed-URL template = createSignedUrl(path, 300) (app/api/procurement/[id]/tender/download/route.ts:67-68); PDF client viewer present (components/reader/pdf-viewer.tsx:4 import { Document, Page } from 'react-pdf'); diffWords primitive live (components/item-detail/version-diff.tsx:18); components.json has no registries block (only style/aliases — extend-ui install must add it).

  • Spine = UnifiedRevision TYPE + adapter layer (NOT a DB change). One type generalises RevisionBlob; three adapters (content-item, qa-pair, source-document) project each substrate into it; the three view-depths all project through this one revision abstraction. The ONLY DB change in the whole Task is the source_document_diffs DROP (cluster E).
  • UnifiedDiff type-design nit (REV S391, type-design note in item 1). viewDepth and renderMode on UnifiedDiff are partly redundant and admit illegal combos (e.g. viewDepth:'binary' + renderMode:'word-inline'). Derive renderMode’s default FROM viewDepth (binary→binary-split, canonical-markdown/user-edit→unified-line, overridable to side-by-side/word-inline only for the content-item depths) rather than carrying both as independent free fields — or constrain via a discriminated union. This is a type-shape refinement on the spine, not a spine change.
  • lib/diff/ has ONE owner: ID-117 (REV S391). ID-117 hosts unified-revision.ts (item 1), adapters/ (items 1–2), and line-diff.ts (item 3, extracted from the shared LCS primitive jscpd CMP-2 named). jscpd Wave-3 S2 is struck. revision-diff-view.tsx AND prompt-diff-view.tsx both consume the extracted primitive — the duplication CMP-2 flagged is killed inside ID-117’s churn window instead of by a separate Wave-3 PR re-touching a file ID-117 just rewrote.
  • Extend, don’t replace, RevisionDiffView. New renderMode prop (default derived as above → byte-identical user-edit output, INV-12). Side-by-side + word-inline are render-mode additions, not a new engine. The binary leg is a NEW composition alongside the engine, with the text line-diff shown alongside (option C).
  • Cluster E is THREE slices (notification investigation, code-deletion, then DB-drop) in strict order to respect the type-regen ordering trap (TECH §6) AND the new notification gate: item 7 resolves the notification path’s fate (gate); item 8 removes all code couplings INCLUDING the full upload→banner chain + export-user-data.ts:496’s type ref; item 9 then drops the table, regens types, regens api-views, and re-vendors. Reversing the order breaks bun run build on regen; bypassing item 7 silently kills a notification feature.
  • Unify legs (A–D) are INDEPENDENT of cluster E (TECH §6) — they share no file with the retire slices except the rebuilt /documents/[id]/diff route (item 6 ↔ item 8 coordinate the rebuild-vs-delete boundary). The dep graph sequences accordingly so the drop lands after the new surface preserves the leg-1 use-case.
item 1 UnifiedRevision type (+renderMode-from-viewDepth) + content-item & qa-pair adapters (cluster A) deps: none
├── item 2 source-document adapter + binary-url signed route (cluster A→B) deps: 1
│ │
├── item 3 RevisionDiffView renderMode + extract lib/diff/line-diff.ts (cluster C+D, CMP-2) deps: 1
│ │ + re-point PromptDiffView
│ │
item 4 extend-ui-kit registry install (DOCX/XLSX; PDF=react-pdf) (cluster B) deps: none (parallel)
│ │
└──────┴── item 5 binary-diff-pane compose + INV-6 fallback (cluster B) deps: 2,3,4
item 6 UnifiedDiffContainer + entry-point wiring (cluster F+A) deps: 3,5
item 7 INVESTIGATE re-ingest notification path (GATE) (cluster E) deps: 6
│ → retire-vs-rehome DECISION (Liam, deferred to finding)
item 8 RETIRE legacy code/UI/MCP + full upload→banner chain (cluster E) deps: 7
item 9 drop migration + type/api-view regen + re-vendor (cluster E) deps: 8

Item 4 (install) is parallelisable with items 1–3. Cluster E (7→8→9) is strictly last and strictly ordered; item 8 (the DROP code-deletion) must NOT proceed past item 7 without resolving the notification path’s fate (Liam-gated). 9 Subtasks — well under the 25-Subtask soft ceiling. All dependencies are sibling-only (ID-117 internal); no cross-Task dependency surfaced — no escalation required. (The ID-121 CodeQL overlap in §7 is a sequencing note, not a Subtask dependency.)

4. Subtask list (cluster → invariants → verification)

Section titled “4. Subtask list (cluster → invariants → verification)”
#TitleClusterSpec slicesDepsAcceptance (testStrategy)
1UnifiedRevision type (renderMode-from-viewDepth) + content-item/qa-pair adaptersAINV-1/2/3, TECH §1.1New adapters yield well-formed UnifiedRevision from fixtures; depth→recordKind correct; renderMode default derived from viewDepth (no illegal combo); no >2-rev path; no diff write; bun run test+build green
2source-document adapter + signed-URL binary-url routeA→BINV-4/8, OQ-117-41Route: unauth→authFailureResponse, cross-workspace row→no URL, success→300s URL on documents; adapter synthesises reingest/initial_ingest provenance
3RevisionDiffView renderMode + extract lib/diff/line-diff.ts + re-point PromptDiffView (CMP-2)C+DINV-10/11/12/13, jscpd CMP-21INV-12 regression: existing suite passes UNCHANGED, callers byte-identical; line-diff extracted to lib/diff/line-diff.ts, BOTH RevisionDiffView+PromptDiffView consume it; new modes render diffWords + semantic tokens
4extend-ui-kit registry install (DOCX/XLSX viewers)BINV-5, Empiricalcomponents.json registry added; DOCX/XLSX viewers import clean vs React 19.2.6/Next 16.2.6; dated re-verification line recorded; build green
5binary-diff-pane compose + INV-6 fallback + text summaryBINV-5/6/7 (OQ-117-2′=C)2,3,4PDF/DOCX/XLSX→correct viewer; unsupported/signed-URL-fail→fallback notice (not blank); text summary alongside; NO binary inline-marking
6UnifiedDiffContainer + entry-point wiringF+AINV-1/17/18/19/203,5Binary at /documents/[id]/diff, md/user-edit inline; read-only, NO apply/dismiss (RETIRE); workspace-scoped; no AI labelling; INV-12 preserved
7INVESTIGATE re-ingest notification path (live-usage finding + retire-vs-rehome decision) — GATEEINV-15 (gate), A16Finding documents whether sendSourceDocumentUpdateNotifications is live/used (callers, telemetry, Liam input); retire-vs-rehome decided + recorded; DROP (item 8) blocked until resolved
8RETIRE legacy code/UI/MCP + full diff_available→reupload-banner chain + GDPREINV-14/15/16 (RETIRE), A1/A27grep source_document_diffs → zero non-migration/non-comment TS hits; reupload-banner diff-link + diff_available plumbing removed; both entry points 404-gap-free; notification path handled per item-7 decision; build green
9drop migration + type/api-view regen + task-view re-vendor (+ test/e2e deletion)EINV-14/16 (RETIRE), A28View-before-table drop; e2e/document-diff.spec.ts+4 unit tests removed; types regen clean; api-view excludes view; task-view-vendor-drift/schema-parity GREEN after re-vendor; build+test green

Full load-bearing details per Subtask (file lists, line anchors, deletion lists, gitnexus discipline reminders, the full upload→banner chain, the notification-gate decision record) are carried in the dispatch briefs the Orchestrator forms from this PLAN — not duplicated here.

4.1 Widened item-8 deletion set (REV S391 — full upload→banner feature chain, A1)

Section titled “4.1 Widened item-8 deletion set (REV S391 — full upload→banner feature chain, A1)”

Item 8 must delete/clean the WHOLE chain, not just the upload/route.ts:943 INSERT the prior draft caught:

  • app/api/upload/route.ts:903–969 — remove the re-ingest diff block (the computeDocumentDiff + source_document_diffs INSERT @943); item 7’s decision governs whether the analyseDocumentImpact/sendSourceDocumentUpdateNotifications lines survive (rehome) or go (retire). Remove the diff_available: true response field @1057 if diffAvailable is no longer set.
  • hooks/use-file-upload-pipeline.ts:62 (diffAvailable? field) + :354 (diffAvailable: data.diff_available ?? false) — remove the plumbing.
  • components/create-content/upload-tab-content.tsx:682 (diffAvailable={…} prop) + :683 (diffDocumentId={…newDocumentId}) — remove the props passed to <ReuploadBanner>.
  • components/source-document/reupload-banner.tsx:20/45 (diffAvailable/diffDocumentId props) + :93–101 (the live <Link href={/documents/${diffDocumentId}/diff}> “Review Q&A changes”) — remove the dangling affordance (the SECOND entry point INV-15 did not name).
  • The INV-15-named compare link components/source-document/source-document-history.tsx:256 (already in scope).

4.2 Widened item-9 deletion set (REV S391 — test/e2e surface, A2)

Section titled “4.2 Widened item-9 deletion set (REV S391 — test/e2e surface, A2)”

Item 9’s own acceptance grep (“zero non-migration TS hits for source_document_diffs”) is tripped by the test/e2e files unless they are deleted in the same slice. Add to item 9’s deletion set (named in RESEARCH §6, absent from the prior per-subtask acceptance):

  • e2e/tests/document-diff.spec.tsseeds the table, so it hard-fails post-drop (not merely a dangling reference). Delete.
  • Unit tests referencing source_document_diffs / the retired engine: __tests__/mcp/document-diff-tool.test.ts, __tests__/lib/source-documents/document-diff.test.ts, __tests__/api/source-documents-diff.test.ts, __tests__/components/source-document-diff-review.test.tsx, plus the use-diff-review/reupload-banner/source-document-history test assertions that reference the deleted couplings. (Executor runs the full grep at item-9 start to enumerate the exact set; the acceptance gate is zero non-migration hits.)
  • After items 1–3 (spine + engine + CMP-2 extraction): bun run test green incl. the INV-12 regression suite UNCHANGED; bun run build clean; UnifiedRevision adapters + render modes unit-tested; lib/diff/line-diff.ts extracted, BOTH RevisionDiffView and PromptDiffView consume it (CMP-2 resolved — jscpd Wave-3 S2 confirmed struck). Review before the binary leg.
  • After items 4–6 (binary leg + container): binary depth renders at /documents/[id]/diff; INV-6 fallback verified (no blank panel); all three depths back onto the one engine; read-only roles enforced. The new surface PRESERVES the leg-1 use-case — prerequisite for the drop.
  • After item 7 (notification GATE): the re-ingest notification path’s live-usage finding is recorded and a retire-vs-rehome decision is made (Liam) and documented. Item 8 MUST NOT start until this gate resolves. This is the explicit forcing function the A1 review demands.
  • After items 8–9 (retirement): zero source_document_diffs references survive (grep/ast sweep incl. the full upload→banner chain + test/e2e files); the notification path is handled per the item-7 decision; table+view dropped; types/api-views regenerated; re-vendor leaves the side-workflows GREEN; GDPR export runs clean. Task close.
RiskImpactMitigation
Notification path silently killed by the DROP (REV S391, A1)sendSourceDocumentUpdateNotifications is independent of the diff display; the prior draft’s item-7 deletion would have removed it with no decisionHighNEW item 7 gate: investigate live usage FIRST; retire-vs-rehome decided + recorded BEFORE item 8; item 8 cannot proceed past it unresolved.
Second dangling affordance (REV S391, A1)reupload-banner.tsx:93 “Review Q&A changes” link to the deleted /documents/{id}/diff page, NOT named by INV-15HighItem 8 deletion set §4.1 enumerates the full diff_available→banner chain (route→hook→tab→banner); both entry points must be 404-gap-free.
item-9 acceptance grep tripped by test/e2e files (REV S391, A2)e2e/document-diff.spec.ts SEEDS the table → hard-fail post-dropMedItem 9 deletion set §4.2 adds the e2e spec + the 4 unit tests so the acceptance grep passes.
Upload-route write coupling (upload/route.ts:903-969) missed → drop breaks re-ingest at runtime + buildHighSurfaced + line-anchored in §1 / §4.1; folded into item 8’s explicit deletion list.
Type-regen ordering trap (drop before removing export-user-data.ts:496) → build break on regenHighStrict dep 8→9; item 8 removes the export type ref BEFORE item 9’s regen (TECH §6).
lib/diff/ two-owner collision with jscpd Wave-3 S2 (REV S391, A3/C)MedID-117 absorbs CMP-2 (item 3 extracts line-diff.ts); jscpd Wave-3 S2 struck (parent records the roadmap edit on main); lib/diff/ has one owner.
RevisionBlobUnifiedRevision widen regresses the 2 callersMedINV-12 regression gate (existing suite passes unchanged) + structural-subset design + LOW gitnexus_impact (2 callers) — run impact before the widen (item 1/3).
extend-ui DOCX/XLSX viewer fails to install vs React 19.2.6/Next 16.2.6MedItem 4 re-fetches the registry + records a dated empirical line; on fail, escalate; INV-6 text fallback covers a missing viewer gracefully (never blank); PDF uses the working react-pdf.
Re-vendor step skipped → task-view-vendor-drift/schema-parity red in CIMedItem 9 makes re-vendor an explicit, verified step (INV-16); checkpoint gates on the side-workflows green.
binary-url route added to proxy.ts publicRoutes by mistakeLowItem 2 details explicitly: auth-gated route stays OFF the allowlist (TECH §6 inverse-mistake note).

7. ID-121 (CodeQL baseline) sequencing note (REV S391, edit B — non-blocking)

Section titled “7. ID-121 (CodeQL baseline) sequencing note (REV S391, edit B — non-blocking)”

ID-121’s CodeQL resolution waves must defer/dismiss the source-document/* diff-file findings as “retired by ID-117” — do NOT fix code ID-117 deletes. Concretely:

  • js/implicit-return at components/source-document/source-document-diff-review.tsx:84 (getDiffTypeBadgeClasses) — this file is DELETED by ID-117 item 8. A CodeQL fix here is wasted work on a file about to be removed; ID-121 should dismiss/defer it as “retired by ID-117”.
  • js/mixed-returns at components/source-document/source-document-history.tsx:69 — ID-117 item 8 modifies this file (removes the compare link @256). Don’t both touch: ID-121 defers, or sequences after ID-117 cluster E.
  • js/mixed-returns at components/reader/pdf-viewer.tsx:171 — ID-117’s binary leg may reuse this (OQ-117-7 PDF path); low-risk, but coordinate so neither stream churns it twice.

ID-121’s genuine-fix core (lib/validation/index.ts:111-120, lib/attention.ts:368, lib/reorient.ts:485, error-boundary.tsx:24) is disjoint from ID-117 — the two tasks are ~95% independent; only the 3 diff/source-document files above need a “don’t both touch” note. This is a SEQUENCING note for the Orchestrator/ID-121, NOT an ID-117 Subtask dependency.

8. Revision log (REV S391) + open questions carried to implementation (non-blocking)

Section titled “8. Revision log (REV S391) + open questions carried to implementation (non-blocking)”

REV S391 — what changed vs the 8-Subtask draft (5 staff-review edits + 2 Liam decisions):

  1. A1 [HIGH] — item 8 (RETIRE) widened + NEW item 7 (notification gate). The prior draft caught only the upload/route.ts:943 INSERT. The full reach (§1, §4.1) is the diff_availableuse-file-upload-pipeline.tsupload-tab-content.tsxreupload-banner.tsx:93 “Review Q&A changes” link — a SECOND dangling affordance to the deleted page that INV-15 does not name. The same block drives analyseDocumentImpact + sendSourceDocumentUpdateNotifications — a re-ingest notification feature independent of the diff display that the DROP would silently kill. Liam decision: investigate live usage FIRST — NEW item 7 (its own Subtask, sequenced before the DROP). Retire-vs-rehome of the notification path is DEFERRED to that finding; item 8 must not proceed past item 7 unresolved (gated).
  2. A2 [MED] — item 9 deletion list completed (§4.2). Added e2e/tests/document-diff.spec.ts (which SEEDS the table → hard-fails post-drop) + the unit tests referencing source_document_diffs (named in RESEARCH §6) so item 9’s own acceptance grep passes.
  3. Nit — UnifiedDiff type fixed (item 1, §2). viewDepth + renderMode were partly redundant and admitted illegal combos; derive renderMode’s default FROM viewDepth (or discriminated union) rather than store both free.
  4. B — ID-121 sequencing note added (§7). ID-121 should defer/dismiss the source-document/* diff-file findings (e.g. js/implicit-return @ source-document-diff-review.tsx:84) as “retired by ID-117”; don’t fix code ID-117 deletes.
  5. C — jscpd CMP-2 folded into item 3 (Liam-approved). Item 3 additionally extracts lib/diff/line-diff.ts (the shared computeLineDiff/buildLcsTable/OP_CLASS/OP_PREFIX used by BOTH revision-diff-view.tsx AND prompt-diff-view.tsx) and re-points PromptDiffView. ID-117 is the single owner of lib/diff/. jscpd Wave-3 S2 (CMP-2) is struck in favour of this (the parent records that roadmap edit on main). CMP-1 (PDF clone) is adjacent — coordinate the OQ-117-7 PDF path so ID-117 doesn’t add a third PDF consumer while Wave-3/S1 extracts pdf-document.tsx.

The APPROVED spine is UNCHANGED: UnifiedRevision/UnifiedDiff type+adapter, option-C binary leg (visual-compare v1 + alongside text, overlay v1.1), gated source_document_diffs DROP, api-view regen

  • task-view re-vendor. None of the 5 edits touch the spine — they tighten the blast radius the spine already implies.

Open questions carried to implementation (non-blocking):

  • OQ-117-7 (PDF viewer choice) — item 4 default: reuse react-pdf for PDF, extend-ui only for DOCX/XLSX. Confirm at install time. (Coordinate with jscpd CMP-1 per edit 5.)
  • Item 6 ↔ item 8 boundary — whether item 8 deletes app/documents/[id]/diff/page.tsx and item 6 rebuilds it, or item 6 owns the full replace. Either is valid; the route must never 404-gap. Executor picks the lower-churn boundary; flagged in both Subtasks’ details.
  • OQ-NOTIF (item 7 output) — the retire-vs-rehome decision for the re-ingest notification path is deferred to item 7’s finding; item 8’s handling of analyseDocumentImpact/ sendSourceDocumentUpdateNotifications follows that decision.

End of PLAN (REV S391). Decomposes the ratified PRODUCT+TECH into 9 sibling-ordered Subtasks across clusters A–E (cluster F folded into the container slice). OQ-117-1 RETIRE / OQ-117-2′ option-C / OQ-117-3/-4/-5/-6 dispositions honoured (not re-litigated). 5 staff-review edits + 2 Liam decisions applied without touching the APPROVED spine: item 3 absorbs jscpd CMP-2; item 8 (RETIRE) widened to the full upload→banner chain; NEW item 7 gates the re-ingest notification path; item 9 deletion list widened to the test/e2e surface; ID-121 sequencing note added; UnifiedDiff type nit noted. Code-intel orientation re-verified by grep (gitnexus fallback). No cross-Task dependency — no escalation. No implementation, no DDL, no ledger writes, no commits performed.