Skip to content

Procurement Workspaces — TECH

⚠️ PARTIALLY SUPERSEDED (S462, 2026-07-11) — DR-038 + ID-145. Section A (the workspace umbrella, the procurement_workspaces satellite B-3/B-4, and the workspace-level 10-state workflow B-5..B-7) is superseded — id-130 moved per-stage state onto form_templates and 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-145specs/id-145-procurement-form-first/RESEARCH.md.

Status: PARTIALLY-SUPERSEDED (S462) — NEW-S242. Companion to PRODUCT.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.

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.

  • ./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.md v5 (RATIFIED-S242 — ABSORBED-S242; TECH content folded into T-7..T-13 below).

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-migration SELECT count(*) FROM workspaces WHERE type='bid' (currently 4 prod rows per 04-workspace-types.md §2.2; verify via mcp__supabase__execute_sql pre-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-14BID_STATES const tuple (10 values: draft, questions_extracted, matching, drafting, in_review, ready_for_export, submitted, won, lost, withdrawn).
  • types/bid.ts:16BidState TS type derived from BID_STATES.
  • lib/bid/bid-state-machine.ts — 86 lines. Imports BidState + BID_STATES from @/types/bid; re-exports both; exports BID_STATE_LABELS, BID_STATE_SHORT_LABELS, BID_STATE_COLOURS, VALID_TRANSITIONS (internal const), canTransition(), getAvailableTransitions(), isTerminal(), isActive(), BID_STATE_PROGRESSION.
  • types/bid-metadata.tsBidMetadata interface (40 lines).

Target state:

  • types/procurement.ts — receives the moved BID_STATESPROCUREMENT_WORKFLOW_STATES const (10 values preserved verbatim per Q-OQR1-02 cascaded-renames table — 04-workspace-types.md §7.2); BidStateProcurementWorkflowState type.
  • lib/procurement/procurement-workflow.ts — receives the moved state-machine file (86 lines preserved verbatim except for renamed identifiers).
  • All exports rename:
    • BID_STATESPROCUREMENT_WORKFLOW_STATES.
    • BID_STATE_LABELSPROCUREMENT_WORKFLOW_STATE_LABELS.
    • BID_STATE_SHORT_LABELSPROCUREMENT_WORKFLOW_STATE_SHORT_LABELS.
    • BID_STATE_COLOURSPROCUREMENT_WORKFLOW_STATE_COLOURS.
    • BID_STATE_PROGRESSIONPROCUREMENT_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 test full 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 per PLAN.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) preventing bid regressions 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_workspaces table exists (the legacy satellite); per-bid metadata lives in workspaces.domain_metadata JSONB blob (per types/bid.ts:42-54 Bid interface — domain_metadata: BidMetadata).
  • types/bid.ts:26-40BidMetadata interface 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):

  1. Rename bid_workspacesprocurement_workspaces (per 04-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 NULL with UNIQUE(workspace_id) constraint enforcing the 1:1 cardinality from B-4, plus created_at / updated_at timestamps.
  2. 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 NULL with CHECK (outcome IS NULL OR outcome IN ('won','lost','withdrawn')).
    • outcome_recorded_at TIMESTAMPTZ NULL.
    • outcome_recorded_by UUID NULL REFERENCES auth.users(id).
  3. Backfill the 6 columns from existing workspaces.domain_metadata JSONB blobs. SQL pattern:
    UPDATE procurement_workspaces pw
    SET
    buyer = 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')::uuid
    FROM workspaces w
    WHERE pw.workspace_id = w.id;
  4. Decision per T2 migration body: EITHER drop workspaces.domain_metadata entirely after backfill OR retain a residual JSONB column for migration-tail compatibility (the 6 non-promoted BidMetadata fields — reference_number, estimated_value, tender_source, tender_document_ids, outcome_notes, notes). If retain, the residual JSONB lives on procurement_workspaces (NOT on workspaces — the 6 fields are procurement-specific), populated from the read-side of the backfill before workspaces.domain_metadata is dropped. Authoritative decision lives in T2 migration body, not in this spec — see PLAN.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_workspaces rows pre-migration, capture their domain_metadata JSONB, 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 with 23514 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 per 06-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 via git mv per PLAN.md §4.4 T4.2; file names lose the bid- prefix where the file is procurement-only OR keep the prefix where the file describes a form_type legitimately).
  • 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) OR types/procurement-form.ts (composer surface types).
  • lib/mcp/tools/procurement.ts (post-rename) — tools rename: list_active_bidslist_active_procurement, get_bid_detailget_procurement_detail, get_bid_questionget_procurement_question.
  • All project_idworkspace_id per 04-workspace-types.md §7.4 (44-file sweep). Affects types/bid.ts:97 BidQuestion.project_idworkspace_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 per PLAN.md §4.4 T4 acceptance criteria.
  • bun run knip shows no orphaned lib/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:

lib/procurement/procurement-workflow.ts
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, assert procurement_workspaces.submission_date is non-NULL and within 1s of now().
  • Integration test: create workspace, transition to won, assert outcome='won' + outcome_recorded_at non-NULL + outcome_recorded_by = the calling user’s UUID.
  • Negative test: transition from submitted to submitted (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 via git mv per PLAN.md §4.4 T4.5).
  • The composer renders inside the procurement workspace’s question-review page; the legacy kb-integration-review.tsx consumes the question_matches table (post-T10 per PLAN.md §4.10) with separate embedding_score + fulltext_score columns surfaced per question.
  • Data fetching: TanStack Query exclusively per CLAUDE.md “Data fetching” gotcha. Query keys in lib/query/query-keys.ts; fetchers in lib/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.tsx post-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 captures citing_entity='bid_response' per B-12.
  • A11y: composer keyboard-navigable; match candidates surfaced via semantic <table> with aria-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.tsx exists.
  • supabase/migrations/20260428174512_add_ingest_source_to_content_items.sql — adds ingest_source column to content_items with '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/:

RoutePurposeAuth
presign/route.tsIssue short-lived Supabase Storage presigned URLs (5 min)getAuthorisedClient(['admin'])
analyse/route.tsPre-flight analysis: tracked-changes detection, pair count, draft/final heuristic, dedup hintsgetAuthorisedClient(['admin'])
import/route.tsExecute the import; create pipeline_runs row; kick off async importgetAuthorisedClient(['admin'])

Pre-flight helpers:

HelperPathNotes
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:

