Skip to content

ID-117 {117.1} RESEARCH — Unified document/revision diff surface (binary + canonical-markdown + user-edit) over extend-ui viewers

ID-117 {117.1} RESEARCH — Unified diff/revision surface (3 view-depths over a common revision abstraction)

Section titled “ID-117 {117.1} RESEARCH — Unified diff/revision surface (3 view-depths over a common revision abstraction)”

Supersedes the S383 draft; grounded on the S380 decision (transcript dc46727f) + the ID-117 ledger framing. The S383 draft drifted: it treated extend-ui-kit’s not-yet-installed status as a blocker and recommended “markdown side-by-side v1 + binary render v1.1” (the OQ-117-2 fork). That contradicts S380 — where Liam locked extend-ui-kit as the binary-leg approach and made the single coherent surface (with the binary leg) the v1 target. This rewrite preserves the S383 file:line findings (re-verified against current main, S384), fixes the extend-ui framing, and kills the deferral fork. It also adds the reference-page overlap (S383 omitted it) and an empirically confirmed 0-row count for the retirement target (S383 could not run it).

Status: RESEARCH complete — recommendations PENDING Liam (§10 decision queue). RESEARCH-only — no implementation, no DDL, no code edits, no ledger writes.

Task: ID-117 (Track C, S380, spec_needed, priority high, 0 deps) — “Unified document/revision diff surface (binary + canonical-markdown + in-platform user-edit) over extend-ui viewers”. Promoted from bl-267 (S243-RATIFIED diff-UI retention). NOT an ID-45 gate (render-layer, droppable legacy leaf). Retires source_document_diffs as part of this Task.

Companion artefacts (inputs, not replacements): ID-59’s PRODUCT.md ({59.2}) and TECH.md ({59.3}) — the user-edit Diff-UI (INV-14..INV-17) that ID-117 builds on top of. ID-59’s RevisionDiffView IS the source-agnostic engine ID-117 extends (§3.3).


0. What this rewrite corrects (the S383 → S380 reconciliation)

Section titled “0. What this rewrite corrects (the S383 → S380 reconciliation)”
The S383 draft saidThe S380 decision (authoritative)This rewrite
extend-ui-kit is “ABSENT” → a blocker → recommend markdown-side-by-side v1, binary render deferred to v1.1 (§5(C), §7.4, OQ-117-2)extend-ui-kit is the LOCKED approach for the binary leg (Liam, S380): “Build the surface with extend-ui-kit Document Split Viewer. Single coherent diff surface.” Binary is v1.extend-ui-kit absence is a build prerequisite (a shadcn-registry install), not grounds to defer. The binary leg ships in v1 on extend-ui viewers (§5).
Binary leg is “largely greenfield UI — either adopt extend-ui-kit or build on installed primitives” — framed as an open forkThe fork is already resolved: extend-ui-kit, because “most of our client’s content will likely be binary, before our pipeline converts it” (S380).The installed primitives (react-pdf etc.) are a partial fallback (PDF only); extend-ui-kit is the chosen substrate (§5).
Leaned on the S243 “markdown-first / markdown-sidecar” lineageS380 supersedes the markdown-first framing with “unify on extend-ui viewers”The S243 markdown-sidecar framing is cited only as superseded (§1).
(omitted)NEW: extend-ui’s “Document Split Viewer” is a packet-splitter (“review large document packets as separate files”), not a two-version comparator — so the binary version-diff is composed from extend-ui file-viewers, not delegated to one component (§5, OQ-117-2′). A PRODUCT clarification, not a deferral.
source_document_diffs 0-rows claim “inherited, not verified”NEW: empirically CONFIRMED 0 rows on the dev/platform DB (zjqbrdctesqvouboziae), this RESEARCH (§6, §8).
(omitted)NEW: the reference detail page (app/reference/[id]) assessed — not a current consumer; future seam only (§7).

The S383 evidence base (the three-surface map, the UnifiedRevision abstraction, the retire-list, the installed-primitive inventory) was sound and is preserved — re-verified against current main (HEAD includes the ID-111 reference fix 913effe8), with drifted line numbers corrected.


1. The grounding decision (S380, transcript dc46727f)

