Skip to content

ID-58 {58.4} PLAN — Citations polymorphic cite-target + version-on-cite (T11)

ID-58 {58.4} PLAN — Citations polymorphic cite-target + version-on-cite

Section titled “ID-58 {58.4} PLAN — Citations polymorphic cite-target + version-on-cite”

Status: PLAN draft — authored 09/06/2026 (Session 330) by a FRESH planner instance (Q-PLANNER-2: NOT the {58.1}/{58.2}/{58.3} author). Reads the ratified {58.2} PRODUCT (25 invariants Inv-1..25) and {58.3} TECH (25 proposed-changes, 1:1) in full, plus the {58.1} RESEARCH. Decomposes the ratified DDL slice + consumer re-point into {58.5+} implementation Subtasks. Folds the four S330 Orchestrator-ratified decisions (below).

No DDL applied here — spec/decomposition only. The migration is authored + applied by {58.5} via the Supabase CLI, staging-first; prod push is a Liam gate.

Replace the fixed content_items × bid_responses junction content_citations (0 rows, staging + prod) with a polymorphic citations table: per-target discriminated nullable FK columns + a cited_target_kind enum + one-of CHECK; a citing_entity_kind enum (form_response only valid v1); inline integer version-on-cite per kind; NEW native Anthropic citation-span columns (S330 decision #1); per-kind partial-unique dedup indexes; RLS + REVOKE-from-anon. Re-point the update_citation_count() trigger + 4 win-rate RPCs + 8 TS/Python consumer sites (the TECH.md 5 + 3 additional sites this PLAN’s grep sweep surfaced — see §Consumer surface correction). DROP content_citations last. All in one migration PR (Inv-25 / OQ-58-4 SAME-PR).

Code-intelligence orientation (recorded, S330 09/06/2026). Both code-intel tools UNAVAILABLE at planning time, consistent with {58.1}/{58.3}: gitnexus LadybugDB version 41 vs build 40 (index mid-rebuild, confirmed via the PreToolUse hook returning only 5 fuzzy draft* symbols — no citation/citations/cite_content symbol resolved); ccc search "citations win-rate trigger RPC" errored (cocoindex-code daemon traceback). This is in any case the authoritative path for this SQL-heavy surface: ast-dataflow/ gitnexus do not index SQL migrations or Python (per .ast-dataflow/CLAUDE.md), and the citations surface is 1 table + 1 trigger fn + 4 RPCs (SQL) + a handful of TS/Python .from('content_citations') sites. The consumer surface was therefore (re-)mapped by direct grep -rn "content_citations" app/ lib/ scripts/ + file reads + migration reads — the authoritative path. gitnexus orientation: no live symbol index available (LadybugDB v41/build v40; ccc daemon down) — SQL/Python/TS surface mapped via grep + file/migration read. The {58.5+} implementation Subtasks each carry a code-intel discipline block: the SQL/Python sites use grep + migration-read scope containment (git diff --name-only is the authoritative scope fallback in worktrees per CLAUDE.md gitnexus_detect_changes() gotcha); the TS sites (writer, MCP tool, effectiveness reader) run ast-dataflow callers / references + gitnexus_impact (if the index recovers) before editing the symbols they touch.

Architecture decisions (the four S330 ratifications folded in)

Section titled “Architecture decisions (the four S330 ratifications folded in)”

D-S330-1 — Citations-span columns (NEW; Liam ratified 09/06/2026). The native Anthropic citation spans that lib/ai/draft.ts already extracts via lib/citations.ts:extractCitedResponse (→ CitationEntry in types/procurement-metadata.ts:7-15: cited_text, source_index (= Anthropic search_result_index), start_block_index, end_block_index) currently persist only in bid_responses.metadata.citations_data JSONB. The relational table writes 1 row per matched content item (route.ts:286 maps matchedContent), NOT 1 row per Anthropic citation — a lossy FK+version projection. ADD to the citations DDL slice four nullable span columns: cited_text text, cited_location_kind text CHECK ('block'|'char'|'page'), cited_start int, cited_end int. The draft-stream writer ({58.6}) maps EACH CitationEntry → one citations row: source_indexmatchedContent[idx].idcited_content_item_id (the mapping lib/citations.ts:63 already resolves), cited_textcited_text, cited_location_kind='block', start_block_indexcited_start, end_block_indexcited_end. This makes citations the authoritative span store, not a lossy projection. The matched-content rows that yield no Anthropic citation are still recorded (citation_type='reference', span columns NULL) so win-rate cardinality is preserved (citation_count counts matched items, not spans — see §Cardinality note). A TECH.md ADDENDUM block (“PC-26 / R3 — span-capture columns + per-citation writer cardinality”) is authored same-PR by {58.6} to keep the spec 1:1 and source-of-truth.

D-S330-2 — OQ-58-6 = RENAME the cite_content arg. The LLM-facing inputSchema arg bid_response_idform_response_id (lib/mcp/tools/procurement.ts:425), same PR. The tool name cite_content is UNCHANGED (preserves mcp-fixture-sync.test.ts TOOL_COUNT / CANONICAL_TOOL_NAMES). The rename propagates to the 2 eval invocation sites (fixtures.ts:641, functional-correctness.ts:2045) — owned by {58.9}.

D-S330-3 — HARD GATE on {64.14}. ID-58’s migration ({58.5}) CANNOT apply until ID-64 {64.14} (bid_responses → form_responses table rename) is done: citing_form_response_id FKs form_responses, which does not exist yet (verified 09/06/2026: ID-64 in_progress, {64.14} = pending; no form_responses table in the migration corpus). {58.5}‘s first action is a gate-confirm of {64.14} done; if not done, {58.5} STOPS and escalates (do NOT stop-gap target bid_responses). This is a cross-Task dependency expressed at the Task level (ID-58 → ID-64); the Subtask records below carry it as a crossTaskGate note on {58.5}, NOT as a sibling Subtask dependency (sibling-only rule).

D-S330-4 — q_a_pair path ships DORMANT (D1). The q_a_pair cited column/enum/version column/index all ship structurally present in v1 but UNUSED. Their use (writing q_a_pair-cited rows in production) is gated on bl-74 (the q_a_pair_history superseded_by + source_workspace_id lineage columns) landing first, via the ID-45 pre-promotion lane {64.8} G3 — NOT re-owned here (Inv-15). T11 only asserts the dependency. This is a second cross-Task gate (bl-74), again Task-level, carried as a dormantGate note, not a sibling dep.

D-arch — Version substrate (R1, from {58.3}). There is NO content_items.version / q_a_pairs.version column. The cite-time version is COALESCE((SELECT MAX(version) FROM content_history WHERE content_item_id = X), 0) for the content_item path (verified: pre_squash:155 auto_version_content_history keys on MAX(version) FROM content_history). The writer ({58.6}) reads the history MAX, not a column. The dormant q_a_pair version read (MAX(version) FROM q_a_pair_history) is deferred with D1.

Consumer surface correction (load-bearing — PLAN grep sweep, 09/06/2026)

Section titled “Consumer surface correction (load-bearing — PLAN grep sweep, 09/06/2026)”

TECH.md §Context lists 5 TS consumer sites. A fresh grep -rn "content_citations" app/ lib/ scripts/ surfaced 3 additional sites + 2 comment-only references that the DROP (Inv-25 “no dangling consumer”) MUST clear:

SiteKindRe-pointOwner
app/api/procurement/[id]/responses/draft-stream/route.ts:286-306writer (real)per-citation cardinality + spans + version{58.6}
lib/mcp/tools/procurement.ts:419-490MCP cite_content (real)columns + arg rename + formatter{58.7}
lib/mcp/formatters/procurements.ts:170-188CitationResult/formatCitationfield rename{58.7}
app/api/items/[id]/effectiveness/route.ts:97-117effectiveness reader (real)columns + embed alias{58.8}
scripts/mcp-eval/fixtures.ts:478, 641eval delete + cite_content arg (real)columns + arg rename{58.9}
scripts/mcp-eval/functional-correctness.ts:2045, 2276eval cite_content arg + cleanup delete (real)columns + arg rename{58.9}
scripts/export-user-data.ts:514, 715, 738, 760GDPR export (real, NEW).from('citations') + type field rename{58.10}
scripts/cleanup-stale-test-artifacts.ts:185test cleanup tuple (real, NEW)['citations','cited_content_item_id']{58.10}
scripts/wipe-procurement-responses.ts:358comment only (NEW)comment text → citations{58.10}
lib/quality/quality-score.ts:29comment only (NEW)comment text → citations{58.10}

NOTE on functional-correctness.ts:2276: TECH.md described this as “insert a test citation row”; the verified reality is a .delete().eq('content_item_id', id) cleanup. The re-point is mechanical either way (.from('citations').delete().eq('cited_content_item_id', id)).

Cardinality note (resolves Inv-14 vs D-S330-1 interaction)

Section titled “Cardinality note (resolves Inv-14 vs D-S330-1 interaction)”

The OLD writer wrote 1 row per matched item. D-S330-1’s writer writes 1 row per Anthropic CitationEntry, PLUS retains 1 reference row per matched item that produced no Anthropic citation (so win-rate coverage is not lost when the LLM cites only a subset). The per-kind partial-unique index UNIQUE (citing_form_response_id, cited_content_item_id) WHERE cited_kind='content_item' means a single (form_response, content_item) pair is ONE row — so multiple Anthropic spans against the same content item COLLAPSE to one row via upsert, keeping the LAST span (or the writer aggregates spans before insert). {58.6} MUST resolve this explicitly (the {58.6} brief specifies: upsert per (form_response, content_item); if multiple CitationEntry resolve to the same content item, write the first/primary span and the row still counts once for citation_count). citation_count (Inv-14) therefore still counts DISTINCT cited content items per the partial-unique index — unchanged semantics. The span columns are best-effort capture of the primary span per cited-item row; full multi-span fidelity remains in JSONB. This keeps Inv-14 and the 4 RPCs (which COUNT(DISTINCT cited_content_item_id)) regression-free.

{58.5} DDL migration (staging) ← cross-Task GATE: {64.14} done (D-S330-3)
│ (serial root — owns the ONE migration file; all TS/Python re-points depend on type regen post-apply)
├── {58.6} writer re-point + spans + version + TECH ADDENDUM (route.ts, draft-stream)
├── {58.7} cite_content MCP tool + arg rename + formatter (procurement.ts, procurements.ts)
├── {58.8} effectiveness reader re-point (effectiveness/route.ts)
├── {58.10} 3 script consumers + 2 comments (export-user-data, cleanup-stale, wipe, quality-score)
└── {58.9} mcp-eval fixtures + functional-correctness (depends 58.5 + 58.7 — consumes the arg rename)
{58.11} DROP content_citations + grep-clean + full regression gate (depends 58.6,58.7,58.8,58.9,58.10)
{58.12} staging verification + RPC/trigger behaviour + Liam prod-push handoff (depends 58.11)

Wave plan (parallel-vs-serial):

  • Wave 1 (serial root): {58.5} alone. Owns the single migration file; nothing else can re-point until types regenerate against the applied staging schema. Gate-confirms {64.14} first.
  • Wave 2 (parallel — disjoint file ownership): {58.6}, {58.7}, {58.8}, {58.10} all depend only on {58.5} and touch DISJOINT files (writer route / MCP tool+formatter / effectiveness route / scripts). Dispatch concurrently (worktree-isolated, cherry-pick).
  • Wave 3 (serial after 58.7): {58.9} (mcp-eval) consumes the {58.7} arg rename (form_response_id), so it depends on {58.7} as well as {58.5}. Run after Wave 2’s 58.7 lands. (Could parallelize with the tail of Wave 2 if 58.7 finishes first.)
  • Wave 4 (serial): {58.11} DROP + grep-clean + bun build/bun lint/bun run test — depends on ALL re-points landed (58.6/7/8/9/10), because the DROP must be last (Inv-25).
  • Wave 5 (serial): {58.12} staging verification + behaviour tests + Liam prod-push gate handoff — depends on {58.11}.

File-ownership conflicts: none within a wave. {58.7} owns both procurement.ts and procurements.ts (tool + its formatter) so they cannot race. {58.9} owns both mcp-eval files. {58.10} owns all 4 script/comment sites. {58.5} owns the sole migration file and is the only DDL author (no other subtask runs db push).

RiskImpactMitigation
{64.14} not done at {58.5} applyHighD-S330-3 hard gate: {58.5} gate-confirms + STOPS/escalates; never targets bid_responses
RPC RETURNS drift breaks CREATE OR REPLACEHigh{58.5} re-points against authoritative latest bodies (id84 20260606194939; id197 20260530121355), preserves RETURNS byte-for-byte ({58.3} R3)
Span columns balloon row count / break win-rate COUNT(DISTINCT)MedCardinality note: upsert per (form_response, content_item); count stays DISTINCT-item
q_a_pair dormant path exercised before bl-74MedD-S330-4 dormantGate: no production q_a_pair write until bl-74; {58.12} asserts
Type regen stale at TS re-pointMed{58.5} regenerates database.types.ts after staging apply, before Wave 2 dispatch
project-ref drift to prodHigh{58.5} first action: cat supabase/.temp/project-ref + relink staging (turayklvaunphgbgscat) per CLAUDE.md
anon grant survivesMedInv-24: explicit REVOKE + {58.12} has_table_privilege('anon',...) assertion; migration-revoke-guard.yml lint

Open questions (carried to Orchestrator / Liam)

Section titled “Open questions (carried to Orchestrator / Liam)”
  • TECH ADDENDUM ownership: {58.6} authors the “PC-26 / R3” ADDENDUM block into TECH.md same-PR (per D-S330-1). Confirm the Orchestrator wants the span columns folded into the existing TECH.md as an addendum (recommended — keeps 1:1) rather than a new PC-26 invariant in PRODUCT.md (which would break the ratified 25-invariant count).
  • Multi-span fidelity: the cardinality note keeps ONE span per cited-item row (primary). If full multi-span relational fidelity is later wanted, that is a post-v1 ADD (a citation_spans child table) — flagged, not scoped here.
  • Inv-15 / R2 ordering: confirm {64.14} and {64.8} G3 (bl-74) still own their DDL and land before {58.5} / before the q_a_pair path is exercised.

Subtask records — see the {58.5+} TM-shape JSON returned to the Orchestrator

Section titled “Subtask records — see the {58.5+} TM-shape JSON returned to the Orchestrator”

The 8 implementation Subtasks ({58.5}–{58.12}) are returned as TM-shape JSON for the Orchestrator to append to docs/reference/task-list.json. Each carries a load-bearing details dispatch brief (file paths, line refs, the code-intel discipline block, spec-slice references PRODUCT Inv-N / TECH PC-N), a pending status, sibling-only dependencies, and a one-line testStrategy. The {58.5} migration record carries the {64.14} cross-Task gate note explicitly.