FilePurpose
app/settings/page.tsxAdd 'bid-library-import' section enum value (post-T4 rename MAY become 'procurement-library-import')
components/settings/settings-sidebar.tsxExisting — useUserRole admin filter gates the section entry
components/settings/bid-library-import-section.tsxNEW — entry point lazy-loaded by sidebar
components/settings/bid-library-import/upload-dropzone.tsxNEW — drag-and-drop + file picker; uploads via presigned URL
components/settings/bid-library-import/analysis-table.tsxNEW — pre-flight results table
components/settings/bid-library-import/import-options-form.tsxNEW — checkboxes for auto_supersede, require_clean, force, skip_dedup, batch_tag
components/settings/bid-library-import/import-progress-poller.tsxNEW — TanStack Query polling renderer (1-2s interval)
components/settings/bid-library-import/import-summary-card.tsxNEW — 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.ts to invoke the TS analyse + import pipeline against the same fixtures as the CLI; assert structural equality. Gated behind KH_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:

  1. History insert (content_history) — writeHistory: boolean = true. change_reason is mandatory per CLAUDE.md feedback_content_history_change_reason_mandatory precedent.
  2. Layer inference (lib/ai/layer-inference.ts::inferLayerFlag) — inferLayerFlag: boolean = true.
  3. Chunk + embedwriteChunks: boolean = true. Per CLAUDE.md “Embedding vector serialisation” — use JSON.stringify(embedding) for Supabase RPC vector params.
  4. Classification (lib/ai/classify.ts::classifyContent) — gated by classification param being passed. Per CLAUDE.md “classifyContent userId must be a UUID” — pipeline service-account UUID a0000000-0000-4000-8000-000000000001 for non-user invocations.
  5. Entity extractionstoreEntitiesFlag: boolean = true.
  6. Temporal metadata bridgebridgeTemporal: boolean = true.
  7. Temporal metadata writewriteTemporal: boolean = true.
  8. 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_hash is GENERATED ALWAYS per CLAUDE.md gotcha — OMIT from payload entirely. PG auto-computes via md5(normalised content).
  • summary is the column name (NOT ai_summary) per CLAUDE.md “content_items.summary (not ai_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_roles row, 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: true for 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 sets progress JSONB + optionally completed_at + status.
  • Final-state row MAY use recordPipelineRun() from @/lib/pipeline/record-run for the single-insert-on-finish pattern.

recordPipelineRun signature (per EP8 v5 §7.2 footnote + CLAUDE.md recordPipelineRun() cron pattern):

  • supabase client arg is REQUIRED (not optional).
  • status enum: 'completed' | 'completed_with_errors' | 'failed' (NOT 'partial', NOT 'complete').
  • Completion-record field is result (not metadata).
  • itemsCreated is string[] (item IDs), not a count.

Schema-verified claims (per EP8 v5 §7.2):

  • Column name is pipeline_name (NOT run_type).
  • steps_total / steps_completed are inside progress JSONB, not top-level columns.
  • Valid status CHECK values: running, completed, completed_with_errors, failed.
  • Raw inserts MUST use sb() from @/lib/supabase/safe per the silent-failure-prevention spec.

Lands in: EP8 import route (T-7 above).

Validation:

  • Integration test: invoke import; poll GET /api/pipeline-runs/:id; assert progress JSONB transitions through expected step values; assert final status is 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):

  1. UI surface (per B-19 — docs/specs/qa-promotion-ui/PRODUCT.md is the UI spec destination): post-ship surface in components/procurement/response-actions.tsx (post-rename) exposes a “promote to Q&A library” action when the workspace state is ready_for_export / submitted (and not in lost per default-skip B-18).
  2. Server endpoint: POST /api/q-a-pairs/promote-from-bid-response accepts { bid_response_id, override_skip?: boolean } payload.
  3. Server logic:
    • Look up the source bid_response + its bid_question + parent procurement_workspace.
    • If workspace outcome='lost' and override_skip not set → return 400 with “lost-bid skip-default” message (B-18).
    • Run cosine-similarity check against existing q_a_pairs (threshold 0.85 default per 05-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_pair row with origin_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_review via the existing cross-application review queue (per 0.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_pair draft created with the lineage columns populated.
  • Negative test: promote a lost-workspace bid response without override_skip → returns 400.
  • Concurrency test: two simultaneous promotion requests for the same bid_response_id → only one q_a_pair row 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 into q_a_pair_history; NULL pre-shipment, set on ready_for_export state transition).
  • created_at, updated_at timestamps.

For the procurement application, the citation INSERT pattern (called from the state-machine transition handler at the ready_for_export step):

lib/procurement/citation-capture.ts
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; assert citations rows created with q_a_pair_version_at_cite populated.
  • 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_bids
  • get_bid_detail
  • get_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_bidslist_active_procurement.
    • get_bid_detailget_procurement_detail.
    • get_bid_questionget_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 + commit lib/mcp/plugin-bundle.ts per CLAUDE.md “Plugin bundle is committed” gotcha.
  • docs/generated/mcp-inventory.md regenerated: bun run generate:mcp-inventory per 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_select
ON public.procurement_workspaces
FOR SELECT
USING (
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 authenticated role 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.md T-5 test fixture, adapted for the 6 typed columns.

The test plan maps every B-N invariant to one or more concrete verification steps:

  • B-5 state-machine values verbatim: __tests__/lib/procurement/procurement-workflow.test.ts (renamed from __tests__/lib/bid/bid-state-machine.test.ts) — assert PROCUREMENT_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.
  • 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) — mock question_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.ts per EP8 §9.2.
  • B-15 CLI ↔ UI parity: __tests__/integration/bid-library-ingest/parity.integration.test.ts (existing, extended per EP8 §9.3) — gated behind KH_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_skip for a lost workspace returns 400.
  • B-29 RLS isolation: __tests__/integration/procurement-rls.integration.test.ts (NEW) — tenant A creates workspace + satellite; tenant B cannot read. Per docs/specs/reserved-workspace-seats/TECH.md T-5 pattern adapted to procurement.
  • B-14 import happy path: e2e/tests/bid-library-import.spec.ts per 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.
  • B-5 state-set preservation: CI guard test __tests__/guards/procurement-state-set-parity.test.ts asserts PROCUREMENT_WORKFLOW_STATES matches the pre-rename BID_STATES snapshot. 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.ts snapshots the 6 column names + types via information_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.
  • B-8 composer page render: load /workspaces/<procurement-id>/questions in 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.

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 canonical BidMetadata field set; resolve any drift.
  • Backfill verification: post-apply, SELECT count(*) FROM procurement_workspaces WHERE buyer = '' should match the count of pre-rename bid_workspaces rows with no buyer JSONB 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_workspacesprocurement_workspaces, entity_aliases.categoryprovenance, 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_migration auto-generates server-side timestamps” — rename local file to match post-apply; UPDATE staging row if drift detected.

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_pairs with origin_kind set 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/safe per 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 verified header 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-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.


DocWhat 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.2OQ-Q38-E 6-column ratification (this spec FILLS IN the per-app columns)
docs/plans/phase-0-investigation/architecture/04-workspace-types.md §7Procurement umbrella rename cascaded effects (cited by T-1..T-4)
docs/plans/phase-0-investigation/architecture/05-qa-flow.md §7 + §8 + §9Q&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 + §7change_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 + §8cocoindex 6-stage flow + edit_intent policy (forward-refs from B-23)
docs/plans/phase-0-investigation/architecture/06-mcp-tooling.md §6.3MCP tool rename direction (cited by T-13)
docs/specs/reserved-workspace-seats/{PRODUCT,TECH}.mdSubstrate 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-5RLS auto-trigger + grants helper inherited (cited by T-14)
docs/specs/ep8-qa-docx-import-ui-spec.md v5ABSORBED-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)

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.