Section titled “1. The grounding decision (S380, transcript dc46727f)”

S380 (2026-06-20, 532 events; ID-117 session_refs: ["ca-s380"]) is the canonical decision source — no ca-s380 continuation prompt was written (the session hit the S381 API-error interrupt), so the decision survives only in the raw transcript. Verbatim Liam turns:

  • “is it feasible to create a single coherent diff surface which includes all three concepts (the table is empty as this is a dev DB — and even in the client DB, the diff engine was newly built prior to the pivot, so will also be empty — app hasn’t launched yet).”

  • extend-ui simplifies this to some degree, with its binary viewers — keeping in mind that most of our client’s content will likely be binary, before our pipeline converts it.

  • “Drop the 0-row source_document_diffs table + document-diff.ts engine … Build the surface with extend-ui-kit Document Split Viewer. Single coherent diff surface.

  • Locked: document UI (diff / Q&A / file-tree) uses extend-ui-kit open-source components; substantial unspecced work → research-first. Action taken: promote bl-267 → Task 117 (Track C diff surface).

The framing (matches the ID-117 ledger description — authoritative): ONE unified diff/revision surface; three view-depths over a common revision abstraction:

  1. Binary source diff — docx/pdf/xlsx (what the client actually edits; most content is binary pre-pipeline) via extend-ui-kit DOCX/PDF/XLSX viewers (the “Document Split Viewer” family).
  2. Canonical-markdown revision diff — the post-Docling markdown projection.
  3. In-platform user-edit diff — over content_history / q_a_pair_history (the live {59.12} RevisionDiffView).

