Skip to content

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_citations rename-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), priority should, deps [41] — RESEARCH-only here.
  • Hand-off source: ID-64 {64.14} (bid→form rename, naming target form_* ratified 08/06/2026) renames bid_responses/bid_response_history/bid_questions + the search_for_bid_response RPC, but excludes content_citations and the (unbuilt) citing_entity enum 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).

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_impact UNAVAILABLE 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 entire content_citations surface 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 no gitnexus_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:

ColumnTypeNullNote
iduuidNOgen_random_uuid()
content_item_iduuidNOFK → content_items(id) ON DELETE CASCADE (:5340). The cited side.
bid_response_iduuidNOFK → bid_responses(id) ON DELETE CASCADE (:5335). The citing side.
citation_typetextNODEFAULT 'reference'; CHECK ∈ {reference, copied, adapted, inspired} (:3478).
created_attimestamptzYESnow()
created_byuuidYESFK → 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 upsert onConflict targets (§3.3).
  • Indexes: idx_content_citations_item, _response, _created_by (:4635–4643).
  • Triggers trg_citation_count_insert/deleteupdate_citation_count() maintains content_items.citation_count (:3254–3279, :5243–5247).
  • RLS: SELECT any authenticated; INSERT/UPDATE editor+admin; DELETE admin (:5757–5871). GRANT ALL … TO anon exists (:8289) — a REVOKE-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–299the 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–467record_citation MCP tool. Upserts on onConflict: 'content_item_id,bid_response_id', selects back id, 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 embedded bid_responses!inner → bid_questions!inner → workspaces!inner to 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 per content_item_id, writes content_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_response outcome for win-rate analytics, keyed on content_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_responseform_response

Section titled “3.4 Naming note — bid_response → form_response”

{64.14} renames bid_responsesform_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):

  1. 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.
  2. 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.
  3. {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_citations and the citing_entity enum out of the bid→form rename. Replacing here is the disposition that exclusion was reserving.
  4. New canonical name citations matches the s299/ID-93 expectation (“content_citations → citations rename 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).

{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 integritycited_id cannot FK to a single table, so a deleted content_item / q_a_pair orphans the citation silently (exactly the matched_content_ids uuid[] orphaning hazard ID-93 §4 flagged). Loses the ON DELETE CASCADE that today keeps citation_count honest.
  • 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_count trigger 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:

-- enums
cited_target_kind ENUM ('content_item', 'q_a_pair') -- extend per future kind
citing_entity_kind ENUM ('form_response') -- v1; extend for bl-136
-- table
citations (
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 join cited_content_item_id — a one-line change from today’s content_item_id. The citation_count trigger keys on cited_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_items versioning → content_history. content_history has version integer NOT NULL (live staging + pre_squash:3488); each edit snapshots the prior state with an incrementing version, keyed by content_item_id. content_items itself carries the current version state. So the revision pointer for a content_item-cited row is the integer content_history.version at cite time. 578 content_history rows today (one-to-one with 578 content_items — staging).
  • q_a_pairs versioning → q_a_pair_history. q_a_pairs and q_a_pair_history BOTH carry version integer NOT NULL (verified). q_a_pair_history is the append-only snapshot table. So the revision pointer for a q_a_pair-cited row is the integer q_a_pairs.version (resolvable to a q_a_pair_history snapshot).

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 integer version + the already-present cited_*_id is sufficient to resolve the exact content_history / q_a_pair_history snapshot. 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.ts already has the matched content_items in hand — it can read their version in 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 (2 ADD 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_history is 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 on q_a_pair_history before 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, priority medium, effort null (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 citations table (§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 a q_a_pair as well as a content_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 citations with the right citation_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_type correctly + 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 citations table (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.

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.

  1. content_citations disposition → REPLACE with a new citations table (§4). 0 rows ⇒ clean CREATE/DROP; {64.14} already excluded it to reserve this. Needs Liam ratification (it is the disposition {64.14} deferred).
  2. Polymorphism shape → Option (b): discriminated nullable FK columns + cited_kind enum + 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-kind ADD COLUMN cost is acceptable.
  3. version-on-cite → inline integer version pointer per cited kind (cited_version for content_items via content_history.version; cited_q_a_pair_version via q_a_pairs.version), denormalised, not a history-row FK (§6).
  4. citing side → model as form_response (not bid_response), generalised via a citing_entity_kind enum so bl-136 can later add citing kinds (§3.4, §5.4).
  5. 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).
  6. 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).
  7. Pre-cutover DDL slice (ready to ratify) = the citations table + 2 enums + per-kind CHECKs + partial-unique dedup indexes + per-kind indexes + re-pointed update_citation_count() trigger + re-pointed 4 RPCs + REVOKE … FROM anon hygiene + drop content_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_pair cited kind now (enum value + cited_q_a_pair_id column), or land content_item-only and ADD COLUMN the 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_response citing. Confirm whether to bake the citing_entity_kind enum 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_responses directly (§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.

ClaimEvidence
Pipeline never writes citationsgrep citation… scripts/cocoindex_pipeline/flow.py → 0 matches (08/06/2026)
content_citations = 0 rowsstaging count(*) = 0 (08/06/2026); prod 0 (ID-93, 07/06/2026)
Table is 2-FK junction, not polymorphicinformation_schema.columns (staging) + pre_squash:3471–3479
No citing_entity enum existspg_type WHERE typname='citing_entity' → 0 (staging); only a comment in 20260601180058_…sql:13
content_history.version is the corpus-revision pointerinformation_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 RPCsgrep .from('content_citations') + content_citations cc in pre_squash (:888/1135/2544/3083, trigger :3254)
{64.14} excludes content_citations + citing_entityledger show task 64 {64.14} details; seq-doc Lane A row
cocoindex pin (no external-API check needed)requirements.txtcocoindex[postgres]==1.0.7; pipeline never touches citations
gitnexus unavailable at authoringLadybugDB … version 41 vs 40 error (recorded §1.1)