ID-58 {58.1} RESEARCH — Citations polymorphic cite-target + version-on-cite (T11)
ID-58 {58.1} RESEARCH — Citations polymorphic cite-target + version-on-cite
Section titled “ID-58 {58.1} RESEARCH — Citations polymorphic cite-target + version-on-cite”Status: RESEARCH complete — recommendations PENDING Liam ratification (§8). Authored 08/06/2026. Purpose: the FIRST artefact of the ID-58 (T11) spec-chain, started per ID-93 §6.2 (“structurally complete, based on ratified specs” — Liam, 08/06/2026): the T11 citations schema slice must ratify pre-cutover so its DDL can land before the ID-45 (T7) full-corpus re-ingest. RESEARCH-only — no DDL, no code edits here. This doc resolves: (1) the
content_citationsrename-vs-replace disposition that {64.14} deliberately defers to T11, (2) the cite-target polymorphism shape, (3) the version-on-cite substrate, (4) the bl-74 / bl-136 fold-in dispositions.
- Task: ID-58 (
in_progress), priorityshould, deps[41]— RESEARCH-only here. - Hand-off source: ID-64 {64.14} (bid→form rename, naming target
form_*ratified 08/06/2026) renamesbid_responses/bid_response_history/bid_questions+ thesearch_for_bid_responseRPC, but excludescontent_citationsand the (unbuilt)citing_entityenum value — both owned by this Task. - Sibling surfaces: ID-93 register (
specs/ID-93-pre-reingest-intent-gap/RESEARCH.md),themes/canonical-pipeline/reference/v1-completion-sequence.md(Lane B), s299 plan (v1-ledger-mutation-plan-s299.md§3 fold-in registers).
1. Method & verification discipline
Section titled “1. Method & verification discipline”Per the ID-93 adversarial-verification rule and the {58.1} brief: no claim about the
current write surface or schema enters this register without a citation against
scripts/cocoindex_pipeline/flow.py, the migration corpus, the live schema, or a live
row count. database.types.ts is read-restricted in this shell; the schema was instead
read directly off the live staging branch (turayklvaunphgbgscat) via the Supabase
read-only MCP, and cross-checked against the canonical DDL in
supabase/migrations/20260416102457_pre_squash_reconciliation.sql.
1.1 Code-intelligence orientation (recorded)
Section titled “1.1 Code-intelligence orientation (recorded)”gitnexus_query/gitnexus_impactUNAVAILABLE at authoring time —LadybugDB unavailable for knowledge-hub … Database file version: 41, Current build storage version: 40(index mid-rebuild). Per the planner code-intel fallback, the TS/SQL consumer surface was instead mapped via direct grep + file reads + RPC inspection. This is in any case the authoritative path for this Task: ast-dataflow/gitnexus do not cover SQL migrations or Python (per.ast-dataflow/CLAUDE.md), and the entirecontent_citationssurface is SQL (table + trigger + 4 RPCs) plus a handful of TS.from('content_citations')call sites. No internal symbol is being renamed in this RESEARCH artefact, so nogitnexus_impact-before-edit gate is engaged here; it engages at {58.3} TECH / {58.5+} implementation.- Consumer surface (grep, verified): 5 live TS call sites + 1 trigger fn + 4 RPCs read
or write
content_citations(§3.3).
1.2 No external-library API claims in scope (Q-EX2 check)
Section titled “1.2 No external-library API claims in scope (Q-EX2 check)”This RESEARCH cites only internal KH schema (content_citations, content_items,
content_history, q_a_pairs, q_a_pair_history) and internal RPCs. It cites no
third-party library symbol (cocoindex/anthropic/supabase-client method on a non-pinned
major / ts-morph / Zod). The cocoindex pin (cocoindex[postgres]==1.0.7,
requirements.txt) is recorded for completeness, but the pipeline never touches
citations (§2), so no import-and-call empirical check is required by the OQ-3 / Q-EX2
forcing function. Verification load for this Task is the schema reality check (done,
§3) rather than an external-API check.
2. The pipeline never writes citations (re-confirmed C5 — verified-decoupled)
Section titled “2. The pipeline never writes citations (re-confirmed C5 — verified-decoupled)”VERIFIED: grep -n "citation|citing_entity|cited_" scripts/cocoindex_pipeline/flow.py
returns zero matches. Citations are a 100% post-ingest, application-write surface
(the bid-draft path writes them; §3.3). This re-confirms the ID-93 §5.1 finding:
T11 is fully post-ingest and lands no rows at ingestion, so building it after the
ID-45 re-ingest forces no second LLM run. Consequence: the schema slice can be
designed freely now and the table built either pre- or post-cutover — the §6.2 policy
(“structurally complete, based on ratified specs”) is what pulls it pre-cutover, not any
ingest coupling. Coupling class: C4 shape-only (DDL) + C3 id-coupled at the data level
(citations reference content_items ids that renumber at re-ingest — but at 0 rows
today, so renumbering exposure is nil unless UAT lands rows before the run; §6).
3. Current state — content_citations today (cited evidence)
Section titled “3. Current state — content_citations today (cited evidence)”3.1 Table shape (live staging + pre_squash DDL)
Section titled “3.1 Table shape (live staging + pre_squash DDL)”Live columns (staging information_schema.columns, 08/06/2026) — identical to the
canonical DDL at pre_squash_reconciliation.sql:3471–3479:
| Column | Type | Null | Note |
|---|---|---|---|
id | uuid | NO | gen_random_uuid() |
content_item_id | uuid | NO | FK → content_items(id) ON DELETE CASCADE (:5340). The cited side. |
bid_response_id | uuid | NO | FK → bid_responses(id) ON DELETE CASCADE (:5335). The citing side. |
citation_type | text | NO | DEFAULT 'reference'; CHECK ∈ {reference, copied, adapted, inspired} (:3478). |
created_at | timestamptz | YES | now() |
created_by | uuid | YES | FK → auth.users(id) ON DELETE SET NULL (:5345) |
Constraints/indexes/triggers/policies (all in pre_squash):
- UNIQUE
(content_item_id, bid_response_id)(:4297) — the dedup key the MCP tool’s upsertonConflicttargets (§3.3). - Indexes:
idx_content_citations_item,_response,_created_by(:4635–4643). - Triggers
trg_citation_count_insert/delete→update_citation_count()maintainscontent_items.citation_count(:3254–3279,:5243–5247). - RLS: SELECT any authenticated; INSERT/UPDATE editor+admin; DELETE admin
(
:5757–5871).GRANT ALL … TO anonexists (:8289) — aREVOKE-from-anon hygiene item to carry into the redesign migration per the CLAUDE.md function-grant gotcha.
3.2 Key reality check — it is a content_items × bid_responses junction, NOT polymorphic
Section titled “3.2 Key reality check — it is a content_items × bid_responses junction, NOT polymorphic”Today both ends are fixed: the cited side is ALWAYS content_items; the citing side
is ALWAYS bid_response. There is no discriminator column and no citing_entity enum
type in the schema. Verified:
SELECT count(*) FROM pg_type WHERE typname='citing_entity' → 0 (staging). The string
citing_entity appears in the codebase exactly once — as a forward-reference comment
in 20260601180058_id64_origin_kind_rename_form_response.sql:13 (“the T11 citing_entity
enum value 'bid_response' … deliberately OUT of scope here”). So the “enum” {64.14}
excludes is an as-yet-unbuilt construct this Task defines from scratch.
3.3 Live consumer surface (grep — the rename/replace blast radius)
Section titled “3.3 Live consumer surface (grep — the rename/replace blast radius)”Row count: content_citations = 0 on staging (08/06/2026) and prod (ID-93, 07/06/2026).
So all consumers below operate against an empty table — pure code+DDL, zero backfill.
TS write/read call sites (.from('content_citations')):
app/api/procurement/[id]/responses/draft-stream/route.ts:294–299— the sole writer. After a draft response is saved, deletes prior citations for the response then inserts one row per matched content item:{ bid_response_id, content_item_id, citation_type: 'reference', created_by }. Win-rate tracking.lib/mcp/tools/procurement.ts:462–467—record_citationMCP tool. Upserts ononConflict: 'content_item_id,bid_response_id', selects backid, content_item_id, bid_response_id, citation_type. Hard-coded to the 2-FK shape.app/api/items/[id]/effectiveness/route.ts:98–113— reads citations with embeddedbid_responses!inner → bid_questions!inner → workspaces!innerto list which bids cited an item.scripts/mcp-eval/fixtures.ts:478,scripts/mcp-eval/functional-correctness.ts:2276— eval fixtures (delete-by-content_item_id, insert test rows).
SQL surface (pre_squash):
- Trigger fn
update_citation_count()(:3254) — counts citations percontent_item_id, writescontent_items.citation_count. - 4 RPCs read
content_citations cc:get_aggregate_win_rate_stats()(:888),get_content_win_rate(p_content_item_id)(:1135),hybrid_search(...)(:2544),search_for_bid_response(...)(:3083). All join citation →bid_responseoutcome for win-rate analytics, keyed oncontent_item_id(cited) +bid_response_id(citing).
Implication for the redesign: every analytics RPC + the trigger + the win-rate read
path assume (content_item_id, bid_response_id). Any shape change MUST preserve the
content_items-cited × bid_response-citing win-rate path or it breaks 4 RPCs and the
effectiveness UI. This is the single largest constraint on the polymorphism choice (§5).
3.4 Naming note — bid_response → form_response
Section titled “3.4 Naming note — bid_response → form_response”{64.14} renames bid_responses → form_responses (naming target form_* ratified
08/06/2026). If T11 renames content_citations it inherits a now-form_response_id
FK; if it replaces, the new table models the citing side via the new
form_responses table. Either way the citing-side column/enum value should read
form_response, not bid_response, to stay consistent with the ratified rename.
4. Question 1 — content_citations disposition: RENAME vs REPLACE
Section titled “4. Question 1 — content_citations disposition: RENAME vs REPLACE”4.1 Recommendation: REPLACE (drop content_citations, create citations)
Section titled “4.1 Recommendation: REPLACE (drop content_citations, create citations)”Decision: REPLACE. Create a new citations table with the polymorphic cite-target
shape (§5); drop content_citations; re-point the 5 TS call sites, the trigger, and the 4
RPCs. Do not do an in-place column-add rename.
Rationale (why replace beats rename):
- Zero data to preserve. 0 rows on prod + staging. The only argument for rename
(migrate existing rows in place) does not apply. A clean
CREATE … ; DROP …is simpler and leaves no legacy 2-FK columns to deprecate. - The shape change is structural, not cosmetic. Going polymorphic on the cited side
adds a discriminator + (chosen design) drops a NOT-NULL FK in favour of a target ref.
An in-place rename that then ALTERs the columns is strictly more migration steps than a
fresh
CREATE, for an empty table. - {64.14} already excluded it precisely so this could be a clean replace. ID-93 §3.1
flagged “T11 may REPLACE rather than rename … an argument for deciding jointly”; {64.14}
acted on that by leaving
content_citationsand theciting_entityenum out of the bid→form rename. Replacing here is the disposition that exclusion was reserving. - New canonical name
citationsmatches the s299/ID-93 expectation (“content_citations → citationsrename has live consumers + RPCs”, seq-doc Lane B) — replace lands the same target name with a better internal shape.
Cost of replace (the only downside): the 4 RPCs + trigger + 5 TS sites must be
re-pointed in the same migration/PR — but they must be re-pointed under a rename too (the
shape changes regardless), so replace adds no net consumer work. The win-rate RPCs need a
backward-compatible projection of the content_items-cited × form_response-citing path
(§5.4).
4.2 Sequencing with {64.14}
Section titled “4.2 Sequencing with {64.14}”{64.14} (form rename) and {58.x} (citations replace) touch disjoint tables —
{64.14} excludes content_citations explicitly, and the replace’s only dependency on
{64.14} is that the citing-side FK should target the renamed form_responses. So:
land {64.14} first (or concurrently), then the citations replace references
form_responses. If {58.x} lands before {64.14}, the citing FK temporarily targets
bid_responses and the rename sweep updates it — workable but adds a step; prefer
{64.14} first. This is a Task-level ordering note (ID-58 dep on ID-64), not a
subtask cross-Task dep — correct per the sibling-only rule.
5. Question 2 — cite-target polymorphism shape
Section titled “5. Question 2 — cite-target polymorphism shape”The cited side must address content_items OR q_a_pairs OR future kinds (e.g. a
future reference_items / KG node). The citing side stays form_response for v1 but
should be modelled so it, too, can generalise later (bl-136 copy-tracking implies the
citing side could become a q_a_pair or a workspace artefact). Three options evaluated.
5.1 Option (a) — single polymorphic discriminator + target_id (enum + bare uuid)
Section titled “5.1 Option (a) — single polymorphic discriminator + target_id (enum + bare uuid)”cited_kind cited_target_kind NOT NULL (pg enum) + cited_id uuid NOT NULL, no FK on
cited_id.
- + One shape covers all future kinds; adding a kind = one enum value, no DDL on the table. Simplest column set. Natural fit for a generic “cite anything” surface.
- − No referential integrity —
cited_idcannot FK to a single table, so a deletedcontent_item/q_a_pairorphans the citation silently (exactly thematched_content_ids uuid[]orphaning hazard ID-93 §4 flagged). Loses the ON DELETE CASCADE that today keepscitation_counthonest. - − Query ergonomics: every read needs
CASE cited_kind WHEN … JOIN …or a per-kind union; can’t write one declarative join. The 4 win-rate RPCs become discriminator-aware. - − RLS on the cited row’s visibility can’t be expressed as a simple FK-join policy.
5.2 Option (b) — per-target nullable discriminated FK columns + CHECK
Section titled “5.2 Option (b) — per-target nullable discriminated FK columns + CHECK”cited_content_item_id uuid NULL REFERENCES content_items,
cited_q_a_pair_id uuid NULL REFERENCES q_a_pairs, … + a cited_kind enum +
a CHECK that exactly one target column is non-null and matches cited_kind.
- + Real FK integrity per kind — ON DELETE CASCADE per target preserved; no silent
orphans;
citation_counttrigger stays correct. - + Declarative joins per kind; RLS can use the real FK. Postgres-idiomatic (“exclusive-arc” / “polymorphic-via-nullable-FKs” pattern).
- − Adding a future kind = a DDL
ADD COLUMN cited_x_id … REFERENCES x+ CHECK update (not just an enum value). For a roadmap with a small, known set of cite-targets (content_items,q_a_pairs, maybe one KG/reference kind) this is a few migrations over the product’s life — acceptable. - − Wider table; the CHECK constraint is the load-bearing integrity guard and must be written carefully (one-and-only-one-non-null + kind agreement).
5.3 Option (c) — per-target join tables (one citation table per cited kind)
Section titled “5.3 Option (c) — per-target join tables (one citation table per cited kind)”content_item_citations, q_a_pair_citations, … each a clean 2-FK junction.
- + Cleanest integrity; no discriminator.
- − Citation count and “what cited X” become cross-table unions; the win-rate analytics fan out across N tables; bl-136 “what was copied where” needs a union view over all of them. Proliferates tables as kinds grow. Over-normalised for a surface whose whole point is “one place that records any citation”.
5.4 Recommendation: Option (b) — discriminated nullable FK columns + cited_kind enum + CHECK
Section titled “5.4 Recommendation: Option (b) — discriminated nullable FK columns + cited_kind enum + CHECK”Recommended shape for the new citations table:
-- enumscited_target_kind ENUM ('content_item', 'q_a_pair') -- extend per future kindciting_entity_kind ENUM ('form_response') -- v1; extend for bl-136
-- tablecitations ( id uuid PK default gen_random_uuid(), -- citing side (generalised; v1 only form_response) citing_kind citing_entity_kind NOT NULL default 'form_response', citing_form_response_id uuid NULL REFERENCES form_responses(id) ON DELETE CASCADE, -- cited side (polymorphic via nullable FKs) cited_kind cited_target_kind NOT NULL, cited_content_item_id uuid NULL REFERENCES content_items(id) ON DELETE CASCADE, cited_q_a_pair_id uuid NULL REFERENCES q_a_pairs(id) ON DELETE CASCADE, -- version-on-cite (see §6) cited_version integer NULL, -- corpus revision cited; content_items path cited_q_a_pair_version integer NULL, -- q_a_pairs path (q_a_pairs.version) -- payload citation_type text NOT NULL default 'reference' CHECK (citation_type IN ('reference','copied','adapted','inspired')), created_at timestamptz NOT NULL default now(), created_by uuid NULL REFERENCES auth.users(id) ON DELETE SET NULL, -- integrity CHECK ( -- exactly one cited target, matching cited_kind (cited_kind='content_item' AND cited_content_item_id IS NOT NULL AND cited_q_a_pair_id IS NULL) OR (cited_kind='q_a_pair' AND cited_q_a_pair_id IS NOT NULL AND cited_content_item_id IS NULL) ), CHECK ( -- exactly one citing source, matching citing_kind (citing_kind='form_response' AND citing_form_response_id IS NOT NULL) ));Plus: partial UNIQUE indexes per kind to replace today’s
UNIQUE(content_item_id, bid_response_id) dedup key (e.g.
UNIQUE (citing_form_response_id, cited_content_item_id) WHERE cited_kind='content_item'),
and indexes on each cited_*_id + citing_form_response_id.
Why (b) over (a): the decisive factor is the 4 win-rate RPCs + the citation_count
trigger (§3.3). They depend on ON DELETE CASCADE and on being able to join citation →
cited content_item and citation → citing form_response declaratively. Option (a)‘s bare
uuid loses both. The cite-target set is small and known (content_item, q_a_pair, +
maybe one), so (a)‘s “add a kind with no DDL” advantage is marginal, while its orphaning
hazard is exactly the silent-orphan failure mode ID-93 §4 called out. (b) keeps integrity,
keeps the analytics path, and the per-kind ADD COLUMN cost is a handful of migrations
over the product’s life.
Trade-offs accepted with (b):
- FK integrity: strong (per-kind CASCADE) — the deciding advantage.
- Query ergonomics: good for the known kinds; the win-rate RPCs filter
WHERE cited_kind='content_item'and joincited_content_item_id— a one-line change from today’scontent_item_id. Thecitation_counttrigger keys oncited_content_item_id WHERE cited_kind='content_item'(preserves current semantics). - RLS: expressible via the real FKs (view-citation = authenticated; the cited row’s own RLS still governs the joined read).
- Extensibility: future kind =
ADD COLUMN cited_x_id … REFERENCES x+ enum value + CHECK update. Documented as the extension recipe in {58.3} TECH.
6. Question 3 — version-on-cite substrate
Section titled “6. Question 3 — version-on-cite substrate”Requirement: a citation must record which corpus revision of the cited target was cited (so “this bid cited v3 of item X” survives item X being edited to v4).
6.1 Substrate that exists today (verified)
Section titled “6.1 Substrate that exists today (verified)”content_itemsversioning →content_history.content_historyhasversion integer NOT NULL(live staging +pre_squash:3488); each edit snapshots the prior state with an incrementingversion, keyed bycontent_item_id.content_itemsitself carries the current version state. So the revision pointer for acontent_item-cited row is the integercontent_history.versionat cite time. 578content_historyrows today (one-to-one with 578content_items— staging).q_a_pairsversioning →q_a_pair_history.q_a_pairsandq_a_pair_historyBOTH carryversion integer NOT NULL(verified).q_a_pair_historyis the append-only snapshot table. So the revision pointer for aq_a_pair-cited row is the integerq_a_pairs.version(resolvable to aq_a_pair_historysnapshot).
6.2 Recommendation: store the integer version inline on the citation (denormalised pointer), per cited kind
Section titled “6.2 Recommendation: store the integer version inline on the citation (denormalised pointer), per cited kind”Add cited_version integer NULL (content_items path) and cited_q_a_pair_version integer NULL (q_a_pairs path) to the citations table (§5.4). At write time, the citing path
captures the cited target’s current version and stamps it on the citation row. This
is a denormalised revision pointer, not an FK to the history row, because:
- History tables are append-only and keyed
(target_id, version)— an integerversion+ the already-presentcited_*_idis sufficient to resolve the exactcontent_history/q_a_pair_historysnapshot. No extra FK needed; the integer is the natural join key. - It survives target deletion semantics correctly: even if we later soften
ON DELETE CASCADE, the integer records what was cited regardless of whether the history row is later pruned. - Cheap to capture at the existing write site
(
draft-stream/route.tsalready has the matchedcontent_itemsin hand — it can read theirversionin the same query).
Alternative considered (rejected for v1): a hard FK to content_history(id) /
q_a_pair_history(id). Rejected because (a) the history row for the current version may
not exist yet at cite time (history snapshots the prior state on edit), so an FK to a
specific history id is awkward to populate at cite-of-current-version; (b) the integer
(cited_id, version) is a stable, simpler natural pointer. Revisit if a strict FK is
wanted at {58.3}.
6.3 Relationship to bl-74 — version-on-cite is why bl-74 must land first for the q_a_pair path
Section titled “6.3 Relationship to bl-74 — version-on-cite is why bl-74 must land first for the q_a_pair path”For the content_item cited path, version-on-cite is self-contained (the
content_history.version integer is all that’s needed; nothing in bl-74 blocks it).
For the q_a_pair cited path, version-on-cite interacts with bl-74: a citation that
records “cited v2 of q_a_pair Y” is only fully auditable if the q_a_pair_history snapshot
for v2 carries the lineage fields. bl-74 verified (live staging, 08/06/2026):
q_a_pair_history has 0 of superseded_by / source_workspace_id (it has version);
q_a_pairs (live) has both. So the history snapshot drops the two lineage columns on
every version — exactly bl-74. This matters to T11 because a version-on-cite pointer into
q_a_pair_history resolves to a snapshot missing the lineage needed to answer “what
workspace authored the version this bid cited, and what superseded it” — the bl-136
copy-tracking question. See §7.1.
7. Question 4 — fold-in dispositions (s299 §3, ID-93)
Section titled “7. Question 4 — fold-in dispositions (s299 §3, ID-93)”7.1 bl-74 — q_a_pair_history snapshot superseded_by + source_workspace_id
Section titled “7.1 bl-74 — q_a_pair_history snapshot superseded_by + source_workspace_id”- Backlog status:
ready(flipped by ID-93 §6.5). Effort: ~30 min (2ADD COLUMN- trigger body update), per the backlog entry. Verified gap is real (§6.3).
- Disposition: FOLD IN as a T11 dependency, but LAND IT VIA THE ID-45 / ID-64 LANE, NOT
inside the T11 feature build. ID-93 §6.5 + seq-doc Lane A already re-homed bl-74 to
“before the ID-45 promotion step, {64.8} gate G3” — because
q_a_pair_historyis append-only, so if the columns land after ID-45 fills the history, the lineage is lost forever for those rows. T11 must not re-own the landing; it consumes the columns. T11’s stake: the version-on-cite q_a_pair path (§6.3) and bl-136 copy-tracking (§7.2) both depend on bl-74 having landed. Action: record in {58.3} TECH a hard pre-req note — “bl-74 columns MUST exist onq_a_pair_historybefore the q_a_pair cite-target path is exercised” — and confirm with the Orchestrator that {64.8} G3 / the ID-45 promotion ordering still owns the actual DDL. If for any reason that ordering slips, T11 escalates rather than silently landing the columns late. - Net: bl-74 is a consumed dependency, dispositioned to the pre-promotion lane; T11 owns only the dependency assertion, not the migration.
7.2 bl-136 — Q&A copy-tracking “what was copied where” UI surface
Section titled “7.2 bl-136 — Q&A copy-tracking “what was copied where” UI surface”- Backlog status:
spec_needed, prioritymedium, effortnull(the brief asks for an estimate). The “citation-on-paste” data capture is absorbed by T11 (per the backlog note: “citation-on-paste absorbed by canonical-pipeline T11”); bl-136 is the wider UI that reads it. - What T11 provides for it: the polymorphic
citationstable (§5.4) is precisely the substrate “what was copied where” queries —citation_type ∈ {copied, adapted, inspired}already distinguishes copy-events from plain references, and the polymorphic cited side lets a copy be tracked from aq_a_pairas well as acontent_item. So the T11 schema slice unblocks bl-136’s data layer; the UI itself is a separate feature build. - Disposition: SPLIT. (i) Data capture (write copy-events into
citationswith the rightcitation_type+ cited/citing kinds) — fold into the T11 build. (ii) The UI surface (“what was copied where” views/filters) — keep as a separate post-T11 feature, dependent on the T11 table. Do not scope the UI into ID-58’s pre-cutover schema slice (the schema slice is what must ratify pre-cutover; the UI is post-cutover). - Effort estimate (the brief’s explicit ask):
- Data-capture half (into T11): ~0.5 session — it is mostly “set
citation_typecorrectly + capture cited_kind/version at the copy/paste write site”; rides the T11 write-path work. - UI half (separate feature): ~1–1.5 sessions — a read-only “copy provenance” panel
/ filter over the
citationstable (one new read RPC or TanStack query + one panel component + tests). Recommend logging this as bl-136 effort = ~1.5 sessions total (0.5 data in T11 + ~1 UI), UI half tracked as its own post-cutover Lane-B/Lane-C item.
- Data-capture half (into T11): ~0.5 session — it is mostly “set
8. Recommendations summary & Liam decision queue
Section titled “8. Recommendations summary & Liam decision queue”The Orchestrator/Liam write the ledger, not this doc. These are proposals.
content_citationsdisposition → REPLACE with a newcitationstable (§4). 0 rows ⇒ cleanCREATE/DROP; {64.14} already excluded it to reserve this. Needs Liam ratification (it is the disposition {64.14} deferred).- Polymorphism shape → Option (b): discriminated nullable FK columns +
cited_kindenum + one-and-only-one CHECK (§5.4). Preserves FK integrity / CASCADE / the 4 win-rate RPCs; cite-target set is small+known so the per-kindADD COLUMNcost is acceptable. - version-on-cite → inline integer version pointer per cited kind (
cited_versionfor content_items viacontent_history.version;cited_q_a_pair_versionviaq_a_pairs.version), denormalised, not a history-row FK (§6). - citing side → model as
form_response(notbid_response), generalised via aciting_entity_kindenum so bl-136 can later add citing kinds (§3.4, §5.4). - bl-74 → consumed dependency, landed pre-ID-45-promotion via {64.8} G3 (NOT re-owned by T11); T11 records the hard pre-req assertion (§7.1).
- bl-136 → SPLIT: data-capture folds into T11 (~0.5 session); UI is a separate post-cutover feature (~1 session); logged effort ≈ 1.5 sessions (§7.2).
- Pre-cutover DDL slice (ready to ratify) = the
citationstable + 2 enums + per-kind CHECKs + partial-unique dedup indexes + per-kind indexes + re-pointedupdate_citation_count()trigger + re-pointed 4 RPCs +REVOKE … FROM anonhygiene + dropcontent_citations. This is the slice the §6.2 policy lands before ID-45 cutover once {58.2} PRODUCT + {58.3} TECH ratify. The TS consumer re-point (5 call sites) can ride the same migration PR or a fast-follow — flag at {58.3}.
8.1 Open questions needing Liam (route via Orchestrator)
Section titled “8.1 Open questions needing Liam (route via Orchestrator)”- OQ-58-1 (ratify the replace). Confirm REPLACE over rename. (Recommend REPLACE.)
- OQ-58-2 (q_a_pair cite-target in v1?). Should the v1 schema slice include the
q_a_paircited kind now (enum value +cited_q_a_pair_idcolumn), or land content_item-only andADD COLUMNthe q_a_pair path later? Recommend include the enum/column now (it is the whole point of “polymorphic”, and the §6.2 policy wants the structural shape complete pre-cutover) — but its use is gated on bl-74 landing (§7.1). Liam call on whether to ship the column dormant vs defer it. - OQ-58-3 (citing-side generalisation depth). v1 needs only
form_responseciting. Confirm whether to bake theciting_entity_kindenum now (cheap future-proofing for bl-136) or keep the citing side a single FK and generalise later. Recommend the enum now. - OQ-58-4 (DDL slice timing vs TS re-point). Confirm the TS consumer re-point (5 sites) may fast-follow the DDL, or must be same-PR. Affects {58.4} PLAN decomposition.
- OQ-58-5 (ID-58 dep on ID-64 ordering). Recommend {64.14} (form rename) lands before
the citations replace so the citing FK targets
form_responsesdirectly (§4.2). Confirm the Task-level ordering (ID-58 after ID-64’s {64.14}).
8.2 Recommendation to Orchestrator on the spec chain
Section titled “8.2 Recommendation to Orchestrator on the spec chain”- {58.2} PRODUCT — RECOMMENDED. The cite-target behaviour, copy-vs-reference semantics, and version-on-cite are behaviourally non-trivial and user-facing (bl-136). A PRODUCT spec with numbered invariants (e.g. “a citation records exactly one cited target of a declared kind”; “a citation records the cited target’s version at cite time”) is warranted.
- {58.3} TECH — RECOMMENDED. Multi-subsystem (table + 2 enums + CHECKs + trigger + 4 RPCs + 5 TS sites), and the migration is risk-bearing (drop+create + RPC re-point under the §6.2 pre-cutover gate). Fresh-Planner pass against the ratified PRODUCT.
- {58.4} PLAN — LIKELY (compound: DDL slice + RPC re-point + TS re-point + bl-136 data-capture are ≥3 chained slices; est. > 2h). Confirm at {58.3} hand-off.
9. Verification appendix (evidence index)
Section titled “9. Verification appendix (evidence index)”| Claim | Evidence |
|---|---|
| Pipeline never writes citations | grep citation… scripts/cocoindex_pipeline/flow.py → 0 matches (08/06/2026) |
content_citations = 0 rows | staging count(*) = 0 (08/06/2026); prod 0 (ID-93, 07/06/2026) |
| Table is 2-FK junction, not polymorphic | information_schema.columns (staging) + pre_squash:3471–3479 |
No citing_entity enum exists | pg_type WHERE typname='citing_entity' → 0 (staging); only a comment in 20260601180058_…sql:13 |
content_history.version is the corpus-revision pointer | information_schema.columns (staging) + pre_squash:3488 |
q_a_pair_history lacks superseded_by/source_workspace_id (bl-74) | staging: has_superseded_by=0, has_source_ws=0, has_version=1; q_a_pairs has both |
| 5 TS consumers + trigger + 4 RPCs | grep .from('content_citations') + content_citations cc in pre_squash (:888/1135/2544/3083, trigger :3254) |
| {64.14} excludes content_citations + citing_entity | ledger show task 64 {64.14} details; seq-doc Lane A row |
| cocoindex pin (no external-API check needed) | requirements.txt → cocoindex[postgres]==1.0.7; pipeline never touches citations |
| gitnexus unavailable at authoring | LadybugDB … version 41 vs 40 error (recorded §1.1) |