ID-59 {59.2} PRODUCT — Per-UC write-back (UC1/4/6/3/5/8) + edit_intent capture + Diff-UI
ID-59 {59.2} PRODUCT — Per-UC write-back + edit_intent capture + Diff-UI
Section titled “ID-59 {59.2} PRODUCT — Per-UC write-back + edit_intent capture + Diff-UI”Status: PRODUCT draft — authored 08/06/2026 by a FRESH planner instance (Q-PLANNER-2: not the {59.1} RESEARCH author). Inputs read in full: ratified {59.1}
RESEARCH.md(08/06/2026) + the pre-canonicalTECH.md(05/06/2026, reconciled below — see “TECH.md reconciliation”). Carries the Liam 08/06/2026 ratifications:edit_intentlives on acontent_history.edit_intent textcolumn (sidecar.mdREJECTED); the column lands pre-cutover in the {64.7}/{64.8} window and does NOT gate the re-ingest.Task: ID-59 (T9) — “Per-UC write-back (UC1/4/6/3/5/8) + Diff-UI sidecar re-point”. Largest of the T9–T14 family (3 PLAN units).
Companion artefacts: sibling
RESEARCH.md({59.1}, ratified) +TECH.md(pre-canonical arbitration rule spec — INPUT to {59.3}, reconciled here, NOT a replacement). The {59.3} TECH (write-back adapter contract + storage slice + per-UC wiring) is the next chain step.
Summary
Section titled “Summary”When a user edits in-platform content — a content_item body (UC1/UC4), a Q&A pair (UC6), or a
sweeping cross-corpus rename (UC3) — the platform records why the edit was made (edit_intent:
cosmetic / data / structural), arbitrates that intent when edits collide, writes the result
back to the correct durable surface (the walked source file at source_documents.storage_path for
file-backed content; q_a_pairs for KH-DB-only Q&A), and lets the user review what changed across
revisions via a Diff-UI built on the existing version history. Cross-workspace dedup (UC8) and
bid→Q&A promotion (UC5) write Q&A surfaces with merge/promotion lineage. The user experience is that
edits are durable, attributed, intent-labelled, reviewable, and never silently lost or mis-routed.
Problem
Section titled “Problem”Today the platform lets admins/editors edit content_item bodies (PUT /api/items/[id] writes a
content_history row with change_type='edit') and Q&A pairs, but the intent of an edit is never
captured, edits to file-backed content are not written back to the walked source file that seeds the
content’s identity, and there is no purpose-built surface to review what changed between two user
revisions. Under the canonical cocoindex pipeline this gap is load-bearing: the source-relative POSIX
path stored in source_documents.storage_path is BOTH the on-disk write target AND the uuid5 PK
seed for content_items / source_documents / content_chunks / entity_mentions — so an edit
that does not write the file back, or writes it to a different path, either strands the edit (DB and
file diverge) or mints a new identity (orphaning the old content). The edit_intent label gates
whether the next POST /walk re-classifies the changed file (cosmetic skips re-classification;
data / structural trigger it), so capturing and arbitrating it correctly is what keeps the corpus
classification honest after user edits.
Code-intelligence orientation. gitnexus_query / gitnexus_context were unavailable at authoring
time (LadybugDB storage-version mismatch — index v41 vs build v40; transient rebuild). Applied the
S276 ccc/grep fallback. The fallback findings are themselves the correct greenfield signal the
RESEARCH already established:
edit_intent/arbitratesurface — greenfield.grep -rniE 'edit[_]?intent|arbitrat'overlib/ app/ components/ types/ hooks/ contexts/returns ZERO hits (08/06/2026). T9 is greenfield implementation against a ratified rule (the pre-canonicalTECH.mdT-1..T-5, commit12a20963), NOT a re-point onto landed code.- Live
content_itemedit write-back surface — EXISTS, no intent.app/api/items/[id]/route.tsguards withgetAuthorisedClient(['admin','editor']), validates transitions, and inserts acontent_historyrow withchange_type='edit'(route.ts ~L746–760) via thecontent_history_auto_version()trigger. Noedit_intentis stamped. Sibling routes:app/api/items/[id]/history/route.ts,.../history/[versionId]/route.ts,.../rollback/route.ts. - Version-history surface — EXISTS.
components/item-detail/version-history.tsx(content-item version list) +components/item-detail/content-editor.tsx(Tiptap editor). The Diff-UI is an extension of this surface, not a new substrate. - Q&A history substrate — EXISTS.
q_a_pair_history(trigger-written version snapshots:version,question_text,answer_standard,answer_advanced,scope_tag,changed_at,changed_by;supabase/migrations/20260520225456_t6_q_a_pairs_full_schema.sql:153). - Write-back target.
source_documents.storage_path text NOT NULL(supabase/migrations/20260416102457_pre_squash_reconciliation.sql:4037); pipeline writes the source-relative POSIX path here AND derives theuuid5PK from it. There is NOoriginal_pathcolumn. source_document_diffs— EXISTS, but is NOT the user-edit revision diff store (see “Diff-UI substrate clarification” below). It is a re-ingest-version Q&A/full-text diff review store.
TECH.md reconciliation (pre-canonical TECH.md, 05/06/2026)
Section titled “TECH.md reconciliation (pre-canonical TECH.md, 05/06/2026)”The sibling TECH.md is the arbitration rule spec, written before the canonical-pipeline
decisions. Its rule logic is still valid and is carried forward; its storage/path/landed-code
claims are stale and are SUPERSEDED by the ratified {59.1} RESEARCH and the live surface. This
PRODUCT does not silently ignore it and does not carry its stale claims forward.
CARRIED forward (still-valid prior findings)
Section titled “CARRIED forward (still-valid prior findings)”| # | Carried finding (from TECH.md) | Where it lands in this PRODUCT |
|---|---|---|
| C-a | edit_intent is a closed 3-value CV: cosmetic / data / structural (S234 ONT.14). The S244-prompt freeform value is REJECTED (not in the ratified CV). | INV-7, INV-8 (CHECK domain). |
| C-b | Arbitration truth table is commutative + idempotent, cosmetic is the unit element: any merge that includes a data or structural intent → merged data; cosmetic + cosmetic → cosmetic. N-way reduces pairwise, order-independent. | INV-9, INV-10. |
| C-c | The v1 safe default (RATIFIED-S243 Item 6): conservative re-run over silent skip — any concurrent edit including data/structural → data. False-positive re-runs are cheap; false-negative skips silently corrupt corpus quality. | INV-9 rationale. |
| C-d | Finer arbitration is v1.1 (e.g. structural+structural→structural with version-locked rebase; user-prompted conflict-resolution UX). v1 has no consumer that branches structural vs data — both trigger re-run. | INV-11 (non-goal / deferral). |
| C-e | Null/unknown-intent fallback → treat as cosmetic (the unit element), so an explicit intent on the other side is not diluted by a silent (version-skewed) client; every fallback emits a structured log line. | INV-12. |
| C-f | Arbitration is a KH-server pre-write concern that runs once per save, BETWEEN the CRDT merge and the durable write — NOT inside cocoindex and NOT a Yjs awareness concern. | INV-2, INV-9. |
| C-g | Audit must survive arbitration: the merged intent + the per-actor inputs must be reconstructable. | INV-8 (arbitration_inputs), INV-13. |
SUPERSEDED (stale claims — corrected fact stated)
Section titled “SUPERSEDED (stale claims — corrected fact stated)”| # | Stale claim (TECH.md) | SUPERSEDED — corrected fact (evidence) |
|---|---|---|
| S-a | ”the merged write goes to source_documents.original_path (or sidecar .md)” (TECH §1, §4.2 step 5) | SUPERSEDED. The real, NOT-NULL column is source_documents.storage_path (the source-relative POSIX path = the uuid5 PK seed). There is no original_path column. Write-back targets storage_path. (…pre_squash…:4037; flow.py:1971.) |
| S-b | Storage of edit_intent is “deferred… column on content_history, sidecar metadata file, or cocoindex flow arg… the most likely choice” (TECH §1 out-of-scope, §4.2 step 5, §4.4) | SUPERSEDED + RATIFIED. Liam ratified (08/06/2026) edit_intent = a content_history.edit_intent text column (+ optional arbitration_inputs jsonb). The sidecar .md is REJECTED (it would mint junk content_items and gate the re-ingest). The flow-arg is a transport, not a store. (RESEARCH §4.3.) |
| S-c | content_history.edit_intent / content_history.actor_id / content_history.arbitration_inputs columns are named as if they exist (TECH §4.4) | SUPERSEDED. None exist today. content_history is id, content_item_id, version, title, content, brief, detail, reference, metadata, change_summary, change_type, created_by, created_at, change_reason. The edit_intent (+ optional arbitration_inputs) column is NEW (this Task’s pre-cutover slice). Note: there is no actor_id column — multi-actor attribution rides in arbitration_inputs (INV-8). (…pre_squash…:3485.) |
| S-d | The arbitration substrate “lands” as if it could be reused (TECH framed as an implementation reference for landed wiring) | SUPERSEDED. Only the rule TECH landed (S244, commit 12a20963). No arbitration CODE exists (grep: zero hits). T9 is greenfield implementation of the rule, not a re-point. |
| S-e | ”Tiptap is present but the Yjs collab plugin is not yet wired” (TECH T-1) | SUPERSEDED (refinement). The Yjs/collab dependencies ARE installed (@tiptap/extension-collaboration@3.23.4, yjs@^13.6.30, y-prosemirror@^1.3.7). What is absent is the collab-provider wiring + the edit_intent capture UX. (RESEARCH §1.1, §6.) |
Diff-UI substrate clarification (correction to RESEARCH §3)
Section titled “Diff-UI substrate clarification (correction to RESEARCH §3)”The RESEARCH §3 states source_document_diffs “is not a table — zero hits”. That is imprecise.
source_document_diffs DOES exist (…pre_squash…:3970): a review store of Q&A-pair-level and
full-text diffs computed between two source-document re-ingest VERSIONS (old_document_id,
new_document_id, diff_type ∈ added|removed|modified|unchanged, status ∈
pending_review|applied|dismissed, diff_mode ∈ qa|full_text). It is the substrate for the
AI-suggest / re-ingest change-detection review queue (UC6 AI-suggest adjacent), NOT a
user-edit revision diff store.
The RESEARCH’s recommendation is nonetheless correct and is adopted: the user-edit Diff-UI
(INV-14..INV-17) is built against the per-revision version model — content_history for content
items, q_a_pair_history for Q&A pairs — NOT against source_document_diffs. The “sidecar re-point”
in the Task title resolves to “build the user-edit Diff-UI against the existing revision tables” — no
new diff-storage table is minted. source_document_diffs is left to its existing re-ingest-review
role and is explicitly out of scope for the user-edit Diff-UI (INV-17).
Goals / Non-goals
Section titled “Goals / Non-goals”Goals. Durable, attributed, intent-labelled write-back for UC1/UC4 (file-backed content),
UC6 (KH-DB-only Q&A), UC3 (sweeping rename), UC5 (bid→Q&A promotion), UC8 (cross-workspace dedup);
edit_intent capture + arbitration at the write site; a user-edit Diff-UI over the existing
revision tables.
Non-goals (v1). User-facing conflict-resolution UX (“your intent was overridden” prompt) — v1.1
(INV-11). Finer arbitration (structural-preserving merge) — v1.1. Yjs y-supabase persistence —
DEFERRED-v1.1. Q&A file-sidecar materialisation — DEFERRED-v1.1. UC7/UC10 (external-folder change,
handled natively by cocoindex source binding, T8 ✅), UC9 (scope-tag taxonomy — separate OPS
package), UC2 (collapsed into UC1, S228) — out of T9 scope. The pre-cutover ALTER itself is a
{59.3}/{59.4} + {64.7}/{64.8} concern; this PRODUCT specifies the behaviour the column enables, not
the DDL.
Figma: none provided. The Diff-UI extends the existing version-history.tsx surface; visual
treatment follows Warm Meridian semantic tokens (INV-16).
Behavior
Section titled “Behavior”A. Write-back routing per use-case
Section titled “A. Write-back routing per use-case”-
UC1 (typo fix) / UC4 (paragraph rewrite) — file-backed content edit. When an admin or editor saves an in-platform edit to a
content_itemwhosesource_document.storage_pathresolves to a walked source file, the platform writes the edit to BOTH (a) thecontent_itemsrow (DB) and (b) the on-disk source file at thatstorage_path, in a single user-visible save action. The source-relative path is preserved byte-for-byte across the write so the file’suuid5identity is stable on the nextPOST /walk; the platform MUST NOT write the edited bytes to a different path (a path change would mint a new identity and orphan the existing content — the user would perceive their edit as “lost” and a duplicate as “appeared”). UC4 additionally re-anchors any citations affected by the paragraph rewrite. -
UC1/UC4 atomicity + ordering. The DB write and the file write are presented to the user as one atomic save: on success the user sees a single “saved” confirmation and the new content; on failure of either leg the user sees a single failure state and NEITHER leg is left applied in isolation (no “DB updated but file stale”, no “file written but DB stale”). The
edit_intentis resolved (arbitrated — INV-9) BEFORE either write and stamped on the resultingcontent_historyrow (INV-7). -
UC1/UC4 — content_item without a
source_document(OQ-59-3 disposition). Somecontent_itemshavesource_document_id IS NULL(e.g. items created via MCPcreate_content_item, or legacy imports). For these, the edit write-back is KH-DB-only: the edit is written to thecontent_itemsrow + acontent_historyrow (withedit_intent), and NO file is written. The platform MUST NOT auto-create asource_document/ mint aconnector='mcp'storage path on the edit path in v1 — doing so would inject a new walked-file identity as a side effect of an edit, which is exactly the source-layout-freeze hazard the re-ingest must avoid. The user experience is identical to a file-backed edit except no file is touched; the Diff-UI (INV-14) still works because it readscontent_history.- Open item for Liam (OQ-59-3): this disposition (KH-DB-only fallback, no auto-create) needs a
prod sweep to size the
source_document_id IS NULLpopulation and confirm none of those items are expected to round-trip to a file. Recommend running the sweep at this PRODUCT stage so {59.3} TECH can confirm the fallback is the only path needed (vs a later opt-in “materialise to file” affordance, which would be v1.1).
- Open item for Liam (OQ-59-3): this disposition (KH-DB-only fallback, no auto-create) needs a
prod sweep to size the
-
UC6 user-direct (Q&A pair revision) — KH-DB-only. When a user revises a Q&A pair in-platform, the platform writes the revision to
q_a_pairsand a newq_a_pair_historysnapshot row; it does NOT write any source file (Q&A pairs are KH-DB-only in v1 — file-sidecar materialisation is DEFERRED-v1.1). Theedit_intentis captured and arbitrated identically to UC1/UC4 (INV-9), and is recorded against the Q&A revision (INV-7 applies to the Q&A history surface as well ascontent_history). -
UC6 AI-suggest (out of the arbitration path). When a source-document change is detected and the platform proposes a Q&A revision via the Anthropic API, the proposal lands in a review queue and is only written to
q_a_pairsafter a single user commits it. This is a single-actor commit — it does NOT invoke arbitration (no CRDT merge) — and is the existing re-ingest change-detection /source_document_diffsreview flow, not part of this Task’s edit write-back. Theedit_intenton the committed revision is the user’s commit-time selection (single value, no arbitration). -
UC3 (sweeping rename) — batched single-actor file write-back. A cross-corpus find-and-replace (or smart-agent per-match-approve) rename rewrites the affected walked source files at their
storage_path. Every record touched by one sweep shares a single sweep identifier so the user can see, and roll back, the whole sweep as a unit. The rename is a batched single-actor operation — it does NOT invoke arbitration (no concurrent CRDT merge). Per-match provenance is recorded so a user can audit which files a sweep changed and revert per the existing rollback contract. UC3 is gated on the write-back adapter contract (Spike#3 — tracked as a cross_doc_link, INV-18). -
UC5 (bid→Q&A promotion) — KH-DB-only with lineage. Promoting a bid/form response to a Q&A pair creates a
q_a_pairsdraft carrying lineage back to the source response and its originating question. No file is written. This does NOT invoke arbitration. The promotion surfaces the source response context (theform_*naming per the {64.14} rename — cross_doc_link, INV-18); the user reviews the draft before it is published. -
UC8 (cross-workspace dedup) — KH-DB-only merge with lineage. Cross-workspace deduplication merges near-duplicate
q_a_pairs(KH-server orchestration + Claude similarity reasoning), recording merge lineage inq_a_pair_history(the surviving pair references the superseded one). No file is written; arbitration is not invoked (DB-level merge, not a CRDT edit). UC8 is gated on Spike#10 (dedup substrate), UC8 ratification, and bl-74 (q_a_pair_historysuperseded_by+source_workspace_idlineage columns) — all cross_doc_links (INV-18); the UC8 slice does not start until those clear.
B. edit_intent capture, storage, and arbitration
Section titled “B. edit_intent capture, storage, and arbitration”-
Intent capture + arbitration at the write site. Every UC1/UC4/UC6-user-direct save carries an
edit_intent∈{cosmetic, data, structural}selected at save time. When a single Tiptap+Yjs collab session resolves a CRDT merge of two-or-more concurrent edits with differing intents, the platform arbitrates a single merged intent at the KH-server write site — AFTER the CRDT merge, BEFORE the durable write — by the commutative rule:cosmetic+cosmetic→cosmetic; any combination that includes adataORstructuralintent →data(the v1 safe default: conservative re-run over silent skip, because false-negative skips silently corrupt corpus quality). Arbitration is invoked ONLY on the UC1/UC4/UC6-user-direct CRDT paths; UC3/UC5/UC6-AI/UC8 are single-actor and stamp the actor’s single intent without arbitration. -
N-way concurrency. For N ≥ 2 concurrent editors the merged intent reduces pairwise and is order-independent: the same set of intents yields the same merged value regardless of arrival order (
cosmeticis the unit element; anydata/structuralpresent ⇒data). -
Conflict-resolution UX deferred (v1.1). v1 silently resolves to the arbitrated value and does NOT surface a “your intent was overridden because another user concurrently selected X” prompt, and does NOT preserve a
structural-specific merged outcome. This is acceptable for v1 because no v1 consumer branches onstructuralvsdata(both trigger re-classification). The override-UX and finer (structural-preserving) arbitration are v1.1 — a v1.1 PRODUCT will define them. -
Null / unknown intent fallback. If a save arrives with
edit_intentnull, undefined, or a value outside the closed CV (most likely an older client predating the capture UX), the platform treats that side ascosmetic(the unit element) so an explicit intent on the other side is not diluted. Every such fallback emits a structured log line (event: edit_intent_arbitration_fallback, the received value,treated_as: cosmetic, and the user / content-item / op identifiers) so client-version skew is diagnosable rather than silent. (A high volume of these in production is a signal to address a client-skew gap, not an error the user sees.) -
edit_intentpersistence + audit. The merged (or single)edit_intentis persisted on the revision record: a newcontent_history.edit_intent textcolumn for content-item edits (and the equivalent on the Q&A revision surface for UC6). When arbitration ran, the per-actor inputs are persisted (acontent_history.arbitration_inputs jsonbarray of{actor, intent}pairs) so the arbitration is forensically reconstructable and multi-actor attribution survives (there is no separateactor_idcolumn —created_bycarries the single/primary actor,arbitration_inputscarries the full set). Pre-edit-feature history rows legitimately haveedit_intent = NULL; the column is write-only-forward with no backfill.edit_intentis constrained to the closed CV{cosmetic, data, structural}(a CHECK; NULL allowed). The platform MUST NOT write a value outside that domain.
C. Diff-UI (user-edit revision comparison)
Section titled “C. Diff-UI (user-edit revision comparison)”-
Diff-UI source. The user-edit Diff-UI compares two revisions drawn from the existing version model —
content_historyrows for content items,q_a_pair_historyrows for Q&A pairs — selected from the version-history list. It does NOT readsource_document_diffs(that is the re-ingest / AI-suggest review store, INV-17). No new diff-storage table is introduced; the diff is computed on demand from the two selected revision blobs. -
Diff-UI v1 scope (OQ-59-4 disposition). v1 ships a minimal revision-diff view: from a content item’s (or Q&A pair’s) version history, the user picks two versions (default: the latest two) and sees a clear old↔new comparison of the changed text, with each version’s metadata (
version,change_type,change_summary,created_at,created_by, and the newedit_intent). The richer side-by-side markdown rendering with inline word-level highlighting is DEFERRED to v1.1. Rationale: “retained for v1” (S243 Item 5) is satisfied by a usable comparison surface; the heavier markdown side-by-side is not load-bearing for v1 and can follow once the revision surface is proven.- Open item for Liam (OQ-59-4): confirm minimal-diff-view-for-v1 (richer side-by-side → v1.1). This bounds {59.2}/{59.4} scope.
-
Diff-UI presentation invariants. Additions, removals, and unchanged content are distinguished by MORE than colour alone (e.g. +/− gutters, labels, or weight) per WCAG 2.1 AA — colour is never the sole signal. All colour/spacing/typography uses Warm Meridian semantic tokens (no raw Tailwind colours). Empty/identical revisions render as an explicit “no changes between these versions” state rather than a blank panel. UK English throughout (DD/MM/YYYY timestamps).
-
Diff-UI does NOT gate, and is NOT gated by, the
edit_intentstorage decision. The Diff-UI works off whatever revision rows exist regardless of whereedit_intentis stored; conversely theedit_intentcolumn lands independently of the Diff-UI.source_document_diffsremains in its existing re-ingest-review role and is out of scope for the user-edit Diff-UI.
D. Roles, scoping, and cross-Task coupling
Section titled “D. Roles, scoping, and cross-Task coupling”-
External dependencies are cross_doc_links, not sibling subtask deps. Spike#3 (write-back adapter contract — gates UC1/UC3/UC4 file write-back; largely pre-answered by the landed cocoindex-write-model R1/R4), Spike#10 (UC8 dedup substrate — gates UC8), UC8 ratification (gates UC8), bl-74 (
q_a_pair_historylineage columns — gates UC8 merge audit), and {64.14} (bid_*→form_*rename — UC5 response context) are tracked as cross_doc_links on ID-59, NOT as sibling-subtask dependencies, per the §3.3 sibling-only rule. Within ID-59 the storage slice and the per-UC write-back slices CAN be sibling-ordered (theedit_intentcolumn before the UC1/UC4/UC6 slices that stamp it) — no cross-Task subtask dep is required, so no escalation. -
Authorisation. Content-item edit write-back (UC1/UC4/UC3) is restricted to
admin/editorroles (the existinggetAuthorisedClient(['admin','editor'])guard on the items route); failures route viaauthFailureResponse(auth)to the correct HTTP status. Q&A write-back (UC6/UC5/UC8) follows the Q&A surface’s existing role guard. Viewers/reviewers cannot write back. The Diff-UI (read-only) is visible to any role that can already read the underlying version history. -
Workspace scoping. UC8 dedup is explicitly the only cross-workspace write in scope (it merges near-duplicates ACROSS workspaces, recording
source_workspace_idlineage). UC1/UC4/UC6/UC3/UC5 write-backs are scoped to the content the acting user can already edit; an edit MUST NOT leak content across a workspace boundary the user cannot otherwise reach. -
AI-invisible-infrastructure invariant. Where AI participates (UC6 AI-suggest proposals, UC8 similarity reasoning), the AI involvement is not surfaced as a user-facing “AI feature” — the user reviews and commits ordinary revisions/merges, and the resulting
content_items/q_a_pairsbehave as if the data was always there (per the AI-visibility policy). Theedit_intenton an AI-suggested-then-user-committed revision is the user’s commit-time selection, indistinguishable in storage from a hand-typed edit.
Open questions (collected)
Section titled “Open questions (collected)”- OQ-59-3 (INV-3): Confirm the KH-DB-only fallback (no auto-create of a
connector='mcp'source_document) forcontent_itemswithsource_document_id IS NULL. Recommend a prod sweep at this PRODUCT stage to size that population before {59.3} TECH locks the write-back adapter. - OQ-59-4 (INV-15): Confirm Diff-UI v1 = minimal revision-diff view (richer markdown side-by-side → v1.1).
- OQ-59-pre-cutover (carried from {59.1} OQ-59-2): Land the
content_history.edit_intent(+ optionalarbitration_inputs)ALTERpre-cutover in the {64.7}/{64.8} window (ratified: yes, zero-cost). The DDL itself is {59.3}/{59.4} + {64.7}/{64.8}; this PRODUCT specifies the behaviour the column enables.
End of PRODUCT. Behaviour invariants INV-1..INV-21. Companion {59.3} TECH is the next chain step:
write-back adapter contract (Spike#3 resolution against cocoindex-write-model R1/R4), the
edit_intent column slice, the per-UC wiring onto arbitrate(), and the Diff-UI build — one
Proposed-change per invariant.