Procurement Workspaces — TECH
⚠️ PARTIALLY SUPERSEDED (S462, 2026-07-11) — DR-038 + ID-145. Section A (the workspace umbrella, the
procurement_workspacessatellite B-3/B-4, and the workspace-level 10-state workflow B-5..B-7) is superseded — id-130 moved per-stage state ontoform_templatesand DR-038 retires workspace scoping. Durable invariants: B-2 (form_types), B-9..B-12 (retrieval + citation), B-13..B-16 (EP8 import mechanics), B-17/B-18 (promotion policy) — re-anchor them to the form instance. Rework owner: ID-145 —specs/id-145-procurement-form-first/RESEARCH.md.
Procurement Workspaces — TECH
Section titled “Procurement Workspaces — TECH”Status:
PARTIALLY-SUPERSEDED (S462)— NEW-S242. Companion toPRODUCT.md. Per-invariant implementation references grounded in current code + target migration shape; gates noted inline. Absorbs EP8 v5 TECH content (file:line landings, API route shapes, post-insert pipeline) into the relevant T-N entries below; the standalone EP8 spec is superseded at S242 close.
How to use this doc
Section titled “How to use this doc”This file carries implementation references for each B-N invariant in PRODUCT.md. Each T-N entry contains:
- Current state: code / migration file:line that today implements the invariant pre-rename, or “greenfield” if no code exists.
- Target state: what the platform must do after the T2 combined-PR migration + T4 code-rename land.
- Gate: any STILL-OPEN dependency on another PLAN.md task or external spec.
- Validation: how the invariant is verified (unit / integration / parity test / CI guard / manual).
Test-plan summary in §Testing-and-validation at the foot.
Source-of-truth pointers
Section titled “Source-of-truth pointers”./PRODUCT.md— numbered invariants B-1..B-29.docs/specs/id-31-canonical-pipeline-implementation-plan/PLAN.md§4.4 T4 (procurement umbrella rename — code + state machine) + §4.7 T7 (q_a_pair data migration sequencing) + §5 (NEW specs row “procurement-workspaces”).docs/plans/phase-0-investigation/architecture/04-workspace-types.md§4.2 (OQ-Q38-E 6-column ratification) + §7 (cascaded renames) + §8 (combined-PR walkthrough).docs/plans/phase-0-investigation/architecture/05-qa-flow.md§7 (question_matches retrieval) + §8 (UC6 write-back) + §9 (UC5 promotion).docs/plans/phase-0-investigation/architecture/02-data-flow.md§3 (cocoindex 6-stage flow) + §8 (edit_intent re-classification policy).docs/plans/phase-0-investigation/architecture/06-mcp-tooling.md§6.3 (procurement MCP tool renames).docs/plans/phase-0-investigation/architecture/08-new-features.md§4 (change_reports rename) + §5 (UC9 governance) + §7 (bid feedback 3-UC).docs/specs/reserved-workspace-seats/{PRODUCT,TECH}.md(substrate spec — seat-shape per RWS S-1..S-8; procurement extends via per-app columns here).docs/specs/rls-pattern/{PRODUCT,TECH}.md(RLS auto-trigger + grants helper inherited).docs/specs/ep8-qa-docx-import-ui-spec.mdv5 (RATIFIED-S242 — ABSORBED-S242; TECH content folded into T-7..T-13 below).
Audience
Section titled “Audience”Engineers implementing T2 combined-PR migration + T4 procurement rename + per-column population + EP8 import UI; reviewers verifying behavioural parity post-rename; per-procurement-workspace integration test authors.
T-1 — application_type='procurement' umbrella vocabulary (implements B-1)
Section titled “T-1 — application_type='procurement' umbrella vocabulary (implements B-1)”Current state: workspaces.type='bid' text-column discriminator (pre-Q-OQR1-01 framing per types/bid.ts:42-54 Bid interface). The application_types instance table does NOT exist yet — T2 combined-PR creates it per 04-workspace-types.md §3.
Target state: workspaces.type column dropped; workspaces.application_type_id UUID NOT NULL REFERENCES application_types(id) added; application_types seeded with the 6 baseline rows including procurement (key matches the table-suffix procurement_workspaces). Backfill from existing workspaces.type='bid' rows → application_type_id resolving to the procurement row.
Lands in: Single T2 combined-PR migration per 04-workspace-types.md §8 + PLAN.md §4.2 T2.1. The procurement-specific portion is the satellite rename + column population (T-3 below); the application_types table + FK swap is in scope of T2 generally.
Gate: T3 (RLS-pattern migration apply per PLAN.md §4.3) — auto-RLS event trigger + grants helper must exist before T2 lands.
Validation:
SELECT count(*) FROM application_types WHERE key='procurement'returns 1.SELECT count(*) FROM workspaces WHERE application_type_id = (SELECT id FROM application_types WHERE key='procurement')returns the same count as the pre-migrationSELECT count(*) FROM workspaces WHERE type='bid'(currently 4 prod rows per04-workspace-types.md§2.2; verify viamcp__supabase__execute_sqlpre-apply).
T-2 — PROCUREMENT_WORKFLOW_STATES rename (implements B-5)
Section titled “T-2 — PROCUREMENT_WORKFLOW_STATES rename (implements B-5)”Current state:
types/bid.ts:3-14—BID_STATESconst tuple (10 values:draft,questions_extracted,matching,drafting,in_review,ready_for_export,submitted,won,lost,withdrawn).types/bid.ts:16—BidStateTS type derived fromBID_STATES.lib/bid/bid-state-machine.ts— 86 lines. ImportsBidState+BID_STATESfrom@/types/bid; re-exports both; exportsBID_STATE_LABELS,BID_STATE_SHORT_LABELS,BID_STATE_COLOURS,VALID_TRANSITIONS(internal const),canTransition(),getAvailableTransitions(),isTerminal(),isActive(),BID_STATE_PROGRESSION.types/bid-metadata.ts—BidMetadatainterface (40 lines).
Target state:
types/procurement.ts— receives the movedBID_STATES→PROCUREMENT_WORKFLOW_STATESconst (10 values preserved verbatim per Q-OQR1-02 cascaded-renames table —04-workspace-types.md§7.2);BidState→ProcurementWorkflowStatetype.lib/procurement/procurement-workflow.ts— receives the moved state-machine file (86 lines preserved verbatim except for renamed identifiers).- All exports rename:
BID_STATES→PROCUREMENT_WORKFLOW_STATES.BID_STATE_LABELS→PROCUREMENT_WORKFLOW_STATE_LABELS.BID_STATE_SHORT_LABELS→PROCUREMENT_WORKFLOW_STATE_SHORT_LABELS.BID_STATE_COLOURS→PROCUREMENT_WORKFLOW_STATE_COLOURS.BID_STATE_PROGRESSION→PROCUREMENT_WORKFLOW_STATE_PROGRESSION.VALID_TRANSITIONS(internal const) — name preserved (file-internal only).canTransition(),getAvailableTransitions(),isTerminal(),isActive()— names preserved (generic verbs).
Lands in: T4 code rename per PLAN.md §4.4 T4 subtasks 3+4. The move uses git mv to preserve history per PLAN.md §4.4 T4.2.
Gate: T2 (the DB-side procurement_workspaces rename + 6-column population — T-3 below) MUST land first, so the code rename has the DB schema to bind against.
Validation:
bun run testfull regression on__tests__/lib/procurement/procurement-workflow.test.ts(moved from__tests__/lib/bid/bid-state-machine.test.ts) — verifies the 10 state values, the transition table, and the terminal-state classification function.grep -rn "BID_STATES\b" lib/ scripts/ app/ components/post-rename returns zero hits perPLAN.md§4.4 T4 acceptance criteria.grep -rn "BID_STATE_LABELS\|BID_STATE_SHORT_LABELS\|BID_STATE_COLOURS\|BID_STATE_PROGRESSION" lib/ scripts/ app/ components/post-rename returns zero hits.- CI guard added (per
PLAN.md§4.4 T4 subtask 8) preventingbidregressions in code.
T-3 — procurement_workspaces 6-column population (implements B-3, B-4)
Section titled “T-3 — procurement_workspaces 6-column population (implements B-3, B-4)”Current state:
- Pre-migration:
bid_workspacestable exists (the legacy satellite); per-bid metadata lives inworkspaces.domain_metadataJSONB blob (pertypes/bid.ts:42-54Bid interface —domain_metadata: BidMetadata). types/bid.ts:26-40—BidMetadatainterface enumerates 12 fields:buyer,status,deadline,reference_number,estimated_value,tender_source,tender_document_ids,submission_date,outcome,outcome_notes,notes,outcome_recorded_at,outcome_recorded_by.
Target state: Migration (combined-PR — T2 scope per PLAN.md §4.4 T4.7):
- Rename
bid_workspaces→procurement_workspaces(per04-workspace-types.md§7.2 cascaded-renames table row 2). The satellite preserves the seat-baseline shape per RWS T-1..T-3 —id UUID PK,workspace_id UUID NOT NULLwithUNIQUE(workspace_id)constraint enforcing the 1:1 cardinality from B-4, pluscreated_at/updated_attimestamps. - ADD 6 typed columns per OQ-Q38-E (
04-workspace-types.md§4.2):buyer TEXT NOT NULL.deadline TIMESTAMPTZ NULL.submission_date TIMESTAMPTZ NULL.outcome TEXT NULLwithCHECK (outcome IS NULL OR outcome IN ('won','lost','withdrawn')).outcome_recorded_at TIMESTAMPTZ NULL.outcome_recorded_by UUID NULL REFERENCES auth.users(id).
- Backfill the 6 columns from existing
workspaces.domain_metadataJSONB blobs. SQL pattern:UPDATE procurement_workspaces pwSETbuyer = COALESCE(w.domain_metadata->>'buyer', ''),deadline = (w.domain_metadata->>'deadline')::timestamptz,submission_date = (w.domain_metadata->>'submission_date')::timestamptz,outcome = w.domain_metadata->>'outcome',outcome_recorded_at = (w.domain_metadata->>'outcome_recorded_at')::timestamptz,outcome_recorded_by = (w.domain_metadata->>'outcome_recorded_by')::uuidFROM workspaces wWHERE pw.workspace_id = w.id; - Decision per T2 migration body: EITHER drop
workspaces.domain_metadataentirely after backfill OR retain a residual JSONB column for migration-tail compatibility (the 6 non-promotedBidMetadatafields —reference_number,estimated_value,tender_source,tender_document_ids,outcome_notes,notes). If retain, the residual JSONB lives onprocurement_workspaces(NOT onworkspaces— the 6 fields are procurement-specific), populated from the read-side of the backfill beforeworkspaces.domain_metadatais dropped. Authoritative decision lives in T2 migration body, not in this spec — seePLAN.md§4.2 T2.1 for the migration SQL ownership. Resolution criteria deferred per[GAP-PROC-001].
Lands in: T2 combined-PR migration per PLAN.md §4.4 T4.7. The migration filename follows the Supabase timestamp convention (supabase/migrations/<timestamp>_*.sql) — combined-PR migration name per PLAN.md §4.2 T2.
Gate: T3 (RLS-pattern migration apply per PLAN.md §4.3) MUST land first — auto-RLS event trigger fires on the bid_workspaces rename (which is a CREATE TABLE from PG’s perspective at the SQL trigger level, depending on rename strategy).
Validation:
SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name='procurement_workspaces'returns the expected 6 column rows (+ base seat columns per RWS T-1..T-3).- Spot-check backfill: pick 3 prod
bid_workspacesrows pre-migration, capture theirdomain_metadataJSONB, run the migration on staging, verify the 6 columns match the JSONB values post-apply. - CHECK constraint:
INSERT INTO procurement_workspaces (workspace_id, buyer, outcome) VALUES (...,'invalid_outcome')fails with23514 check_violation.
T-4 — Procurement code sweep (implements B-1, B-2, B-5, B-8, B-11, B-27)
Section titled “T-4 — Procurement code sweep (implements B-1, B-2, B-5, B-8, B-11, B-27)”Current state: PLAN.md §4.4 T4 lists 44 code files affected by the umbrella rename. Key files visible from current inventory:
lib/bid/(7 files:bid-export-data.ts,bid-export-docx.ts,bid-export-types.ts,bid-export-xlsx.ts,bid-helpers.ts,bid-queries.ts,bid-state-machine.ts).components/bid/(21 files:bid-context-provider.tsx,bid-creation-wizard.tsx,bid-export-menu.tsx,bid-list-card.tsx,bid-outcome.tsx,bid-state-indicator.tsx,draft-recovery-dialog.tsx,kb-integration-review.tsx,question-list.tsx,question-navigator.tsx,question-review.tsx,question-row.tsx,readiness-checklist.tsx,response-actions.tsx,response-editor.tsx,response-version-history.tsx,template-completion-summary.tsx,template-field-review.tsx,template-fill-progress.tsx,tender-metadata-prompt.tsx,tender-upload.tsx).types/bid.ts(205 lines,BID_STATES+Bid+BidQuestion+BidResponseSummary+ helpers);types/bid-metadata.ts.lib/mcp/tools/bids.ts— MCP tool registrations (cited per06-mcp-tooling.md§6.3):list_active_bids,get_bid_detail,get_bid_question.app/api/bids/*routes (the procurement workflow API surface — TECH.md does NOT enumerate routes pre-rename;PLAN.md§4.4 T4 owns the file inventory).__tests__/lib/bid/*and__tests__/components/bid/*test mirrors.
Target state:
lib/procurement/(post-rename — files preserved viagit mvperPLAN.md§4.4 T4.2; file names lose thebid-prefix where the file is procurement-only OR keep the prefix where the file describes aform_typelegitimately).components/procurement/(post-rename — same git-mv pattern).types/procurement.ts(state machine + workflow types) +types/procurement-metadata.ts(the residual JSONB shape if retained per T-3) ORtypes/procurement-form.ts(composer surface types).lib/mcp/tools/procurement.ts(post-rename) — tools rename:list_active_bids→list_active_procurement,get_bid_detail→get_procurement_detail,get_bid_question→get_procurement_question.- All
project_id→workspace_idper04-workspace-types.md§7.4 (44-file sweep). Affectstypes/bid.ts:97BidQuestion.project_id→workspace_id(plus 43 other code sites + 2 DB column renames in T2).
Lands in: T4 code rename per PLAN.md §4.4 (entire task) — uses ts-morph + ast-grep per T4.1. Cherry-pick (not merge) per CLAUDE.md “Cherry-pick (not merge) parallel agent branches” gotcha.
Gate: T2 (DB-side rename) MUST land first per PLAN.md §4.4 dependency. T-3 above is the satellite-column part of T2.
Validation:
grep -rn "lib/bid\b\|components/bid\b\|BidState\b\|BID_STATES\b" lib/ scripts/ app/ components/ __tests__/ types/returns zero hits.grep -rn "project_id" lib/ scripts/ app/ components/returns zero hits perPLAN.md§4.4 T4 acceptance criteria.bun run knipshows no orphanedlib/bid/*files.- MCP eval Layer 1 (protocol compliance, 42 checks) passes with the renamed tool names per
06-mcp-tooling.md§6.3.
T-5 — Submission-date side-effect on state transition (implements B-6, B-7)
Section titled “T-5 — Submission-date side-effect on state transition (implements B-6, B-7)”Current state: lib/bid/bid-state-machine.ts:48-59 defines VALID_TRANSITIONS per state. The state-write surface today: workspaces.status column (per types/bid.ts:25 // The 'status' field is enforced at the database level via a dedicated 'status' column on 'workspaces' with a CHECK constraint). Submission-date currently lives in workspaces.domain_metadata->>'submission_date' JSONB blob.
Target state: State-transition write (post-rename: lib/procurement/procurement-workflow.ts::commitTransition() or equivalent) MUST update both workspaces.status and procurement_workspaces.submission_date in the same transaction when the target state is submitted. Pattern:
export async function commitTransition( workspaceId: string, toState: ProcurementWorkflowState, userId: string, client: SupabaseClient): Promise<Result<void, TransitionError>> { // ... canTransition(currentState, toState) precondition check ... const updates: Promise<unknown>[] = [ client.from('workspaces').update({ status: toState }).eq('id', workspaceId), ]; if (toState === 'submitted') { updates.push( client.from('procurement_workspaces').update({ submission_date: new Date().toISOString() }).eq('workspace_id', workspaceId) ); } if (toState === 'won' || toState === 'lost' || toState === 'withdrawn') { updates.push( client.from('procurement_workspaces').update({ outcome: toState, outcome_recorded_at: new Date().toISOString(), outcome_recorded_by: userId, }).eq('workspace_id', workspaceId) ); } // ... txn handling via Postgres function OR client-side Promise.all with rollback ...}Implementation note: The two-write pattern (workspaces + procurement_workspaces) is atomic only inside a Postgres function or transaction. Per CLAUDE.md “REST PATCH on wrong UUID” gotcha, every UPDATE MUST verify row-count and re-query post-write. Use sb() from @/lib/supabase/safe per CLAUDE.md “Silent failures in Supabase calls” gotcha. The recommended pattern is a Postgres function commit_procurement_transition(workspace_id uuid, to_state text, user_id uuid) that runs both writes inside the same transaction; this also lets the state-machine business logic live in one place that the TS layer calls.
Lands in: T4 code rename per PLAN.md §4.4 T4 subtask 3 — when the state machine moves to lib/procurement/, the side-effect writes are added at the same time. The Postgres function is a separate migration that lands [LOCKSTEP-WITH-MIGRATION] with T2 (or in a follow-up migration immediately after T4 — TECH-spec author judgement; either is acceptable per 04-workspace-types.md §8 combined-PR scope).
Gate: T-3 (satellite columns exist).
Validation:
- Integration test: create workspace, transition to
submitted, assertprocurement_workspaces.submission_dateis non-NULL and within 1s ofnow(). - Integration test: create workspace, transition to
won, assertoutcome='won'+outcome_recorded_atnon-NULL +outcome_recorded_by= the calling user’s UUID. - Negative test: transition from
submittedtosubmitted(idempotent or rejected, per state-machine semantics) — verify no duplicate audit row written.
T-6 — Bid composer surface rename (implements B-8 through B-12)
Section titled “T-6 — Bid composer surface rename (implements B-8 through B-12)”Current state:
components/bid/question-review.tsx— the composer page.components/bid/question-list.tsx,question-navigator.tsx,question-row.tsx— composer sub-components.components/bid/response-editor.tsx— the per-response edit surface.components/bid/bid-context-provider.tsx— React context for the current bid workspace.components/bid/kb-integration-review.tsx— the legacy match-review surface (precursor to the post-T10 composer).
Target state:
components/procurement/question-review.tsx(post-rename viagit mvperPLAN.md§4.4 T4.5).- The composer renders inside the procurement workspace’s question-review page; the legacy
kb-integration-review.tsxconsumes thequestion_matchestable (post-T10 perPLAN.md§4.10) with separateembedding_score+fulltext_scorecolumns surfaced per question. - Data fetching: TanStack Query exclusively per CLAUDE.md “Data fetching” gotcha. Query keys in
lib/query/query-keys.ts; fetchers inlib/query/fetchers.ts; no SWR or raw fetch in hooks. New query keys:['procurement', workspaceId, 'questions'],['procurement', workspaceId, 'question', questionId, 'matches'],['procurement', workspaceId, 'question', questionId, 'match', candidateId, 'verbatim']. - Composer two-step retrieval:
useQuery({ queryKey: ['procurement', wsId, 'question', qId, 'matches'], queryFn: () => fetchMatchesList(qId) })for step 1 (list/preview),useQuery({ queryKey: ['procurement', wsId, 'question', qId, 'match', cId, 'verbatim'], queryFn: () => fetchMatchVerbatim(cId), enabled: !!cId })for step 2 (lazy load on user click).
Lands in: T4 code rename per PLAN.md §4.4 T4.5 (components/bid/ → components/procurement/) + T10 retrieval substrate (per PLAN.md §4.10 — the consumer surface).
Gate: T2 + T4 (rename) + T10 (question_matches table). The composer cannot retrieve match candidates until question_matches exists (T10).
Validation:
- Integration test (
__tests__/components/procurement/question-review.test.tsxpost-rename): mount composer for a workspace with 3 mocked questions × 5 mocked match candidates each; assert ranked list renders with both score columns visible. - Manual test: per-form-type happy path — create procurement workspace with
form_type='rfp', extract questions, draft responses using composer, verify Q&A pair citation capturesciting_entity='bid_response'per B-12. - A11y: composer keyboard-navigable; match candidates surfaced via semantic
<table>witharia-rowindex; preview / accept buttons reachable via Tab.
T-7 — Q&A .docx import UI surface (implements B-13, B-14)
Section titled “T-7 — Q&A .docx import UI surface (implements B-13, B-14)”Current state (per EP8 v5 §1.5):
lib/bid-library-ingest/(4 files:docx-to-markdown.ts,extract-qa-pairs.ts,extract-answer.ts,resolve-question.ts) — TS extractor set shipped S192-S194.scripts/import_bid_library.py— the canonical CLI (9-step pipeline per EP8 v5 §3.1).app/api/upload/route.ts— the existing single-file upload route (auth, mammoth, magic-byte MIME validation, pipeline_runs).- No
/api/ingest/qa-docx/*route group exists. - No
components/settings/bid-library-import-section.tsxexists. supabase/migrations/20260428174512_add_ingest_source_to_content_items.sql— addsingest_sourcecolumn tocontent_itemswith'qa_import'as a CHECK enum value.
Target state ([ABSORBED-S242 EP8 v5 §5 + §6 + §13]):
New route group at app/api/ingest/qa-docx/:
| Route | Purpose | Auth |
|---|---|---|
presign/route.ts | Issue short-lived Supabase Storage presigned URLs (5 min) | getAuthorisedClient(['admin']) |
analyse/route.ts | Pre-flight analysis: tracked-changes detection, pair count, draft/final heuristic, dedup hints | getAuthorisedClient(['admin']) |
import/route.ts | Execute the import; create pipeline_runs row; kick off async import | getAuthorisedClient(['admin']) |
Pre-flight helpers:
| Helper | Path | Notes |
|---|---|---|
hasTrackedChanges() | lib/extraction/docx-tracked-changes.ts (NEW) | OOXML <w:ins> / <w:del> detection; ~30 lines per EP8 §3.4 |
shouldAutoSupersede() | lib/bid-library-ingest/filename-heuristic.ts (NEW) | “final” vs “DRAFT” filename heuristic; ~20 lines per EP8 §3.4 |
Settings-page integration:
| File | Purpose |
|---|---|
app/settings/page.tsx | Add 'bid-library-import' section enum value (post-T4 rename MAY become 'procurement-library-import') |
components/settings/settings-sidebar.tsx | Existing — useUserRole admin filter gates the section entry |
components/settings/bid-library-import-section.tsx | NEW — entry point lazy-loaded by sidebar |
components/settings/bid-library-import/upload-dropzone.tsx | NEW — drag-and-drop + file picker; uploads via presigned URL |
components/settings/bid-library-import/analysis-table.tsx | NEW — pre-flight results table |
components/settings/bid-library-import/import-options-form.tsx | NEW — checkboxes for auto_supersede, require_clean, force, skip_dedup, batch_tag |
components/settings/bid-library-import/import-progress-poller.tsx | NEW — TanStack Query polling renderer (1-2s interval) |
components/settings/bid-library-import/import-summary-card.tsx | NEW — final summary card |
Post-T7 cutover (per B-16): Once T7 retires content_type='q_a_pair' and migrates to q_a_pairs, the import/route.ts INSERT target switches from content_items to q_a_pairs. T7 owns the sequencing; this spec respects the cutover and does NOT freeze the post-T7 enum value for origin_kind.
Lands in: EP8 implementation (S243+ per product-roadmap.json §1.11 status — “EP8 build remains”) — folded into the procurement track per [ABSORBED-S242]. The work is sequenced per EP8 v5 §13 Wave 1-4 plan (lib/ primitives + storage bucket migration → API routes → component tree → E2E).
Gate: Pre-cutover the import lands rows into content_items (the EP8 v5 §7.1 payload shape with ingest_source='qa_import'). Post-T7 cutover, switches target to q_a_pairs (T7 owns the cutover sequencing).
Validation (per EP8 v5 §9):
- Unit tests:
lib/extraction/docx-tracked-changes.test.ts,lib/bid-library-ingest/filename-heuristic.test.ts(parity with Python on identical fixtures). - API tests:
__tests__/app/api/ingest/qa-docx/presign.test.ts,analyse.test.ts,import.test.ts. - Parity test: extend existing
__tests__/integration/bid-library-ingest/parity.integration.test.tsto invoke the TS analyse + import pipeline against the same fixtures as the CLI; assert structural equality. Gated behindKH_RUN_INTEGRATION=1. - E2E (optional first cut):
e2e/tests/bid-library-import.spec.ts. - Regression guard:
__tests__/guards/settings-sidebar-admin-only.test.tsx— non-admin users do not see the section entry.
T-8 — run-post-insert.ts 8-step pipeline (implements B-13..B-16 import path)
Section titled “T-8 — run-post-insert.ts 8-step pipeline (implements B-13..B-16 import path)”Current state (per EP8 v5 §6.3, updated post-S209): lib/pipeline/ contains three helpers as of S242: record-run.ts (single-insert-on-finish pattern), start-run.ts (at-start INSERT/UPSERT, Pattern E), and update-progress.ts (mid-flight progress UPDATE; extracted from app/api/upload/route.ts). The two-phase scaffolding for pipeline_runs (T-10 below) is already partly shipped; the run-post-insert.ts 8-step orchestrator remains greenfield. The Python run_post_insert helper (scripts/kb_pipeline/post_insert.py, S185 WP-D) codifies the 8-step canonical order; there is no TS equivalent yet.
Target state: lib/pipeline/run-post-insert.ts mirrors the Python helper 1:1. 8 steps gated by per-caller flags:
- History insert (
content_history) —writeHistory: boolean = true.change_reasonis mandatory per CLAUDE.mdfeedback_content_history_change_reason_mandatoryprecedent. - Layer inference (
lib/ai/layer-inference.ts::inferLayerFlag) —inferLayerFlag: boolean = true. - Chunk + embed —
writeChunks: boolean = true. Per CLAUDE.md “Embedding vector serialisation” — useJSON.stringify(embedding)for Supabase RPC vector params. - Classification (
lib/ai/classify.ts::classifyContent) — gated byclassificationparam being passed. Per CLAUDE.md “classifyContentuserId must be a UUID” — pipeline service-account UUIDa0000000-0000-4000-8000-000000000001for non-user invocations. - Entity extraction —
storeEntitiesFlag: boolean = true. - Temporal metadata bridge —
bridgeTemporal: boolean = true. - Temporal metadata write —
writeTemporal: boolean = true. - Progressive-depth generation (S189 WP3 Python side) —
generateProgressiveDepthFlag: boolean = false(DEFAULT OFF per EP8 v5 §6.3).
Signature shape per EP8 v5 §6.3:
export interface RunPostInsertOptions { itemId: string; title: string; content: string; contentType: string; ingestionSource: string; // mirrors content_items.ingest_source classification?: ClassificationResult; historyChangeSummary?: string; historyChangeReason?: string; // REQUIRED if writeHistory = true writeHistory?: boolean; writeChunks?: boolean; storeEntitiesFlag?: boolean; writeTemporal?: boolean; bridgeTemporal?: boolean; inferLayerFlag?: boolean; generateProgressiveDepthFlag?: boolean; questionText?: string; answerStandard?: string; answerAdvanced?: string; logPrefix?: string; logger?: Logger;}Non-blocking error capture per step — each step’s failure recorded in result.errors[] rather than thrown, matching the Python helper’s posture.
Lands in: EP8 implementation (S243+) per EP8 v5 §6.3 effort estimate (5-6 h including tests).
Gate: None (greenfield TS-side).
Validation (per EP8 v5 §6.3):
- 6-8 unit tests per step covering happy-path + failure-path.
- 4-6 integration tests gated by
KH_RUN_INTEGRATION=1. - Parity integration test (
__tests__/integration/run-post-insert-parity.integration.test.ts) — runs both TS + Python helpers against the same fixture; asserts structural equality of output. - Per CLAUDE.md “REST PATCH on wrong UUID” — every UPDATE step verifies row-count post-write and re-queries.
T-9 — content_items insert payload for Q&A import (implements B-16)
Section titled “T-9 — content_items insert payload for Q&A import (implements B-16)”Current state (per EP8 v5 §7.1): The Python build_content_record() function (scripts/import_bid_library.py:288 writes ingest_source='qa_import'; :467 calls extract_qa_from_docx(emit_markdown=True)).
Target state: TS-side INSERT payload mirrors the Python shape ([ABSORBED-S242 EP8 v5 §7.1] — preserved verbatim subject to T7 timing):
{ title: string, // truncate_at_word_boundary(question_text, 120) content: string, // GFM markdown (matches CLI emit_markdown=True) answer_standard: string | null, answer_advanced: string | null, content_type: 'q_a_pair', platform: 'extraction', source_url: '', source_domain: '', primary_domain: string | null, primary_subtopic: string | null, secondary_domain: string | null, secondary_subtopic: string | null, classification_confidence: number, classified_at: string, // ISO 8601 now summary: string, // truncate_at_word_boundary(answer, 200) ai_keywords: string[], source_file: string | null, dedup_status: 'clean' | 'suspected_duplicate' | 'confirmed_duplicate' | 'confirmed_unique' | 'superseded' | null,
// S201/S205-S207 schema widening (per EP8 v5 §7.1): ingest_source: 'qa_import', // CHECK enum value content_owner_id: string, // resolved via `resolveContentOwnerId({...})` from lib/auth/owner-default.ts publication_status: 'in_review', // EP2 D-A precedent — never default 'published'
metadata: { section_name: string, table_index: number, row_index: number, has_standard: boolean, has_advanced: boolean, import_batch: string, has_tracked_changes: boolean, suspected_duplicate_of?: string, }, user_tags?: string[], layer?: 'brief' | 'detail' | 'reference',}Critical gotchas:
content_text_hashisGENERATED ALWAYSper CLAUDE.md gotcha — OMIT from payload entirely. PG auto-computes viamd5(normalised content).summaryis the column name (NOTai_summary) per CLAUDE.md “content_items.summary(notai_summary)”.
Post-T7 cutover (per B-16): The INSERT target switches to q_a_pairs with the corpus-level shape per 05-qa-flow.md §2.1 (question_text + alternate_question_phrasings + answer_standard + answer_advanced + scope_tag + anti_scope_tag + source_workspace_id NULL + origin_kind + question_embedding + publication_status + valid_from + valid_to). origin_kind value for EP8-driven imports is not frozen here — T7 owns the enum extension (proposed value: imported_docx_v1 or similar; TECH-spec author judgement at T7 cutover).
Lands in: EP8 import-route handler (T-7 above).
Gate: Pre-T7 lands at content_items; post-T7 switches to q_a_pairs.
Validation:
- Insert a test row via the import route; verify all 3 widened columns (
ingest_source,content_owner_id,publication_status) are non-NULL. - Per CLAUDE.md “RLS requires user_roles entry” — the admin user invoking the import MUST have a
user_rolesrow, else the insert silently fails RLS. CI test guards this. - Per CLAUDE.md “Bun fetch hangs on HTTP 204 through sandbox proxy” — supabase-js writes from the route handler in production go through a public proxy and return 200; sandbox dev runs need
dangerouslyDisableSandbox: truefor supabase-writing scripts.
T-10 — pipeline_runs two-phase pattern (implements B-14 import polling)
Section titled “T-10 — pipeline_runs two-phase pattern (implements B-14 import polling)”Current state (per EP8 v5 §7.2, updated post-S209): app/api/upload/route.ts uses the two-phase pattern. lib/pipeline/ now ships three helpers covering both phases: start-run.ts (at-start INSERT/UPSERT) and update-progress.ts (mid-flight progress UPDATEs) cover Phase 1 + ongoing Phase 2 updates; record-run.ts::recordPipelineRun() covers the single-insert-on-finish completion path. The EP8 import route MUST wire start-run.ts + update-progress.ts into the new flow rather than introduce a fresh two-phase pattern.
Target state ([ABSORBED-S242 EP8 v5 §7.2]):
Phase 1 — start-of-import INSERT via service client:
{ id: string, pipeline_name: 'ingest_qa_docx', status: 'running', started_at: string, progress: { step: 'uploading' | 'analysing' | 'storing' | 'complete', steps_completed: number, steps_total: number, detail: string, }, metadata: { user_id: string, file_count: number, batch_name: string, options: { auto_supersede, batch_tag, require_clean, force, skip_dedup }, },}Phase 2 — progress updates + final recording:
- Progress UPDATEs via service client directly (mirrors
app/api/upload/route.ts::updatePipelineProgress). Each UPDATE setsprogressJSONB + optionallycompleted_at+status. - Final-state row MAY use
recordPipelineRun()from@/lib/pipeline/record-runfor the single-insert-on-finish pattern.
recordPipelineRun signature (per EP8 v5 §7.2 footnote + CLAUDE.md recordPipelineRun() cron pattern):
supabaseclient arg is REQUIRED (not optional).statusenum:'completed' | 'completed_with_errors' | 'failed'(NOT'partial', NOT'complete').- Completion-record field is
result(notmetadata). itemsCreatedisstring[](item IDs), not a count.
Schema-verified claims (per EP8 v5 §7.2):
- Column name is
pipeline_name(NOTrun_type). steps_total/steps_completedare insideprogressJSONB, not top-level columns.- Valid
statusCHECK values:running,completed,completed_with_errors,failed. - Raw inserts MUST use
sb()from@/lib/supabase/safeper the silent-failure-prevention spec.
Lands in: EP8 import route (T-7 above).
Validation:
- Integration test: invoke import; poll
GET /api/pipeline-runs/:id; assertprogressJSONB transitions through expected step values; assert finalstatusis one of the three legal values. - Negative test: invoke import with bad fixture; assert
status='completed_with_errors'+result.errors[]populated.
T-11 — UC5 promotion implementation (implements B-17, B-18)
Section titled “T-11 — UC5 promotion implementation (implements B-17, B-18)”Current state: No UC5 promotion path exists today; legacy bid_responses table writes are KH-DB-only with no Q&A corpus integration.
Target state: Promotion flow per 05-qa-flow.md §9.1 (RATIFIED-S229):
- UI surface (per B-19 —
docs/specs/qa-promotion-ui/PRODUCT.mdis the UI spec destination): post-ship surface incomponents/procurement/response-actions.tsx(post-rename) exposes a “promote to Q&A library” action when the workspace state isready_for_export/submitted(and not inlostper default-skipB-18). - Server endpoint:
POST /api/q-a-pairs/promote-from-bid-responseaccepts{ bid_response_id, override_skip?: boolean }payload. - Server logic:
- Look up the source bid_response + its bid_question + parent procurement_workspace.
- If workspace
outcome='lost'andoverride_skipnot set → return 400 with “lost-bid skip-default” message (B-18). - Run cosine-similarity check against existing
q_a_pairs(threshold 0.85 default per05-qa-flow.md§9.1 4.6.Q9 RESOLVED). If close match → return the candidate IDs + present supersede / merge / proceed options to the UI. - INSERT new
q_a_pairrow withorigin_kind='derived_from_bid_response',source_workspace_id= the procurement workspace UUID,promoted_from_bid_response_id= the bid_response.id,valid_from = now(),valid_to = NULL,publication_status='draft'. - Route to
in_reviewvia the existing cross-application review queue (per0.9-edit-flow-investigation.md§6.0.4 — admin / curator only).
Lands in: T9 per-UC implementation per PLAN.md §4.9 T9 subtask 5 (UC5). This spec is the consumer-spec anchor for T9 UC5; the implementation work is PLAN.md-tracked.
Gate: T2 + T6 (q_a_pairs table exists per PLAN.md §4.6 T6) + T4 (procurement rename) + T11 (citations polymorphic enum per PLAN.md §4.11). Promotion UI shape gated on docs/specs/qa-promotion-ui/PRODUCT.md per B-19.
Validation:
- Integration test (per
PLAN.md§4.9 T9 acceptance criteria): promote a bid response →q_a_pairdraft created with the lineage columns populated. - Negative test: promote a
lost-workspace bid response withoutoverride_skip→ returns 400. - Concurrency test: two simultaneous promotion requests for the same bid_response_id → only one
q_a_pairrow created; second request returns 409 conflict.
T-12 — Citation capture on shipped bid responses (implements B-12)
Section titled “T-12 — Citation capture on shipped bid responses (implements B-12)”Current state: No citations table exists today; legacy bid_responses reference matched content via the matched_content_ids array on bid_questions (types/bid.ts:108) — not a polymorphic-enum citation pattern.
Target state: Per T11 (PLAN.md §4.11) the polymorphic citations table lands with the schema per 05-qa-flow.md §6.1:
citing_entity TEXT NOT NULL(CHECK enum: 5 values per N8 RESOLVED-S234).citing_entity_id UUID NOT NULL.cited_q_a_pair_id UUID NOT NULL REFERENCES q_a_pairs(id).q_a_pair_version_at_cite UUID NULL(snapshot ID intoq_a_pair_history; NULL pre-shipment, set onready_for_exportstate transition).created_at,updated_attimestamps.
For the procurement application, the citation INSERT pattern (called from the state-machine transition handler at the ready_for_export step):
async function captureCitationsOnShip(workspaceId: string, client: SupabaseClient) { const responses = await client .from('bid_responses') .select('id, q_a_pair_source_id') .eq('workspace_id', workspaceId) .not('q_a_pair_source_id', 'is', null); for (const r of responses) { const latestVersion = await client .from('q_a_pair_history') .select('id') .eq('q_a_pair_id', r.q_a_pair_source_id) .order('valid_from', { ascending: false }) .limit(1) .single(); await client.from('citations').insert({ citing_entity: 'bid_response', citing_entity_id: r.id, cited_q_a_pair_id: r.q_a_pair_source_id, q_a_pair_version_at_cite: latestVersion.data?.id ?? null, }); }}Lands in: T11 per PLAN.md §4.11 (citations polymorphic enum + version-on-cite). This spec is the consumer-spec anchor; the implementation is T11-owned.
Gate: T2 + T6 + T11 (citations table) + the q_a_pair_history trigger from PLAN.md §4.6 T6.
Validation:
- Integration test: transition a workspace to
ready_for_export; assertcitationsrows created withq_a_pair_version_at_citepopulated. - Version-on-cite immutability test: cite Q&A pair v1 → workspace ships → UC6 revises Q&A pair to v2 → the shipped citation still resolves to v1 (per
05-qa-flow.md§6.3 RATIFIED-S229).
T-13 — MCP tool rename (implements B-27)
Section titled “T-13 — MCP tool rename (implements B-27)”Current state: lib/mcp/tools/bids.ts registers:
list_active_bidsget_bid_detailget_bid_question
Plus cite_content + get_content_effectiveness in adjacent tool modules (application-agnostic; no rename per 06-mcp-tooling.md §6.3).
Target state:
lib/mcp/tools/procurement.ts(post-git mv) — same file content with renamed tool registrations:list_active_bids→list_active_procurement.get_bid_detail→get_procurement_detail.get_bid_question→get_procurement_question.
Lands in: T4 code rename per PLAN.md §4.4 T4 subtask 6 (MCP tool name updates per 06-mcp-tooling.md §6.3). [LOCKSTEP-WITH-MIGRATION] per 06-mcp-tooling.md §6.3 — tool renames ship in the combined PR window, NOT before.
Gate: T2 + T4.
Validation:
- MCP eval Layer 1 (protocol compliance, 42 checks) passes with renamed tools.
- MCP eval Layer 4 (functional correctness, 37 checks, live DB) passes with renamed tools (per CLAUDE.md “Commands”
bun run test:mcp-eval:fc). - Plugin bundle regenerated post-rename:
bun run build:plugin+ commitlib/mcp/plugin-bundle.tsper CLAUDE.md “Plugin bundle is committed” gotcha. docs/generated/mcp-inventory.mdregenerated:bun run generate:mcp-inventoryper CLAUDE.md “Commands”.
T-14 — RLS policies for procurement_workspaces [LOCKSTEP-WITH-MIGRATION] (implements B-29)
Section titled “T-14 — RLS policies for procurement_workspaces [LOCKSTEP-WITH-MIGRATION] (implements B-29)”Current state: Pre-rename, bid_workspaces carries RLS policies (verify post-T2 via pg_policy query). Per docs/specs/reserved-workspace-seats/TECH.md T-5, the pattern delegates to the parent workspaces row via an EXISTS subquery.
Target state: Post-rename procurement_workspaces carries 4 RLS policies (select / insert / update / delete) per the RWS S-5 pattern:
CREATE POLICY procurement_workspaces_selectON public.procurement_workspacesFOR SELECTUSING ( EXISTS ( SELECT 1 FROM workspaces w WHERE w.id = procurement_workspaces.workspace_id AND ( -- tenant access check delegated to workspaces-level pattern; -- exact predicate matches workspaces' own RLS USING clause -- (role-based via get_user_role() per CLAUDE.md "Supabase & Schema" RLS) ) ));-- Identical pattern for INSERT, UPDATE, DELETE.Auto-RLS event trigger from docs/specs/rls-pattern/TECH.md T-1 enables RLS on the renamed table without explicit ALTER TABLE ... ENABLE ROW LEVEL SECURITY — verifies via pg_class.relrowsecurity = true post-rename.
Per-role grants via grant_standard_public_table_access('public.procurement_workspaces'::regclass) from RLS-PATTERN T-2.
Lands in: T2 combined-PR migration per PLAN.md §4.2 T2 subtask 7 (in scope of the combined-PR — the 5 reserved seats per RWS T-1; procurement_workspaces inherits the same grant pattern as part of the rename).
Gate: T3 (RLS-PATTERN combined migration apply).
Validation: Per docs/specs/reserved-workspace-seats/TECH.md T-5 validation pattern:
- Connect as
authenticatedrole for tenant A; insert workspace + procurement_workspaces row for tenant A; assert visible. - Switch to tenant B; assert procurement_workspaces row NOT visible.
- Migration test fixture: copy pattern from
docs/specs/reserved-workspace-seats/TECH.mdT-5 test fixture, adapted for the 6 typed columns.
Testing and validation
Section titled “Testing and validation”The test plan maps every B-N invariant to one or more concrete verification steps:
Unit (Vitest)
Section titled “Unit (Vitest)”- B-5 state-machine values verbatim:
__tests__/lib/procurement/procurement-workflow.test.ts(renamed from__tests__/lib/bid/bid-state-machine.test.ts) — assertPROCUREMENT_WORKFLOW_STATES.length === 10; assert each value=== BID_STATES_v1[i]against a snapshot const for pre-rename state-set parity. - B-9 confidence-posture taxonomy: assert
CONFIDENCE_POSTURE_CONFIG(types/bid.ts:123-156→ renamed file) preserves the 4-value enum verbatim (strong_match/partial_match/needs_sme/no_content). - B-15 tracked-changes detection:
lib/extraction/docx-tracked-changes.test.ts— fixtures (clean, w:ins-only, w:del-only, both); boolean + counts match Python per EP8 §9.1. - B-15 filename heuristic:
lib/bid-library-ingest/filename-heuristic.test.ts— table of (incoming, existing, expected) cases per EP8 §9.1.
Integration (Vitest)
Section titled “Integration (Vitest)”- B-3 + B-6 + B-7 satellite columns + state transitions:
__tests__/integration/procurement-satellite.integration.test.ts(NEW) — create workspace, transition through state machine, assert satellite columns populated correctly at each transition. - B-9 + B-10 composer two-step retrieval:
__tests__/integration/procurement-composer.integration.test.ts(NEW) — mockquestion_matches, render composer, assert list + verbatim queries fire in order. - B-12 citation version-on-cite:
__tests__/integration/procurement-citation-version.integration.test.ts(NEW) — promote Q&A pair, ship workspace, revise Q&A pair to v2, assert shipped citation still resolves to v1. - B-14 4-stage import flow:
__tests__/app/api/ingest/qa-docx/presign.test.ts+analyse.test.ts+import.test.tsper EP8 §9.2. - B-15 CLI ↔ UI parity:
__tests__/integration/bid-library-ingest/parity.integration.test.ts(existing, extended per EP8 §9.3) — gated behindKH_RUN_INTEGRATION=1. - B-17 UC5 promotion:
__tests__/integration/uc5-promotion.integration.test.ts(NEW) — happy path + close-match supersede option + cosine-threshold tunability. - B-18 lost-bid default-skip: assert promotion request without
override_skipfor alostworkspace returns 400. - B-29 RLS isolation:
__tests__/integration/procurement-rls.integration.test.ts(NEW) — tenant A creates workspace + satellite; tenant B cannot read. Perdocs/specs/reserved-workspace-seats/TECH.mdT-5 pattern adapted to procurement.
E2E (Playwright)
Section titled “E2E (Playwright)”- B-14 import happy path:
e2e/tests/bid-library-import.spec.tsper EP8 §9.4 — log in as admin; upload fixture; click Analyse; click Import; assert summary card renders. - B-27 MCP tool rename: MCP eval Layer 1 + Layer 4 (existing
bun run test:mcp-eval+bun run test:mcp-eval:fc) confirm renamed tools register cleanly.
Parity guards
Section titled “Parity guards”- B-5 state-set preservation: CI guard test
__tests__/guards/procurement-state-set-parity.test.tsassertsPROCUREMENT_WORKFLOW_STATESmatches the pre-renameBID_STATESsnapshot. The snapshot is committed at rename time and referenced by the guard going forward; any future state-set change requires a separate ratification cycle per B-5. - B-3 satellite column shape: CI guard test
__tests__/guards/procurement-satellite-columns.test.tssnapshots the 6 column names + types viainformation_schema.columns; fails on accidental schema drift. - B-27 MCP tool name parity: existing
mcp-fixture-sync.test.ts(per CLAUDE.md “Guard tests break on structural changes”) detects accidental regression to old tool names.
Manual
Section titled “Manual”- B-8 composer page render: load
/workspaces/<procurement-id>/questionsin browser; verify ranked match candidates render with separate score columns. - B-13 settings surface entry: log in as admin; verify “Bid Library Import” section appears in
/settings; log in as editor / reviewer / viewer; verify section is NOT visible.
Risks and mitigations
Section titled “Risks and mitigations”Risk: Backfill SQL data loss on the residual JSONB
Section titled “Risk: Backfill SQL data loss on the residual JSONB”Description: The T-3 backfill UPDATE reads workspaces.domain_metadata->>'buyer' etc. and writes to the typed columns. If any source row has a typo in the JSONB key (e.g. "Buyer" instead of "buyer"), the backfill silently inserts NULL into the typed column.
Mitigation:
- Pre-migration: audit
SELECT DISTINCT jsonb_object_keys(domain_metadata) FROM workspaces WHERE type='bid'against the canonicalBidMetadatafield set; resolve any drift. - Backfill verification: post-apply,
SELECT count(*) FROM procurement_workspaces WHERE buyer = ''should match the count of pre-renamebid_workspacesrows with nobuyerJSONB value (verify the two counts pre/post).
Risk: T2 migration locks workspaces table for the entire combined-PR
Section titled “Risk: T2 migration locks workspaces table for the entire combined-PR”Description: The combined PR alters workspaces (drop type col + add FK), bid_workspaces → procurement_workspaces, entity_aliases.category → provenance, and 3 form_templates tables in one transaction (per PLAN.md §4.2 T2 risk). DDL lock contention on a high-traffic table can block reads.
Mitigation:
- Apply during low-traffic window per
PLAN.md§4.2 T2 risk mitigation. - Stage DDL into ordered blocks within one transaction (renames first, FK swap last); verify against staging branch (
turayklvaunphgbgscat) first per CLAUDE.md “Supabase & Schema” gotcha. - Per CLAUDE.md “
mcp__supabase__apply_migrationauto-generates server-side timestamps” — rename local file to match post-apply; UPDATE staging row if drift detected.
Risk: T7 cutover breaks B-16 import path
Section titled “Risk: T7 cutover breaks B-16 import path”Description: B-16 says imported Q&A pairs land in content_items pre-T7 and q_a_pairs post-T7. If the import-route handler is not updated atomically with T7 cutover, imports during the cutover window land in the wrong table.
Mitigation:
- T7 sequencing per
PLAN.md§4.7 includes a cutover-window plan; the import route MUST be updated in the same window. - Integration test: post-T7 cutover, manually exercise import; verify new rows land in
q_a_pairswithorigin_kindset correctly. - Rollback: per
PLAN.md§4.7 T7 the soft-archive + 1-year retention provides a rollback path.
Risk: REST PATCH on wrong UUID silently no-ops on procurement state-transition writes
Section titled “Risk: REST PATCH on wrong UUID silently no-ops on procurement state-transition writes”Description: Per CLAUDE.md “REST PATCH on wrong UUID” gotcha — REST PATCH returns 200 OK with 0 rows if the UUID does not match.
Mitigation:
- T-5 (state-transition write) uses
sb()from@/lib/supabase/safeper CLAUDE.md “Silent failures in Supabase calls” gotcha. - Every UPDATE re-queries by primary key post-write to verify the row was actually updated.
- Prefer a Postgres function (
commit_procurement_transition(...)) that runs both writes in one transaction; the function returns row-count, surfacing silent no-ops as explicit errors.
Risk: EP8 absorption supersession ambiguity
Section titled “Risk: EP8 absorption supersession ambiguity”Description: The standalone EP8 spec (docs/specs/ep8-qa-docx-import-ui-spec.md v5) is RATIFIED-S242 absorbed by this spec but lives in the repo at S242 close. Two-source-of-truth risk between EP8 v5 and this spec’s §C section.
Mitigation:
- S242 close action (Wave 3 deliverable per S242 prompt): archive EP8 v5 to
.planning/.archive/.specs/and add a redirect note pointing to this spec’s §C. - Until Wave 3 close: explicit
[ABSORBED-S242 EP8 v5 §X]markers on every behaviour preserved verbatim; any divergence (e.g. post-T7 cutover behaviour) is flagged inline. - This spec’s
Last verifiedheader is the freshest source-of-truth signal; EP8 v5’s Last verified is 28/04/2026 — readers should prefer this spec when in doubt.
Gap flags
Section titled “Gap flags”[GAP-PROC-001] T2 migration body — residual JSONB column retain-vs-drop decision
Section titled “[GAP-PROC-001] T2 migration body — residual JSONB column retain-vs-drop decision”Category: Implementation decision needed (DB migration body authoritative).
Status: Per T-3 above, the 6 non-promoted BidMetadata fields (reference_number, estimated_value, tender_source, tender_document_ids, outcome_notes, notes) EITHER stay in a residual JSONB column on procurement_workspaces for migration-tail compatibility OR drop entirely. Authoritative decision lives in the T2 migration body, not in this spec.
Sources checked: 04-workspace-types.md §4.2 (column scope per OQ-Q38-E — 6 columns named, residual JSONB not explicitly addressed); PLAN.md §4.2 T2 subtask 1 (migration SQL ownership — combined-PR scope); types/bid.ts:26-40 BidMetadata shape (12 fields; 6 promote, 6 do not).
Prerequisite work: T2 migration draft author decides at draft time. If retain: keep domain_metadata JSONB on procurement_workspaces (not workspaces — the 6 fields are procurement-specific). If drop: explicit ALTER TABLE workspaces DROP COLUMN domain_metadata in the migration body. v1.1 candidate: retire the residual JSONB column once T2 migration tail is fully drained.
Decision-maker: T2 migration draft author per PLAN.md §4.2 T2.
[GAP-PROC-002] Post-T7 origin_kind enum value for EP8 imports
Section titled “[GAP-PROC-002] Post-T7 origin_kind enum value for EP8 imports”Category: T7 sequencing decision needed.
Status: Per B-16 + T-9, post-T7 cutover the EP8 import-route INSERT target switches from content_items to q_a_pairs. The origin_kind enum value for EP8-driven imports is not frozen in this spec — T7 owns the enum extension.
Sources checked: 05-qa-flow.md §2.1 (existing origin_kind values: extracted_from_corpus / curated_explicit / derived_from_bid_response / imported_legacy); EP8 v5 §7.1 (pre-T7 content_type='q_a_pair' + ingest_source='qa_import'); PLAN.md §4.7 T7 (data migration sequencing).
Prerequisite work: T7 cutover decision — extend origin_kind enum with imported_docx_v1 (or similar) for EP8-driven imports vs reusing imported_legacy. Decision lives in T7 migration body per PLAN.md §4.7 T7.
Decision-maker: T7 migration draft author per PLAN.md §4.7 T7.
[GAP-PROC-003] State-transition atomicity pattern — TS-side vs Postgres function
Section titled “[GAP-PROC-003] State-transition atomicity pattern — TS-side vs Postgres function”Category: Implementation pattern choice.
Status: Per T-5, the state-transition write touches two tables (workspaces.status + procurement_workspaces.* side-effects). Atomicity is achieved EITHER inside a Postgres function (one transaction) OR via TS-side Promise.all with manual rollback. The spec does NOT freeze the choice; T4 implementation author decides.
Sources checked: lib/bid/bid-state-machine.ts:48-86 (current pattern — TS-only canTransition() + downstream caller writes; full file is 86 lines); CLAUDE.md “REST PATCH on wrong UUID” + “Silent failures in Supabase calls” gotchas (TS-side risk); PLAN.md §4.4 T4 (procurement workflow rename — no atomicity pattern frozen).
Prerequisite work: Recommend Postgres-function pattern for atomicity + observability (the function can emit RAISE LOG audit trail per RLS-PATTERN P-5 pattern); fallback to TS-side Promise.all with row-count re-query verification per CLAUDE.md gotcha.
Decision-maker: T4 implementation author per PLAN.md §4.4 T4.3.
Cross-doc cross-references
Section titled “Cross-doc cross-references”| Doc | What it references |
|---|---|
docs/specs/id-31-canonical-pipeline-implementation-plan/PLAN.md §4.4 T4 + §4.7 T7 + §5 row “procurement-workspaces” | Implementation task that consumes this spec |
docs/plans/phase-0-investigation/architecture/04-workspace-types.md §4.2 | OQ-Q38-E 6-column ratification (this spec FILLS IN the per-app columns) |
docs/plans/phase-0-investigation/architecture/04-workspace-types.md §7 | Procurement umbrella rename cascaded effects (cited by T-1..T-4) |
docs/plans/phase-0-investigation/architecture/05-qa-flow.md §7 + §8 + §9 | Q&A composer + UC6 + UC5 (forward-refs from B-9, B-21, B-17) |
docs/plans/phase-0-investigation/architecture/08-new-features.md §4 + §5 + §7 | change_reports, UC9, bid feedback 3-UC (forward-refs from B-25, B-26, B-20) |
docs/plans/phase-0-investigation/architecture/02-data-flow.md §3 + §8 | cocoindex 6-stage flow + edit_intent policy (forward-refs from B-23) |
docs/plans/phase-0-investigation/architecture/06-mcp-tooling.md §6.3 | MCP tool rename direction (cited by T-13) |
docs/specs/reserved-workspace-seats/{PRODUCT,TECH}.md | Substrate spec — procurement seat is OUT of scope for RWS; this spec FILLS IN per-app columns |
docs/specs/rls-pattern/{PRODUCT,TECH}.md P-1..P-5 | RLS auto-trigger + grants helper inherited (cited by T-14) |
docs/specs/ep8-qa-docx-import-ui-spec.md v5 | ABSORBED-S242 — superseded by §C in PRODUCT.md + T-7..T-10 here; standalone EP8 spec to be archived in Wave 3 per S242 prompt |
Future docs/specs/qa-promotion-ui/PRODUCT.md (CONDITIONAL) | UC5 promotion-UI shape sub-decisions (deferred from B-19) |
Future docs/specs/procurement-question-matching/TECH.md (CONDITIONAL) | Per-method scoring tunability (deferred from B-9) |
Source-doc supersession
Section titled “Source-doc supersession”The standalone EP8 v5 spec (docs/specs/ep8-qa-docx-import-ui-spec.md) is [ABSORBED-S242] by §C of this spec’s PRODUCT.md + T-7..T-13 in this TECH.md. The EP8 spec is preserved at its current path during S242 for audit-trail purposes; archival to .planning/.archive/.specs/ is a Wave 3 deliverable per the S242 prompt. Per CLAUDE.md “Historical planning” — .planning/.archive/.specs/ is the destination directory pattern.
Heritage docs feeding this sub-doc
Section titled “Heritage docs feeding this sub-doc”| Doc | Date | Status | Useful for |
|---|---|---|---|
docs/plans/phase-0-investigation/architecture/04-workspace-types.md §4 + §7 + §8 | 14-15/05/2026 (S239 + S240 ratifications) | [CURRENT-CANONICAL] for the satellite pattern + procurement rename | Substrate for T-1..T-4, T-14 |
docs/plans/phase-0-investigation/architecture/05-qa-flow.md §6 + §7 + §8 + §9 | 15/05/2026 (S240 Wave 2 split) | [CURRENT-CANONICAL] for citations / question_matches / UC6 / UC5 | Substrate for T-11, T-12; forward-refs from B-9, B-12, B-17, B-21 |
docs/plans/phase-0-investigation/architecture/08-new-features.md §3 + §4 + §5 + §7 | 15/05/2026 (S240 Wave 2 tail) | [CURRENT-CANONICAL] for change_reports / UC8 / UC9 / bid feedback 3-UC | Substrate for B-20..B-22, B-25, B-26 |
docs/specs/ep8-qa-docx-import-ui-spec.md v5 | 28/04/2026 (S209 rewrite) | [ABSORBED-S242] — absorbed by §C of this spec; archival in Wave 3 | Substrate for T-7..T-10 (4-stage UI flow, post-insert pipeline, payload shape, pipeline_runs two-phase pattern) |
docs/specs/reserved-workspace-seats/{PRODUCT,TECH}.md | 15/05/2026 (S240 NEW) | [CURRENT-CANONICAL] for the seat pattern this spec extends | Substrate for T-14 (RLS pattern); cardinality + naming conventions for B-4 |
docs/specs/id-38-rls-pattern/{PRODUCT,TECH}.md | 14/05/2026 (S239 NEW) | [CURRENT-CANONICAL] for the auto-RLS + grants pattern inherited | Substrate for T-14 |
docs/specs/id-31-canonical-pipeline-implementation-plan/PLAN.md | 18/05/2026 (S241) | [CURRENT-CANONICAL] for task sequencing + dependency graph | Source for T-N → PLAN.md task mappings throughout |
types/bid.ts (205 lines) | (current code) | [CURRENT-CANONICAL] pre-rename | Substrate for T-2 (state machine), T-3 (BidMetadata shape), T-4 (sweep), T-6 (composer types) |
lib/bid/bid-state-machine.ts (86 lines) | (current code) | [CURRENT-CANONICAL] pre-rename | Substrate for T-2 + T-5 (state-machine code) |
End of TECH spec. Numbered invariants in ./PRODUCT.md.