Aligns with files-are-truth “one record, many views” (#1) + “observe and intervene” (#3). Retire source_document_diffs (0 rows dev — §6, §8). No new DB table.

Superseded predecessor framing (cite, do not follow): bl-267 / S243 ratified “Diff-UI RETAINED for v1, re-pointed OFF source_document_diffs ONTO a markdown sidecar” (id-31-canonical-pipeline- implementation-plan/PLAN.md:21, :575 “Diff-UI retire scope RATIFIED-S243”). The historical markdown-first / V1.1-deferral notes (archive decision-graph.md, project-plan.md §8, deferral-register.md; ID-59-era “markdown-first”) pre-date the S380 unify-on-extend-ui call and are SUPERSEDED by it. They are not current intent and must not drive PRODUCT.


  • gitnexus (repo:'canonical'): query('document revision diff comparison version history') surfaced the legacy binary-diff flow (app/api/source-documents/[id]/diff/route.tsdocument-diff.ts), the Q&A history GET, and the standalone diff/revision components. context({name:'RevisionDiffView'}) returned exactly two incoming callers (CompareVersionsPanel, QARevisionHistory) — the central extension seam.
  • Three parallel survey agents (S384): extend-ui-kit live state · the three surfaces (re-verify file:line vs current main) · the reference-page overlap. All file:line below corrected to current tree.
  • Direct DB verification (Supabase MCP, dev/platform zjqbrdctesqvouboziae): row counts for the retirement target + sibling history tables (§8).

This Task is render-layer with a small DB-retirement (coupling class C5/C2 in the ID-93/ID-59 vocabulary): render-layer over existing revision stores, plus a C2 drop of an empty table. It does NOT touch the cocoindex source-file layout and is NOT an ID-45 gate (Task status_note).


3. The three view-depths — current-state map (re-verified S384)

Section titled “3. The three view-depths — current-state map (re-verified S384)”

ID-117’s thesis: ONE surface, three view-depths over a common revision abstraction. Today these are three disjoint, partially-built surfaces:

LegWhat it comparesCurrent surface (file:line)State
(1) BINARY source difftwo source_documents versions (parent_id chain) — Q&A-pair or full-text diff of extracted_textapp/documents/[id]/diff/page.tsxcomponents/source-document/source-document-diff-review.tsxsource_document_diffs store; computed by lib/source-documents/document-diff.ts; entry source-document-history.tsx:256 (GitCompareArrows/documents/{id}/diff)EXISTS, text-only. Diffs extracted_text stringsno binary rendering (no DOCX/PDF/XLSX render). This is the leg ID-117 replaces with real binary viewers.
(2) canonical-markdown revision difftwo revisions of the post-Docling markdownNo distinct surface. cocoindex markdown → content_items.content (flow.py:2107), versioned via content_history (= leg 3’s substrate). Upload-path markdown → source_documents.extracted_text (upload/route.ts:626), versioned via source_documents.parent_id (= leg 1).ABSENT / ambiguous source (§3.2).
(3) in-platform user-edit difftwo content_history OR q_a_pair_history revisionscomponents/item-detail/revision-diff-view.tsx:229 (RevisionDiffView engine) ← version-history.tsx:92 (CompareVersionsPanel, content) + qa-revision-history.tsx:64 (QARevisionHistory, Q&A)LANDED-FULL ({59.12}+{59.16}). Both legs wired live. Unified line-diff (bespoke LCS), not side-by-side; richer view DEFERRED (ID-59 INV-15).

3.1 Leg 1 — the source_document_diffs engine (the retirement target)

Section titled “3.1 Leg 1 — the source_document_diffs engine (the retirement target)”

Schema (supabase/migrations/20260617130000_squash_baseline.sql:7376): old/new_document_id (FK→source_documents, CASCADE), diff_type {added,removed,modified,unchanged}, diff_mode {qa,full_text}, old/new_content, old/new_question, similarity_score, affected_content_item_id (FK→content_items, SET NULL), status {pending_review,applied,dismissed}, reviewed_at/by, reviewer_note, section_header. Table COMMENT (:7406): “Stores Q&A pair-level diffs between source document versions. Each row represents one matched or unmatched pair.” Plus an api.source_document_diffs security-invoker view (:7434; GRANTs :13488–13490).

It is the re-ingest / AI-suggest REVIEW store, never the user-edit store. Compute+persist: POST /api/source-documents/[id]/diff calls computeDocumentDiff(oldId,newId,old.extracted_text, new.extracted_text) (document-diff.ts:507; computeFullTextDiff:333) and inserts. Read: GET + the diff page. Review: PATCH drives the status workflow. The full retire-list is in §6.

3.2 Leg 2 — the canonical-markdown projection (the ambiguity)

Section titled “3.2 Leg 2 — the canonical-markdown projection (the ambiguity)”

Post-Docling markdown has two homes by ingest path:

  • cocoindex path: convert_binary_to_markdown(file)content_items.content (scripts/cocoindex_pipeline/flow.py:2107), keyed by content_item_id = uuid5(rel_path). Versioned via content_history (auto-version triggers, migration :1203/:1488/:1684) — = leg 3.
  • upload-API path: source_documents.extracted_text (app/api/upload/route.ts:626), versioned via the source_documents.parent_id chain (= leg 1). cocoindex never writes extracted_text (0 hits).

So “diff two revisions of the canonical markdown” is not a third store — it is (a) two content_history versions of content_items.content (already leg 3), OR (b) two source_documents.extracted_text versions (already leg 1). Leg 2 is a framing of legs 1+3 at markdown granularity. → the “three stores” collapse to two physical revision substrates viewed at three depths.

3.3 Leg 3 — the landed user-edit diff (the engine to extend)

Section titled “3.3 Leg 3 — the landed user-edit diff (the engine to extend)”

RevisionDiffView (revision-diff-view.tsx:229–282) takes two RevisionBlobs and computes a bespoke LCS line-diff (buildLcsTable:70, computeLineDiff:94 — does NOT use the diff package), rendering a unified (not side-by-side) view with [+]/[-] gutters + [Added]/[Removed] labels (WCAG: colour never sole signal), semantic tokens (bg-status-success/10, bg-status-error/10), UK dates, and a “no changes” empty state. It is deliberately source-agnostic — header comment (:12):

“deliberately source-agnostic: callers pass two RevisionBlobs derived from either content_history or q_a_pair_history rows, so the same component serves both substrates (PC-14/15/16/17, INV-14..INV-17).”

RevisionBlob shape (:31–46):

RevisionBlob {
version: number
text: string
changeType: string
changeSummary: string | null
createdAt: string
createdByLabel: string
editIntent: string | null
}

A separate word-level VersionDiff (version-diff.tsx:18, diffWords from diff) is live — rendered inline per history row in version-history.tsx:480/490 (NOT an orphan; lower-fidelity companion to RevisionDiffView). It is a candidate primitive for the v1.1 inline-word-highlight (ID-59 INV-15).


4. The COMMON revision abstraction (the central research question)

Section titled “4. The COMMON revision abstraction (the central research question)”

All three legs reduce to “two snapshots of one logical record, rendered at a chosen depth, optionally with a binary original to render alongside.” RevisionBlob (§3.3) is already ~80% of this. The proposed generalisation:

UnifiedRevision {
recordKind: 'content_item' | 'qa_pair' | 'source_document' // which substrate
recordId: string
version: number
text: string // diffable projection: content | answer_standard | extracted_text
changeType: string // change_type | (synthesised 'reingest' for source-doc)
changeSummary: string | null
createdAt: string
createdByLabel: string
editIntent: string | null
binary?: { storagePath: string; mimeType: string; signedUrlFetcher: () => Promise<string> } // binary leg only
}
UnifiedDiff {
older: UnifiedRevision
newer: UnifiedRevision
viewDepth: 'binary' | 'canonical-markdown' | 'user-edit'
renderMode: 'unified-line' | 'side-by-side' | 'word-inline' | 'binary-split'
}

Mapping each leg (the proof it unifies):

LegrecordKindtext sourcebinary?Default renderMode
(1) binary source diffsource_documentextracted_textYES (storage_path + mime_type)binary-split (two extend-ui viewers side-by-side) + side-by-side markdown fallback
(2) canonical-markdowncontent_itemcontent (cocoindex) OR extracted_text (upload)noside-by-side / word-inline (the ID-59 INV-15 deferral)
(3) user-editcontent_item / qa_paircontent / answer_standardnounified-line (today’s RevisionDiffView)

Consequences for PRODUCT/TECH:

  1. Two physical revision substrates, not three — the source_documents parent_id chain and the *_history tables. Leg 2 is leg 1 or leg 3 at markdown granularity. No new diff-storage table (consistent with ID-59 INV-14 — diff computed on demand).
  2. The abstraction is a TYPE + a fetcher/adapter layer, not a DB change. The only DB change is the source_document_diffs DROP (§6). All else is lib/query/fetchers.ts + query-keys.ts + a UnifiedRevision adapter per recordKind + the render container.
  3. The binary leg adds a viewer dimension absent from RevisionDiffView — rendering the actual DOCX/PDF/XLSX side-by-side. This is the only leg needing new viewer components → extend-ui-kit (§5). Legs 2+3 are pure render-mode additions to the existing engine.
  4. source_documents revisions need a synthesised editIntent/changeType — they have no edit_intent column (only content_history/q_a_pair_history do). PRODUCT decides the provenance panel for a re-ingest revision (e.g. changeType:'reingest', editIntent:null) — OQ-117-4.

5. The binary-viewer leg — extend-ui-kit (LOCKED) + the component-mapping clarification

Section titled “5. The binary-viewer leg — extend-ui-kit (LOCKED) + the component-mapping clarification”

extend-ui-kit is the locked substrate (S380). Live state (S384):

  • Install state: ABSENTpackage.json has no extend* dep; node_modules has only unrelated extend/extend-shallow; components.json registers only ui.shadcn.com (not extend-hq/ui); no mcp-apps/ or components/ references. This is a build prerequisite, not a blocker — extend-ui-kit is a shadcn-style registry (extend-hq/ui, MIT, React; install via npx shadcn@latest add @extend/<component>, which copies source into the repo — so there is no compiled peer-dep version lock). No hard blocker found against React 19.2.6 / Next 16.2.6; it consumes radix primitives the repo already has (radix-ui ^1.4.3).

  • Component inventory (extend.ai / ui.extend.ai/docs, verified S384). The relevant subset for ID-117:

    extend-ui componentDocumented purpose
    PDF Viewer”Viewers and editors for PDF files”
    DOCX Viewer / DOCX Editor (exp.)”Viewers and editors for DOCX files”
    Excel (XLSX) Viewer / Editor (exp.)”Viewers and editors for XLSX files”
    CSV Viewer”Viewers and editors for CSV and TSV files”
    Document Splits”Document split viewer for reviewing large document packets as separate files
    Bounding Box Citations”review extracted values against the source PDF … field-level citations … JSON diffing … page overlays”
    File System”file system view for browsing packet files, folders, and document previews”
  • ⚠ Component-mapping clarification (a PRODUCT precision, NOT a contradiction of S380): the “Document Split Viewer” named in the S380 decision is, in the live inventory, a packet-splitter (surfaces individual files within a multi-file packet) — it is not a two-version comparator, and no extend-ui component documents a doc-to-doc diff/compare/side-by-side prop. The only diff-adjacent feature is Bounding Box Citations’ “JSON diffing” (field-extraction review vs source, not version-comparison). Implication: the binary version-diff is composedtwo extend-ui file-viewer instances (PDF/DOCX/XLSX) rendered side-by-side + a bespoke change/compare affordance — rather than dropped onto one magic component. This is fully consistent with S380 (“build the surface with extend-ui … binary viewers”): extend-ui supplies the hard part (client DOCX/XLSX/PDF rendering the repo entirely lacks); ID-117 builds the compare composition on top. It is a v1 deliverable, not a v1.1 deferral. Carried as OQ-117-2′ (binary-diff UX precision).

  • Installed primitives (partial fallback, PDF-only). Versions + usage (S384):

    PackageVersionUse todayClient/server
    react-pdf^10.4.1components/reader/pdf-viewer.tsx (own PDF viewer)client
    diff^9.0.0diff-highlighted-text.tsx, version-diff.tsx, document-diff.tsboth
    mammoth^1.12.0upload/extract routes (DOCX→HTML)server-only
    exceljs4.4.0procurement-export-xlsx.ts (export)server-only
    @tiptap/markdown3.23.4content/response editorsclient

    The repo can render PDF client-side already, but has no client DOCX or XLSX viewer — exactly the gap extend-ui-kit fills. So “build on installed primitives” is NOT a substitute for the binary leg (it covers only 1 of 3 formats); extend-ui-kit is the right substrate, as S380 decided.

Net: the binary leg is v1, on extend-ui-kit. The only open question is UX-shape precision (OQ-117-2′), not whether to build it.


6. The retirement — source_document_diffs (C2 drop; 0 rows CONFIRMED)

Section titled “6. The retirement — source_document_diffs (C2 drop; 0 rows CONFIRMED)”

Empirically confirmed 0 rows on the dev/platform DB (zjqbrdctesqvouboziae, this RESEARCH — §8). The client DB is unreachable from here (still an OQ — §9 OQ-117-1), but S380 states it is also empty (“the diff engine was newly built prior to the pivot … will also be empty — app hasn’t launched yet”).

Exhaustive retire-list (every reader/writer of source_document_diffs, current file:line, S384):

  • lib/source-documents/document-diff.tscomputeFullTextDiff:333, computeDocumentDiff:507 (compute engine)
  • app/api/source-documents/[id]/diff/route.ts.from('source_document_diffs') @ 75, 125, 183, 412, 490, 552, 586, 607 (GET/POST/PATCH)
  • app/documents/[id]/diff/page.tsx.from(...) @ 56, 65, 97, 106, 173, 182, 215, 224, 266
  • components/source-document/source-document-diff-review.tsx + diff-highlighted-text.tsx + hooks/use-diff-review.ts — the review UI (couples indirectly via the API route; no direct table stringcorrecting the S383 claim that these reference the table directly)
  • components/source-document/source-document-history.tsx:256 — compare entry link (GitCompareArrows/documents/{id}/diff)
  • lib/source-documents/source-document-impact.tsanalyseDocumentImpact, .from(...) @ 69, write affected_content_item_id @ 141/142
  • lib/mcp/tools/content.tsget_document_diff def @ 2033, .from(...) @ 2065/2189
  • scripts/generate-api-views.ts:118 — the api. view-set entry (re-vendor trigger — §10)
  • scripts/export-user-data.ts — type @ 496, .from(...) @ 647, assign @ 691 (GDPR export)
  • Tests: __tests__/mcp/document-diff-tool.test.ts, __tests__/lib/source-documents/document-diff.test.ts, __tests__/api/source-documents-diff.test.ts, e2e/tests/document-diff.spec.ts
  • Migration: …squash_baseline.sql CREATE :7376, COMMENT :7406, api view :7434, GRANTs :13488–13490

Two workstreams hide here — PRODUCT must keep them separate:

  1. RETIRE the storage + review workflow (the status/reviewed_by/reviewer_note/AI-suggest-apply path). C2-cheap drop IF the use-case it served (compare two binary source-doc versions, with affected-item impact) is preserved on the unified surface. The review-workflow columns are the part that genuinely disappears — confirm with Liam the re-ingest review workflow is no longer wanted vs. re-homed (OQ-117-1).
  2. UNIFY the three view-depths (§4) — the substantial render-layer build (extend RevisionDiffView / a new container with the binary + markdown legs). They can be sibling-ordered within ID-117; no cross-Task dep.

7. Reference detail page overlap (app/reference/[id]) — assessed, OUT of v1 scope

Section titled “7. Reference detail page overlap (app/reference/[id]) — assessed, OUT of v1 scope”

S383 omitted scope item #4. Findings (S384):

  • app/reference/[id]/page.tsx is an RSC that calls rpc('reference_get_verbatim', {p_reference_id}) over reference_items + enriches from source_documents; renders via reference-detail-client.tsxContentRenderer (components/item-detail/content-renderer.tsx:89, reference-detail-client.tsx:155, <ContentRenderer content={reference.body} />). The recent ID-111 B-5 fix (913effe8) added a UUID_RE gate (page.tsx:33/64) → notFound() on a malformed id.
  • reference_items is a flat, workspace-less, write-once global evidence table — NO reference_history, no revision versioning, no in-platform user-edit path (schema comment: “Global, workspace-less external reference/evidence layer … Never auto-promotes into content_items”; types/reference.ts:8). ContentRenderer renders exactly one revision.
  • Verdict: the reference page is NOT a current consumer of the unified diff surface. Its substrate has no two-revision pair to diff. It is not a thinner twin of item-detail (which has content_historyVersionHistoryRevisionDiffView); it is a structurally different read-only surface.
  • Future seam (documented, not in scope): RevisionDiffView is source-agnostic by design, so if reference_items ever gains a reference_history table + a /api/references/[id]/history fetch analog, the reference page could become a 4th consumer (<RevisionDiffView older newer />). That requires the DB substrate first → explicitly excluded from ID-117 (OQ-117-6 confirms the exclusion).

8. Empirical verification (pre-ratification)

Section titled “8. Empirical verification (pre-ratification)”
FactMethodResult
source_document_diffs row count (retire-safety)Supabase MCP execute_sql on zjqbrdctesqvouboziae (dev/platform)0 rows ✅ (S383 could not run this). Client DB unreachable here → OQ-117-1.
content_history rows (user-edit substrate is live)same11 rows — leg 3 has real data.
q_a_pair_history rowssame0 rows (Q&A-edit leg wired but unexercised in dev).
source_documents / reference_items rowssame1 / 1.
extend-ui-kit / DocumentSplitViewer / DocxViewer / XlsxViewerpackage.json + node_modules + corpus grep + registry WebFetchABSENT in repo; registry extend-hq/ui live, MIT, shadcn-install. No doc-to-doc diff prop on any component (Document Splits = packet-splitter).
react-pdf ^10.4.1 / diff ^9.0.0 / mammoth ^1.12.0 / exceljs 4.4.0 / @tiptap/markdown 3.23.4package.json + usage grepPRESENT; DOCX/XLSX server-only (no client viewer) → extend-ui fills the gap.
RevisionDiffView / RevisionBlob (engine to extend)grep + gitnexus_contextPRESENT revision-diff-view.tsx:229/31; 2 callers (CompareVersionsPanel, QARevisionHistory).
source_document_diffs table + api. view (retire target)migration grepPRESENT squash_baseline.sql:7376/7406/7434.
content_history.edit_intent / q_a_pair_history.edit_intentmigrationPRESENT (ID-59 substrate) — the provenance the unified surface surfaces.

No ABSENT/SIGNATURE_DRIFT on a library the v1 surface relies on. extend-ui-kit (the one ABSENT dependency) is a build prerequisite installed via the registry — it does not block PRODUCT.


  • OQ-117-1 (retirement scope): dev 0-rows CONFIRMED (§8). Remaining: (a) confirm source_document_diffs is also 0 rows in the client DB (unreachable from this RESEARCH context), and (b) confirm the re-ingest review workflow (status/reviewed_by/reviewer_note/AI-suggest-apply via affected_content_item_id) is genuinely retired, vs. re-homed onto the unified surface. The storage drops cleanly only if the workflow is retired, not just the table.
  • OQ-117-2′ (binary-diff UX precision — REPLACES the old defer-fork): the binary leg is extend-ui-kit (LOCKED, v1). Since extend-ui’s Document Split Viewer is a packet-splitter (not a version-comparator), PRODUCT must define the binary version-diff UX: (A) two file-viewer instances side-by-side, visual compare only (no inline change marking) for v1; (B) add a change-detection overlay (e.g. diff the extracted_text/markdown and annotate the rendered originals). Plus: which extend-ui components to install (minimum PDF + DOCX + XLSX viewers; Document Splits + File System optional for multi-file packet navigation). This is a behaviour question, not a v1.1 deferral.
  • OQ-117-3 (canonical-markdown source): for the markdown leg, which projection is canonical — cocoindex content_items.content (content_history-versioned) or upload source_documents.extracted_text (parent_id-versioned)? Post-ID-45 (full-corpus re-ingest), cocoindex is the dominant path — recommend PRODUCT pin content_items.content + content_history as canonical, extracted_text legacy.
  • OQ-117-4 (binary-revision provenance): source_documents revisions have no edit_intent/change_type/change_summary — what does the provenance panel show for a re-ingest revision (synthesised changeType:'reingest', editIntent:null)?
  • OQ-117-5 (entry-point / IA): leg 1 lives at /documents/[id]/diff; leg 3 inline in version-history.tsx / qa-revision-history.tsx. Does the unified surface get ONE route, or stay inline-per-record with a shared engine? Affects {117.4} decomposition.
  • OQ-117-6 (reference-page scope exclusion): confirm reference_items (flat, un-versioned; no reference_history) is explicitly OUT of ID-117 scope — future seam only (§7).

10. Recommendations (Liam / Orchestrator decide — this doc writes no ledger)

Section titled “10. Recommendations (Liam / Orchestrator decide — this doc writes no ledger)”
  1. Binary leg ships in v1 on extend-ui-kit (S380 lock). Install the PDF/DOCX/XLSX viewers from the extend-hq/ui registry as a build prerequisite; compose the version-compare layer on top (two viewers side-by-side + change affordance) — extend-ui supplies the client DOCX/XLSX rendering the repo lacks. Resolve the binary-diff UX precision via OQ-117-2′. Do not defer binary to v1.1.
  2. Common abstraction = UnifiedRevision/UnifiedDiff (§4) — a TYPE + fetcher/adapter layer over two physical substrates (the source_documents chain; the *_history tables). No new diff-storage table. Build by extending the landed RevisionDiffView with new renderModes (side-by-side, word-inline, binary-split).
  3. ID-117 builds ON TOP of the landed {59.12}/{59.16} user-edit diff — it does not consolidate an unbuilt plan. The side-by-side markdown + inline word-highlight that ID-59 INV-15 deferred is the heart of legs 2+3 here; reference ID-59 PRODUCT INV-14..17 as the inherited user-edit contract.
  4. Retire source_document_diffs (§6) — 0 rows confirmed (dev); C2 drop once the review-workflow retire-vs-rehome is settled (OQ-117-1). Keep RETIRE and UNIFY as separate PRODUCT invariant clusters.
  5. Right-sizing: Full chain confirmed. Author {117.2} PRODUCT (per-leg behaviour + retirement + binary-diff UX), {117.3} TECH (the UnifiedRevision type + fetcher/adapter + the source_document_diffs drop migration + the render container + the extend-ui install), then {117.4} PLAN (likely: retire-leg-1 slice; UnifiedRevision adapter+fetchers; side-by-side render-mode; binary-viewer install+compose slice — well under the 25-subtask ceiling).
  6. Re-vendor reminder: dropping source_document_diffs changes the api. view-set (generate-api-views.ts:118) → task-view-vendor-drift / schema-parity will fire; TECH must include the re-vendor step.

SourceUsed for
S380 transcript dc46727f (verbatim Liam turns; in-session extend-ui WebFetch)§0, §1, §5 — the grounding decision + extend-ui inventory
ID-117 ledger record (get task 117description/status_note/session_refs)§0, §1 — authoritative framing
components/item-detail/revision-diff-view.tsx :12 header comment, :31–46 RevisionBlob, :70/:94 LCS, :229–282 RevisionDiffView§3.3, §4 — the source-agnostic engine to extend
components/item-detail/version-history.tsx :92 CompareVersionsPanel, :176 VersionHistory, :283 handleToggleCompare, :480/:490 VersionDiff§3, §3.3 — content user-edit leg
components/qa/qa-revision-history.tsx :45 toRevisionBlob, :64 QARevisionHistory, :149 render§3, §3.3 — Q&A user-edit leg
components/item-detail/version-diff.tsx :4/:18 (diffWords)§3.3 — live word-level primitive (not orphan)
lib/source-documents/document-diff.ts :333 computeFullTextDiff, :507 computeDocumentDiff§3.1, §6 — legacy compute engine
app/api/source-documents/[id]/diff/route.ts (GET/POST/PATCH; .from @75–607)§3.1, §6 — legacy diff API + retire-list
app/documents/[id]/diff/page.tsx (.from @56–266)§3.1, §6 — legacy diff page
components/source-document/{source-document-diff-review,diff-highlighted-text}.tsx + hooks/use-diff-review.ts§6 — legacy review UI (indirect coupling)
components/source-document/source-document-history.tsx :256 compare link§3.1, §6 — version-chain UI + diff entry
lib/source-documents/source-document-impact.ts :69/:141/:142 analyseDocumentImpact§6 — impact-matching write
lib/mcp/tools/content.ts :2033/:2065/:2189 get_document_diff§6 — MCP retire-list
scripts/generate-api-views.ts :118; scripts/export-user-data.ts :496/:647/:691§6, §10 — api. view + GDPR export; re-vendor reminder
scripts/cocoindex_pipeline/flow.py :2107 "content": content_text§3.2, §4 — cocoindex markdown projection
app/api/upload/route.ts :626 extracted_text write; :324 initial_ingest§3.2 — upload-path markdown + version chain
supabase/migrations/20260617130000_squash_baseline.sql :7376/:7406/:7434/:13488–90 (source_document_diffs); :1203/:1488/:1684 (content_history triggers)§3.1, §3.2, §6 — DB shapes
app/reference/[id]/page.tsx :33/:64 (UUID gate, ID-111 913effe8), :71 RPC; reference-detail-client.tsx :155; components/item-detail/content-renderer.tsx :89; types/reference.ts :8§7 — reference-page overlap
package.json (react-pdf ^10.4.1, diff ^9.0.0, mammoth ^1.12.0, exceljs 4.4.0, @tiptap/markdown 3.23.4, radix-ui ^1.4.3; extend*→[]); components.json§5, §8 — installed-viewer empirical + extend-ui absence
extend.ai /resources/extend-ui-kit + ui.extend.ai/docs (registry extend-hq/ui, MIT)§5 — current component inventory + diff-prop check
id-31-canonical-pipeline-implementation-plan/PLAN.md :21/:575 (bl-267 / S243 markdown-sidecar)§1 — superseded predecessor framing
ID-59 PRODUCT.md INV-14..17 (:276 INV-15 v1.1 deferral)§3.3, §6, §10 — inherited user-edit contract
gitnexus query/context (repo:'canonical'); 3 S384 survey agents; Supabase MCP row counts§2, §8 — orientation + verification

End of RESEARCH. Supersedes the S383 draft; grounded on S380 (dc46727f) + the ID-117 ledger. Recommendations PENDING Liam (§9/§10). No implementation, no DDL, no ledger edits performed.