Skip to content

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-canonical TECH.md (05/06/2026, reconciled below — see “TECH.md reconciliation”). Carries the Liam 08/06/2026 ratifications: edit_intent lives on a content_history.edit_intent text column (sidecar .md REJECTED); 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.


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.


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 / arbitrate surface — greenfield. grep -rniE 'edit[_]?intent|arbitrat' over lib/ app/ components/ types/ hooks/ contexts/ returns ZERO hits (08/06/2026). T9 is greenfield implementation against a ratified rule (the pre-canonical TECH.md T-1..T-5, commit 12a20963), NOT a re-point onto landed code.
  • Live content_item edit write-back surface — EXISTS, no intent. app/api/items/[id]/route.ts guards with getAuthorisedClient(['admin','editor']), validates transitions, and inserts a content_history row with change_type='edit' (route.ts ~L746–760) via the content_history_auto_version() trigger. No edit_intent is 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 the uuid5 PK from it. There is NO original_path column.
  • 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-aedit_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-bArbitration truth table is commutative + idempotent, cosmetic is the unit element: any merge that includes a data or structural intent → merged data; cosmetic + cosmeticcosmetic. N-way reduces pairwise, order-independent.INV-9, INV-10.
C-cThe v1 safe default (RATIFIED-S243 Item 6): conservative re-run over silent skip — any concurrent edit including data/structuraldata. False-positive re-runs are cheap; false-negative skips silently corrupt corpus quality.INV-9 rationale.
C-dFiner arbitration is v1.1 (e.g. structural+structuralstructural 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-eNull/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-fArbitration 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-gAudit 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-bStorage 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-ccontent_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-dThe 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_typeadded|removed|modified|unchanged, statuspending_review|applied|dismissed, diff_modeqa|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. 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).


  1. UC1 (typo fix) / UC4 (paragraph rewrite) — file-backed content edit. When an admin or editor saves an in-platform edit to a content_item whose source_document.storage_path resolves to a walked source file, the platform writes the edit to BOTH (a) the content_items row (DB) and (b) the on-disk source file at that storage_path, in a single user-visible save action. The source-relative path is preserved byte-for-byte across the write so the file’s uuid5 identity is stable on the next POST /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.

  2. 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_intent is resolved (arbitrated — INV-9) BEFORE either write and stamped on the resulting content_history row (INV-7).

  3. UC1/UC4 — content_item without a source_document (OQ-59-3 disposition). Some content_items have source_document_id IS NULL (e.g. items created via MCP create_content_item, or legacy imports). For these, the edit write-back is KH-DB-only: the edit is written to the content_items row + a content_history row (with edit_intent), and NO file is written. The platform MUST NOT auto-create a source_document / mint a connector='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 reads content_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 NULL population 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).
  4. 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_pairs and a new q_a_pair_history snapshot row; it does NOT write any source file (Q&A pairs are KH-DB-only in v1 — file-sidecar materialisation is DEFERRED-v1.1). The edit_intent is 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 as content_history).

  5. 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_pairs after 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_diffs review flow, not part of this Task’s edit write-back. The edit_intent on the committed revision is the user’s commit-time selection (single value, no arbitration).

  6. 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).

  7. UC5 (bid→Q&A promotion) — KH-DB-only with lineage. Promoting a bid/form response to a Q&A pair creates a q_a_pairs draft 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 (the form_* naming per the {64.14} rename — cross_doc_link, INV-18); the user reviews the draft before it is published.

  8. 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 in q_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_history superseded_by + source_workspace_id lineage 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”
  1. 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 + cosmeticcosmetic; any combination that includes a data OR structural intent → 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.

  2. 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 (cosmetic is the unit element; any data/structural present ⇒ data).

  3. 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 on structural vs data (both trigger re-classification). The override-UX and finer (structural-preserving) arbitration are v1.1 — a v1.1 PRODUCT will define them.

  4. Null / unknown intent fallback. If a save arrives with edit_intent null, undefined, or a value outside the closed CV (most likely an older client predating the capture UX), the platform treats that side as cosmetic (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.)

  5. edit_intent persistence + audit. The merged (or single) edit_intent is persisted on the revision record: a new content_history.edit_intent text column for content-item edits (and the equivalent on the Q&A revision surface for UC6). When arbitration ran, the per-actor inputs are persisted (a content_history.arbitration_inputs jsonb array of {actor, intent} pairs) so the arbitration is forensically reconstructable and multi-actor attribution survives (there is no separate actor_id column — created_by carries the single/primary actor, arbitration_inputs carries the full set). Pre-edit-feature history rows legitimately have edit_intent = NULL; the column is write-only-forward with no backfill. edit_intent is 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)”
  1. Diff-UI source. The user-edit Diff-UI compares two revisions drawn from the existing version model — content_history rows for content items, q_a_pair_history rows for Q&A pairs — selected from the version-history list. It does NOT read source_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.

  2. 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 new edit_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.
  3. 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).

  4. Diff-UI does NOT gate, and is NOT gated by, the edit_intent storage decision. The Diff-UI works off whatever revision rows exist regardless of where edit_intent is stored; conversely the edit_intent column lands independently of the Diff-UI. source_document_diffs remains 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”
  1. 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_history lineage 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 (the edit_intent column before the UC1/UC4/UC6 slices that stamp it) — no cross-Task subtask dep is required, so no escalation.

  2. Authorisation. Content-item edit write-back (UC1/UC4/UC3) is restricted to admin / editor roles (the existing getAuthorisedClient(['admin','editor']) guard on the items route); failures route via authFailureResponse(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.

  3. Workspace scoping. UC8 dedup is explicitly the only cross-workspace write in scope (it merges near-duplicates ACROSS workspaces, recording source_workspace_id lineage). 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.

  4. 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_pairs behave as if the data was always there (per the AI-visibility policy). The edit_intent on an AI-suggested-then-user-committed revision is the user’s commit-time selection, indistinguishable in storage from a hand-typed edit.


  • OQ-59-3 (INV-3): Confirm the KH-DB-only fallback (no auto-create of a connector='mcp' source_document) for content_items with source_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 (+ optional arbitration_inputs) ALTER pre-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.