Q&A Pair
Q&A Pair
Section titled “Q&A Pair”§1 Mission
Section titled “§1 Mission”A q_a_pair is a curated, corpus-level record of a question-and-answer pair: self-contained, versioned, governed, and ready for direct retrieval by AI via the MCP q_a_search tool. It is NOT a workspace-partitioned artefact — the Q&A corpus is shared across all six baseline application types (procurement, intelligence, sales_proposal, product_guide, competitor_research, training_onboarding). Workspace relevance is computed at query time via scope_tag overlap, not via a static FK. Deduplication of near-duplicate q_a_pairs (ID-120) is INTRA-tenant — across the client’s workspaces and forms within one database — never cross-tenant.
The two-tier model owns the full Q&A lifecycle:
q_a_pairs— the curated golden record. The corpus. Promoted from an extraction, authored directly by a SME, or promoted from a prior bid response (UC5). Published pairs are the retrieval substrate for theq_a_searchMCP tool.q_a_extractions— the derived cache. LLM or parser output from running an extractor over asource_documentsrow (chunked viacontent_chunks) or markdown sidecar. Once promoted,promoted_to_pair_idcaptures the lineage back to the corpus record. Invalidated rows are retained for audit — NOT deleted. As of id-370 (S511), the pipeline gates the declare onanswer_text: a blank form’s questions still extract to a NULLanswer_text(a sanctioned result), but noq_a_extractionsrow is minted for an unanswered question —q_a_extractionsfeeds the answered-pairs-only promotion funnel. Theq_a_extractions_promotion_candidates()RPC mirrors the gate (branch 1 carries the same answered predicate branch 3 already carried). Seespecs/id-370-unanswered-question-routing/.q_a_pair_history— the version table. Trigger-written on everyq_a_pairsUPDATE. Provides the version-on-cite substrate per0.9-edit-flow-investigation.md§6.0.3 — shipped bid responses resolve to the version snapshot at ship time.
This is the Wikipedia Principle applied to Q&A: one record about “ISO 27001 certification status” is one q_a_pair row cited by procurement, sales-proposal, and competitor-research workspaces alike — not three duplicates.
§2 Table shape
Section titled “§2 Table shape”2.1 q_a_pairs columns
Section titled “2.1 q_a_pairs columns”| Column | Type | Nullable | Notes |
|---|---|---|---|
id | uuid | NO | PK, gen_random_uuid() |
question_text | text | NO | Primary retrieval substrate (embedded + FTS) |
alternate_question_phrasings | text[] | NO | Default '{}'; covers Shape D context variants; included in FTS index |
answer_standard | text | NO | Main answer body (NOT NULL post-T6 WP1) |
answer_advanced | text | YES | Extended answer for tiered clients (Phew-style audit-6col shape) |
scope_tag | text[] | NO | Default '{}'; workspace-relevance substrate (CV 21) |
anti_scope_tag | text[] | NO | Default '{}'; explicit exclusion from matching workspaces |
source_workspace_id | uuid | YES | FK → workspaces(id) ON DELETE SET NULL; provenance audit only (NOT a scoping signal per Q-OQR1-07) |
origin_kind | text | NO | CV 22; CHECK ('extracted_from_corpus','curated_explicit','derived_from_bid_response','imported_legacy'); default 'curated_explicit' |
question_embedding | vector(1024) | YES | text-embedding-3-large; NULL until embedding job runs |
publication_status | text | NO | Default 'draft'; CHECK ('draft','in_review','published','archived') |
superseded_by | uuid | YES | Self-referencing FK → q_a_pairs(id) ON DELETE SET NULL; UC8 merge lineage |
valid_from | timestamptz | YES | Temporal validity start |
valid_to | timestamptz | YES | NULL = currently valid; version-on-cite at ship time per §6.0.3 |
created_at | timestamptz | NO | now() |
updated_at | timestamptz | NO | now() |
2.2 GIN indexes
Section titled “2.2 GIN indexes”| Index | Column | Purpose |
|---|---|---|
idx_q_a_pairs_scope_tag | scope_tag GIN | && overlap operator for workspace-relevance filter |
idx_q_a_pairs_anti_scope_tag | anti_scope_tag GIN | Exclusion filter NOT (anti_scope_tag && workspace.scope_tag) |
See §6 for the anti-pattern rule on idx_q_a_pairs_workspace.
2.3 source_workspace_id is nullable provenance audit
Section titled “2.3 source_workspace_id is nullable provenance audit”Per Q-OQR1-07: source_workspace_id is NULLABLE and records the originating workspace for origin_kind='derived_from_bid_response' pairs only. It is NOT a workspace-scoping signal. For origin_kind='extracted_from_corpus' or 'curated_explicit', source_workspace_id is NULL — the pair’s provenance is carried in origin_kind + source_content_item_id on the linked q_a_extractions row.
§3 Lifecycle
Section titled “§3 Lifecycle”publication_status drives the governance lifecycle. State transitions:
| From | To | Trigger |
|---|---|---|
draft | in_review | SME or automated quality gate submits for review |
in_review | published | Reviewer approves |
in_review | draft | Reviewer rejects / returns for rework |
published | in_review | Any edit to a published pair (UC6 §8.3 — revision moves to in_review automatically) |
published / in_review / draft | archived | Administrative retire |
Supersession: a pair may be superseded by a newer pair (superseded_by UUID FK). The valid_to column records when the pair was superseded. Superseded pairs are accessible for version-on-cite lineage — q_a_get_verbatim(uuid) has no publication_status filter so superseded/archived pairs are retrievable for lineage resolution.
Note: publication_status='superseded' was the T2 value; it was replaced in T6 WP1 by 'in_review' because superseded_by UUID carries the lineage — the status enum was redundant for this purpose (see 05-qa-flow.md §8.3).
§4 Provenance
Section titled “§4 Provenance”origin_kind (CV 22) records how a Q&A pair entered the corpus:
| Value | Meaning |
|---|---|
extracted_from_corpus | Extraction from a source_documents row or markdown sidecar via cocoindex |
curated_explicit | Directly authored by a SME (no extraction source); default for manually created pairs |
derived_from_bid_response | Promoted from a bid response via UC5; source_workspace_id records the originating procurement workspace |
imported_legacy | One-shot migration from legacy data (e.g. Phew’s 395 content_items.content_type='q_a_pair' rows via T7, pre-ID-131) |
source_workspace_id is populated only for derived_from_bid_response pairs. For all other origin kinds, source_workspace_id is NULL — the pair’s corpus-level identity is expressed by origin_kind + the q_a_extractions.source_content_item_id lineage.
§5 Retrieval
Section titled “§5 Retrieval”Two-step retrieval pattern per S16 §6.1 and 05-qa-flow.md §7.2-§7.3:
-
q_a_search(p_query text, p_query_embedding vector(1024), p_limit integer DEFAULT 20)— Step 1. Returns a ranked preview list. Filterspublication_status='published' AND question_embedding IS NOT NULL. Returnsembedding_score NUMERIC(5,4)+fulltext_score NUMERIC(5,4)as separate columns per N9 RESOLVED-S236 — NOT a blended single score. Scope filtering is caller-side (scope_tag && caller_scope_tags). InternalORDER BYuses weighted blendembedding * 0.6 + fulltext * 0.4(not exposed as a return column). -
q_a_get_verbatim(p_pair_id uuid)— Step 2. Returns the fullq_a_pairrow for a single pair. Excludesquestion_embedding(payload-size discipline per S16 §6.1 “AI-consumer-first”). Nopublication_statusfilter — caller may fetch superseded/archived pairs for lineage resolution.
The separate-score design surfaces per-method tunability signal to the UI without schema change. Per-method weight configuration and blend policy are feature-spec scope per N9 RESOLVED-S236 (“operational verification deferred to feature spec time”).
§6 Anti-patterns
Section titled “§6 Anti-patterns”The following are explicitly REJECTED per Phase 0.9 ratifications and the T6 migration:
| Anti-pattern | Status | Rationale |
|---|---|---|
q_a_pairs.workspace_id NOT NULL FK | RATIFIED-RETIRE (Q-OQR1-06) | Superseded by corpus-level + scope_tag-driven relevance; 0 of 395 prod rows were workspace-assigned empirically |
q_a_pair_workspaces M:N junction | RATIFIED-DO-NOT-BUILD (Q-OQR1-06) | scope_tag overlap subsumes; heavier than needed |
idx_q_a_pairs_workspace (workspace-partition index) | RATIFIED-DO-NOT-BUILD | Workspace relevance computed at read time via GIN; a workspace-partition index encodes the superseded schema framing. Future maintainers MUST NOT add this index. |
q_a_extractions as corpus records | RATIFIED-DO-NOT-BUILD | Extractions are derived cache, NOT corpus records. Only promoted q_a_pairs rows are corpus records. The two-tier model is load-bearing — conflating them destroys the promotion audit trail and the version-on-cite substrate. |
Relationships
Section titled “Relationships”q_a_extractions.promoted_to_pair_idFK →q_a_pairs(id)— lineage from extraction to promoted corpus record.q_a_extractions.source_document_id(renamed fromsource_content_item_idat ID-131 M2,20260628200000_id131_extract_reparent.sql) FK →source_documents(id)— provenance back to the extracted source document.q_a_pair_history.q_a_pair_idFK →q_a_pairs(id)ON DELETE CASCADE — version snapshots triggered on every UPDATE.q_a_pairs.superseded_byFK →q_a_pairs(id)— self-referencing UC8 merge lineage.q_a_pairs.source_workspace_idFK →workspaces(id)— provenance audit forderived_from_bid_responsepairs only.- Adjacent to CV 22
origin_kind—q_a_pairs.origin_kinduses this vocabulary. - Adjacent to CV 23
extractor_kind—q_a_extractions.extractor_kinduses this vocabulary. - Adjacent to CV 21
scope_tag—q_a_pairs.scope_tag+anti_scope_taguse scope_tag vocabulary. - Layer 5 (Knowledge Graph):
q_a_pairis the canonical Layer-5 “citable fact” entity perphase-b-prerequisite-1-onthology-pipeline.md§3.5.
Consumers
Section titled “Consumers”- MCP
q_a_searchtool (plannedlib/mcp/tools/qa.ts) — primary consumer; uses the two-step retrieval pattern (q_a_search → q_a_get_verbatim). - UC5 promotion flow — bid response → Q&A pair promotion writes
q_a_pairsrows withorigin_kind='derived_from_bid_response'. - UC6 revision flow — user or AI-suggest edits to
publishedpairs viaq_a_pairsUPDATE (triggers history row). question_matchestable — links application-specific form questions to corpus-levelq_a_pairsviaquestion_kinddiscriminator.- Cocoindex extraction pipeline — UPSERTs into
q_a_extractions; human review promotes toq_a_pairs.
- Status: APPLIED-S249 — full schema (T6 WP1 + WP2) applied to staging branch
turayklvaunphgbgscat. Production apply pending Liam ratification. - T2 combined-PR migration (S246/S247) created the minimal
q_a_pairstable sketch. T6 WP1 extended it to the full spec-compliant shape. - T7 (Phew Q&A first-ingest) will use
origin_kind='imported_legacy'+ cocoindex extraction → promotion for the legacy 395content_items.content_type='q_a_pair'rows (pre-ID-131;content_itemsitself was DROPPED at ID-131 M6). Those rows soft-archive post-migration;q_a_pairsbecomes the sole retrieval surface. question_embeddingis NULL until the embedding job runs (post-T6 WP1 apply). Theq_a_searchRPC filtersWHERE question_embedding IS NOT NULLso unembedded pairs are silently excluded from semantic retrieval until the embedding run completes.content_items.content_text_hashwas aGENERATED ALWAYScolumn (CLAUDE.md gotcha, pre-ID-131) — unrelated toq_a_pairs; the column (andcontent_itemsitself) is GONE, DROPPED at ID-131 M6. Kept here only as historical trivia.