DocDateStatusUseful for
docs/plans/phase-0-investigation/architecture/04-workspace-types.md §4 + §7 + §814-15/05/2026 (S239 + S240 ratifications)[CURRENT-CANONICAL] for the satellite pattern + procurement renameSubstrate for T-1..T-4, T-14
docs/plans/phase-0-investigation/architecture/05-qa-flow.md §6 + §7 + §8 + §915/05/2026 (S240 Wave 2 split)[CURRENT-CANONICAL] for citations / question_matches / UC6 / UC5Substrate 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 + §715/05/2026 (S240 Wave 2 tail)[CURRENT-CANONICAL] for change_reports / UC8 / UC9 / bid feedback 3-UCSubstrate for B-20..B-22, B-25, B-26
docs/specs/ep8-qa-docx-import-ui-spec.md v528/04/2026 (S209 rewrite)[ABSORBED-S242] — absorbed by §C of this spec; archival in Wave 3Substrate 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}.md15/05/2026 (S240 NEW)[CURRENT-CANONICAL] for the seat pattern this spec extendsSubstrate for T-14 (RLS pattern); cardinality + naming conventions for B-4
docs/specs/id-38-rls-pattern/{PRODUCT,TECH}.md14/05/2026 (S239 NEW)[CURRENT-CANONICAL] for the auto-RLS + grants pattern inheritedSubstrate for T-14
docs/specs/id-31-canonical-pipeline-implementation-plan/PLAN.md18/05/2026 (S241)[CURRENT-CANONICAL] for task sequencing + dependency graphSource for T-N → PLAN.md task mappings throughout
types/bid.ts (205 lines)(current code)[CURRENT-CANONICAL] pre-renameSubstrate 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-renameSubstrate for T-2 + T-5 (state-machine code)

End of TECH spec. Numbered invariants in ./PRODUCT.md.