Skip to content

ID-130 {130.4} PLAN — Procurement / Form Domain Model + IA

⚠️ PARTIALLY SUPERSEDED (S462, 2026-07-11) — DR-038 + ID-145. The C/Hybrid workspace-umbrella half of this ratified model (PRODUCT B-1, B-3, B-7, B-19, B-22, B-23; TECH AD-2, T-B23 — procurement = a workspaces umbrella holding many forms, procurement_workspaces roll-up) is superseded by DR-038 (S452) + the owner ruling of 2026-07-11: a procurement item IS one form instance. The per-form half (PRODUCT B-2, B-4..B-6, B-8..B-16, B-20, B-21; TECH AD-1, AD-3, AD-4) remains the canonical keeper machinery. Rework owner: ID-145specs/id-145-procurement-form-first/RESEARCH.md. Do not cite the umbrella invariants in new specs.

PLAN — Procurement / Form Domain Model + Information Architecture

Section titled “PLAN — Procurement / Form Domain Model + Information Architecture”

STATUS: RATIFIED — S418, Liam via Lavish gate 2026-06-25. Subtasks {130.5}–{130.16} added to the ledger; priorities deliberately NOT set.

Authored S418 (25/06/2026) by a FRESH Planner (Q-PLANNER-2 / B4 — not the {130.2} PRODUCT or {130.3} TECH author) against the RATIFIED spec chain: RESEARCH.md (D1–D8 §6.0), PRODUCT.md (B-1..B-25), TECH.md (T-B1..T-B25 + AD-1..AD-4 + the 13-step migration plan) — all three done + Liam-ratified (S411). This PLAN is decomposition only: it breaks the ratified TECH into implementation Subtasks {130.5}..{130.16}, sequences the staging-first migration, fixes per-Subtask file-ownership boundaries, holds the ID-45 timing gate, maps the subsumed ID-61 scope, and records the out-of-Task follow-ups. UK English throughout.

This PLAN does NOT re-litigate any ratified decision (the C/Hybrid model, the data-driven form_outcome_types CV, the pqqpsq rename, the win-rate {itt,tender,bid,rfp} final-award set, the code-resident workflow). It decomposes to implement them.

§Code-intelligence orientation cites the empirical re-verification of the TECH landings against the live working tree (procurement@HEAD, baseline main) so the Executor inherits confirmed line numbers. §Wave plan gives the serial-vs-parallel structure. §Subtask register is the human-readable mirror of the TM-shape JSON returned to the Orchestrator. §ID-45 timing gate, §Subsumed ID-61 scope, and §Out-of-Task follow-ups close the scope loop.


Code-intelligence orientation (re-verified 25/06/2026 — cited, not paraphrased)

Section titled “Code-intelligence orientation (re-verified 25/06/2026 — cited, not paraphrased)”

The {130.4} Planner re-ran gitnexus + ast/grep against the live working tree (/Users/liamj/Documents/development/procurement, baseline main) because TECH flagged the gitnexus index as 4 commits stale. Every load-bearing landing below was confirmed against the actual file (gitnexus canTransition context + grep/Read verification):

gitnexus canTransition blast radius (verified — matches TECH’s CRITICAL): 6 incoming callers — app/api/procurement/[id]/outcome/route.ts:POST, …/questions/extract/route.ts:POST, …/questions/match/route.ts:POST, app/api/procurement/[id]/route.ts:PATCH, hooks/procurement/use-procurement-actions.ts:handleStatusTransition, lib/queue/handlers/procurement-draft-all.ts:runFormDraftAllJob. canTransition body = procurement-workflow.ts:72-77 (TECH said 73 — off-by-one, the export line is 73). The de-dup must NOT change canTransition’s signature or home — only its currentStatus source moves from workspaces.status to form_templates.workflow_state.

Win-rate SQL landings (grep-verified against supabase/migrations/):

  • public.get_content_win_rate body at squash-baseline line 2248 (line 513 is the api wrapper — TECH correct). Body reads w.domain_metadata->>'outcome' as bid_outcome joining form_responses br → form_questions bq → workspaces w — confirms the rewrite target AND that the bid_outcome alias still lives in get_content_win_rate (the deferred ID-61 cleanup item; Unit-E 20260624130000 only renamed the alias in get_aggregate_win_rate_stats).
  • public.get_aggregate_win_rate_stats was rewritten by Unit-E 20260624130000 (unique_bids→unique_procurements, bid_outcome→outcome); its body still reads w.domain_metadata->>'outcome' (line 75 of that migration) — the form re-thread target.
  • Consumers: app/api/analytics/win-rate/route.ts (DomainStats 11, OverallStats 22, WinRateStatsSchema 41, .rpc('get_aggregate_win_rate_stats') 62); lib/mcp/formatters/procurements.ts:209 formatContentEffectiveness; app/api/items/[id]/effectiveness/route.ts:81 .rpc('get_content_win_rate').

form_questions re-key blast (grep-verified): 34 TS/TSX files reference form_questions (TECH’s “~24” is the narrower .from('form_questions') write-surface subset; both consistent — the re-key is additive so all stay green, but per-form reads must filter on form_template_id). 7 SQL functions read it in the squash baseline: get_aggregate_win_rate_stats, get_content_win_rate, get_form_question_stats, get_form_question_stats_batch, get_form_summary, hybrid_search, search_for_form_response. 0 Python consumers of the string form_questions in scripts/ (the cocoindex pipeline writes via the manifest / _ingest_form_branch path, not a raw table string — re-verify before the re-key if the pipeline gains a direct write).

schemas.ts de-dup sites (grep-verified): ProcurementMetadataSchema def at line 1019 (the inline .status enum at 1022-1033 per TECH); VALID_BID_STATUSES at 2007; ProcurementListParamsSchema.status: z.enum(VALID_BID_STATUSES) at 2020-2021; parseProcurementMetadata at 1049. Workflow module re-exports PROCUREMENT_WORKFLOW_STATES from @/types/procurement (line 2/4) — the canonical import target (no barrel).

pqq code sites = exactly 2 (grep-verified): lib/coverage/gap-scoring.ts:28 (pqq: 10) and components/coverage/template-coverage-content.tsx:115 (pqq: 'Pre-Qualification Questionnaire'). No other TS/TSX pqq reference.

workspaces.ts bid arg-alias (grep-verified): lib/mcp/tools/workspaces.ts:78const typeFilter = args.type === 'bid' ? 'procurement' : args.type; (guarded by if (args.type) at 77). Retire the alias.

Deferred ID-61 wire-field landings (grep-verified): active_bids/bid_summarylib/activity/bid-summary.ts + lib/dashboard.ts + lib/reorient.ts + lib/attention.ts + app/api/dashboard/route.ts + app/api/reorient/route.ts + lib/mcp/{tools/apps,app-bundles,tools/review}.ts; --bid-* tokens → app/styles/domain-tokens.css; /api/bids JSDoc → lib/queue/handlers/procurement-draft-all.ts + lib/validation/schemas.ts; bid_outcome alias → get_content_win_rate body (folded into the win-rate rewrite Subtask).

PJ landings (grep-verified): app/procurement/page.tsx:155 (“Manage bid submissions and tender responses” — PJ-2), :263/:338 ProcurementListRow (prop named bid), :326 “Create your first bid”; mcp-apps/form-dashboard/src/{types,app}.ts + mcp-apps/reorient-me/src/{types,app}.ts carry the Bid* type names (PJ-5).

OQ-3 / Q-EX2 external-library check: N/A — this decomposition introduces no net-new external-library symbols (carried verbatim from TECH §Context: all code uses internal Canonical symbols, Postgres/Supabase built-ins, and already-pinned Zod). Recorded so the Checker confirms the step was considered, not skipped.


Architecture decisions for the decomposition

Section titled “Architecture decisions for the decomposition”
  • The migration is ONE serial DDL spine, not parallel slices. TECH’s 13-step migration plan is a strict ordering dependency (the form_templates.outcome FK needs the form_outcome_types CV to exist first; the win-rate rewrite needs the CV + columns; the data migration needs every schema object). The spine is decomposed into four serial Subtasks by risk/verification boundary, NOT by table — bundling the pure-additive ADD COLUMN/CREATE TABLE steps that cannot conflict into one migration, and isolating the two highest-risk steps (the 42P13 win-rate rewrite; the data migration) so each carries its own staging verification.
  • Schema before types before code. No TS code Subtask starts until the schema + regenerated types are green (TECH step 13). This is the wave boundary: Wave 1 = schema spine, Wave 2 = types/views regen, Wave 3+ = code fan-out.
  • Code fan-out parallelises on disjoint file ownership — collisions resolved by ownership reassignment, not by hoping isolated worktrees won’t clash. The staff-engineer review found three Wave-3 shared-file collisions; each is resolved so every Wave-3 Subtask OWNS a disjoint file set:
    • lib/validation/schemas.ts is owned by {130.10} (the enum de-dup). The /api/bids JSDoc cleanup that {130.16} would otherwise touch in the same file is folded INTO {130.10} (FIX 1) — {130.16} no longer touches schemas.ts.
    • app/api/procurement/[id]/route.ts is owned entirely by {130.11}: the T-B1 API GET read-shape change (roll-up off procurement_workspaces + list child forms off form_templates, replacing the domain_metadata read) moves from {130.13} INTO {130.11} (FIX 2). {130.13} now owns ONLY the UI page app/procurement/[id]/page.tsx (+ page siblings). T-B1 read-shape is split: API GET → {130.11}, UI consumption → {130.13}.
    • lib/query/query-keys.ts is a 3-way collision ({130.12}/{130.13}/{130.15}). Resolved by SERIALISING the trio (FIX 3): {130.12} ESTABLISHES the form-scoped key namespace under the existing procurement: object (forms, formTypes sub-keys); {130.13} and {130.15} EXTEND it (never rewrite). Deps: {130.13}→[“9”,“12”], {130.15}→[“9”,“11”,“13”].
    • The detail-surface back-nav stays folded into {130.13} (shares [id]/page.tsx); the list-page copy/nav (app/procurement/page.tsx) stays a separate Subtask {130.14} (disjoint file).
  • The win-rate parity test is the load-bearing acceptance gate, not a live snapshot (the 12 live workspaces carry NULL outcome → a live snapshot is vacuous; TECH §Risks). It rides with the win-rate rewrite Subtask as a synthetic-fixture integration test.
  • Sibling-only dependencies hold throughout — every dep below is a bare-digit sibling id. No cross-Task dep surfaced (ID-45 is a timing gate, not a Subtask dep; the deferred ID-61 items are subsumed into this Task, not cross-Task deps). No escalation required.

WAVE 1 — Migration spine (SERIAL; staging-first; ONE migration file PER Subtask — replayable + individually revertible)
{130.5} form_outcome_types CV + form_templates engagement cols + form_questions FK
+ procurement_workspaces rollup cols + q_a_pairs provenance + pqq→psq rename
(TECH steps 1–6 — all additive; its own sequential migration file)
{130.6} Roll-up recompute fn + trigger (TECH step 7; its own migration file) [dep 5]
{130.7} Win-rate rewrite — 42P13 DROP-dance + synthetic parity (TECH step 8; own migration file)
│ [dep 5,6]
{130.8} Data migration — mint 12 forms, re-key 30 Qs, recompute 12 rollups (TECH step 9; own migration file)
│ [dep 5,6,7]
WAVE 2 — Types/views regen (SERIAL; gates all code)
{130.9} Regen api.* views (generate-api-views --check) + regen database.types.ts
+ database-overrides.ts (form_templates shape + FormOutcomeSchema) (TECH steps 10–12)
[dep 5,6,7,8]
WAVE 3 — Code fan-out (mixed parallel/serial; ownership disjoint after collision fixes)
Sub-wave 3a (fully PARALLEL — disjoint files, all dep 9):
{130.10} Enum de-dup + /api/bids JSDoc in schemas.ts (OWNS lib/validation/schemas.ts) [dep 9]
{130.11} Umbrella route re-anchor — GET read-shape + PATCH + outcome (OWNS [id]/route.ts + outcome/route.ts) [dep 9]
{130.12} form_type picker — ESTABLISHES query-key namespace (OWNS components/procurement/ picker + query-keys.ts) [dep 9]
{130.14} List-page copy/nav + PJ-2..PJ-5 + Bid* (OWNS app/procurement/page.tsx + mcp-apps/) [dep 9]
{130.16} Deferred ID-61 wire-field cleanup (NO schemas.ts — FIX 1) [dep 9]
Sub-wave 3b (after 3a — query-keys.ts serialisation + read-shape consumption):
{130.13} Detail-surface UI form-list + roll-up READ render + back-nav (OWNS app/procurement/[id]/page.tsx; EXTENDS query-keys) [dep 9,12]
Sub-wave 3c (after 3b):
{130.15} Composer + Q&A promotion re-anchor (EXTENDS query-keys; promotion provenance) [dep 9,11,13]
WAVE 4 — Gate (Orchestrator, not a Subtask)
Full bun run test + generate-api-views --check + schema-parity; then prod migration; THEN ID-45 GO.

Critical path: 5 → 6 → 7 → 8 → 9 → 12 → 13 → 15 (with 11 feeding 15 in parallel to the 12→13 arm). The migration spine + types gate is irreducibly serial; within Wave 3 the query-keys.ts ownership chain (12 establishes → 13 extends → 15 extends) is the only serialised code arm.

Parallel-vs-serial rationale: Wave 1 is serial because it is one Postgres schema and the FK/CV/function ordering is a hard dependency (the outcome FK needs its CV target; the win-rate rewrite needs the columns it reads); each spine Subtask now authors its own sequentially-numbered migration file (FIX 5) so the staging push is replayable step-by-step and each step is individually revertible. Wave 3 is mixed, not fully parallel: after the three collision fixes, sub-wave 3a (10, 11, 12, 14, 16) is genuinely concurrent on disjoint files; 13 waits on 12 (it EXTENDS the query-keys.ts namespace 12 establishes, and consumes the read-shape 11 ships); 15 waits on 13 (further query-keys.ts extension) and on 11 (the form-scoped write contract). The serialisation is real shared-file ownership, not conservatism.


Subtask register (human mirror of the returned JSON)

Section titled “Subtask register (human mirror of the returned JSON)”

Proposed ids continue the existing series ({1}–{4} taken); the Orchestrator finalises ids at add-subtasks time.

{130.5} — Migration spine: CV + engagement columns + FK + rollup cols + provenance + pqq→psq

Section titled “{130.5} — Migration spine: CV + engagement columns + FK + rollup cols + provenance + pqq→psq”

TECH migration steps 1–6 (all additive / non-conflicting). OWNS its own sequentially-numbered migration file (supabase/migrations/<ts>_id130_spine.sql — NOT a shared <new>.sql; FIX 5 — replayable + individually revertible). Implements T-B2, T-B3, T-B4 (additive), T-B5 (CV table+seed), T-B12/T-B13 (pqq→psq — UPDATE targets the LIVE form_types row, not a migration seed), T-B21 (q_a_pairs provenance col), T-B23 (rollup cols), AD-1, AD-4. Deps: none (spine head). Boundary: DDL only — no fn/trigger ({130.6}), no win-rate rewrite ({130.7}), no data backfill ({130.8}).

{130.6} — Roll-up recompute function + trigger

Section titled “{130.6} — Roll-up recompute function + trigger”

TECH step 7. OWNS its own sequentially-numbered migration file (FIX 5). Idempotent recompute_procurement_rollup(p_workspace_id uuid) (INSERT … ON CONFLICT DO UPDATE, SET search_path, REVOKE EXECUTE … FROM anon) reading form_outcome_types for stage/denominator classification; AFTER trigger on form_templates engagement-column writes. Implements T-B7 (roll-up half), AD-2. Deps: [5].

{130.7} — Win-rate engine rewrite (42P13 DROP-dance) + synthetic parity test

Section titled “{130.7} — Win-rate engine rewrite (42P13 DROP-dance) + synthetic parity test”

RISKIEST. TECH step 8. OWNS its own sequentially-numbered migration file (FIX 5)

  • app/api/analytics/win-rate/route.ts (types). get_aggregate_win_rate_stats (return-shape change → DROP api.* wrapper → DROP public.* → CREATE → recreate wrapper); get_content_win_rate (body-only, plain CREATE OR REPLACE, also retires the bid_outcome alias). Both re-thread the CTE through form_responses → form_questions → form_templates, read ft.outcome, join form_outcome_types fot, denominator = fot.counts_toward_win_rate=true + terminal won/lost; add the separate shortlist pass-rate aggregate. Synthetic-fixture parity integration test is the load-bearing acceptance gate (NOT a live snapshot — vacuous against all-NULL live data). Implements T-B7 (win-rate half), T-B24 (bid_outcome alias). Deps: [5, 6].

{130.8} — Data migration: mint 12 forms, re-key 30 questions, recompute 12 rollups

Section titled “{130.8} — Data migration: mint 12 forms, re-key 30 questions, recompute 12 rollups”

TECH step 9. OWNS its own sequentially-numbered migration file (FIX 5). Per-workspace INSERT form_templates with the T-B22 transforms (issuing_organisation←buyer, deadline, submission_date, workflow_state←COALESCE(status,'draft'), withdrawnworkflow_state='withdrawn'+outcome=NULL, {won,lost}outcome+audit, default form_type='bid'); UPDATE form_questions.form_template_id (30 rows); recompute_procurement_rollup ×12. domain_metadata per-engagement keys left in place (reversibility). Implements T-B22, T-B1 (read-shape prep). Deps: [5, 6, 7]. Note: no valid win-rate output exists between {130.7} and the end of {130.8} (functions read empty ft.outcome) — defer win-rate validation to here.

{130.9} — Regenerate api.* views + types + overrides

Section titled “{130.9} — Regenerate api.* views + types + overrides”

TECH steps 10–12. bun scripts/generate-api-views.ts for form_templates, form_questions, procurement_workspaces, form_outcome_types; generate-api-views --check green; regen database.types.ts (--schema public,api, never hand-edit); update database-overrides.ts (new form_templates engagement shape + FormOutcomeSchema types + the ProcurementMetadata override). RLS verification (TECH step 12: delegate-to-workspaces EXISTS pattern; form_outcome_types public read-only CV; new fns REVOKE … FROM anon). Deps: [5,6,7,8]. Gates all of Wave 3.

{130.10} — Workflow enum single source of truth (de-dup) + /api/bids JSDoc in schemas.ts

Section titled “{130.10} — Workflow enum single source of truth (de-dup) + /api/bids JSDoc in schemas.ts”

Delete VALID_BID_STATUSES (schemas.ts:2007-2018); rewrite ProcurementMetadataSchema.status (1022-1033) and ProcurementListParamsSchema.status (2020-2021) to z.enum(PROCUREMENT_WORKFLOW_STATES) imported direct from @/types/procurement (no barrel). FIX 1 fold: also clears the stale /api/bids JSDoc site(s) in the SAME file (schemas.ts) — {130.16} no longer touches schemas.ts. Guard test: grep for a 2nd inline copy returns zero. Implements T-B11, AD-3, T-B24 (the schemas.ts JSDoc slice). OWNS lib/validation/schemas.ts. Deps: [9].

{130.11} — Umbrella route re-anchored to forms: GET read-shape + PATCH transition + outcome write

Section titled “{130.11} — Umbrella route re-anchored to forms: GET read-shape + PATCH transition + outcome write”

FIX 2: OWNS app/api/procurement/[id]/route.ts ENTIRELY (both the GET read-shape AND the PATCH transition) + app/api/procurement/[id]/outcome/route.ts. GET read-shape (T-B1 API half, ~lines 26–132): move from “read the single tender off domain_metadata” to “read the roll-up off procurement_workspaces + list child forms off form_templates”. PATCH + outcome write (T-B6/T-B8/T-B9/T-B10): target the form — write form_templates.workflow_state; on submitted set submission_date=now(); on terminal set {outcome, outcome_recorded_at, outcome_recorded_by} atomically, audit required-on-terminal; withdrawnworkflow_state not outcome; sb()/tryQuery() + post-UPDATE row-count check; fire roll-up recompute. Do NOT change canTransition’s signature. Dual-writer guard test. T-B1 read-shape is split: API GET here, UI consumption in {130.13}. Implements T-B1 (API half), T-B6, T-B8, T-B9, T-B10. Deps: [9].

{130.12} — form_type infer-then-confirm picker (establishes the query-key namespace)

Section titled “{130.12} — form_type infer-then-confirm picker (establishes the query-key namespace)”

Net-new picker in components/procurement/ (semantic tokens, WCAG label+selected-state, UK labels). Pre-selects inferred type (reuse pipeline classification); records the confirmed choice as form_templates.form_type; option list fetched at runtime from api.form_types (filtered 'procurement'=ANY(applicable_application_types)) via TanStack Query; confirm-first (no downstream keys on unconfirmed inference); no-document → no pre-selection. FIX 3: ESTABLISHES the form-scoped query-key namespace in lib/query/query-keys.ts — add concrete sub-keys under the existing procurement: object (line 328): forms, formTypes; {130.13}/{130.15} EXTEND this, never rewrite. OWNS the picker component + lib/query/query-keys.ts. Implements T-B14, T-B16, T-B12 (runtime fetch). Deps: [9].

{130.13} — Procurement detail surface UI: form-list + roll-up READ render + back-nav

Section titled “{130.13} — Procurement detail surface UI: form-list + roll-up READ render + back-nav”

FIX 2/3: OWNS ONLY app/procurement/[id]/page.tsx (+ its [id]/ page siblings) — the UI form-list level, the roll-up READ rendering, and the back-nav copy. Does NOT touch the API GET (that is {130.11}). Net-new form-list level (each form: form_type, workflow_state, deadline, outcome); selecting a form opens its composer ({130.15}); renders the roll-up off the {130.11} GET; single-form v1 case renders a one-item list. Back-nav “Back to Procurement” (T-B17 UI half). EXTENDS the {130.12} query-keys.ts namespace (form-list/detail read keys), never rewrites it. T-B1 read-shape: UI consumption here, API GET in {130.11}. Implements T-B1 (UI half), T-B15, T-B19, T-B17 (back-nav). Deps: [9, 12].

{130.14} — List-page copy/nav fold + PJ-2..PJ-5 + Bid* mcp-apps types

Section titled “{130.14} — List-page copy/nav fold + PJ-2..PJ-5 + Bid* mcp-apps types”

app/procurement/page.tsx — singular nav noun “Procurement” (PJ-1/T-B17 list-page half), PJ-2 rephrase (:155), keep “bid-writing” craft term (PJ-3), “Past responses” card (PJ-4), ProcurementListRow bid prop tidy; mcp-apps/{form-dashboard,reorient-me}/src/ Bid* types (PJ-5 — retain where form_type-scoped, rename where umbrella). UK English. Implements T-B17 (list-page), T-B18. OWNS app/procurement/page.tsx + mcp-apps/{form-dashboard,reorient-me}/src/. Deps: [9].

{130.15} — Composer + Q&A promotion re-anchored to the form

Section titled “{130.15} — Composer + Q&A promotion re-anchored to the form”

FIX 4 (concrete OWNS): the composer surface = components/procurement/question-review.tsx + question-list.tsx + question-navigator.tsx + question-row.tsx + response-editor.tsx + response-actions.tsx (the per-question composer cluster rendered from [id]/page.tsx); the promotion provenance write = app/api/q-a-pairs/promote/route.ts + lib/q-a-pairs/promote-corpus.ts (the two files that write source_workspace_id/source_form_response_id — they gain source_form_template_id); plus lib/query/query-keys.ts (EXTEND only). Composer scoped to form_template_id (T-B4); match candidates still corpus-level q_a_pairs (guardrail); query keys re-scope workspace→form (extending the {130.12} namespace); promotion records q_a_pairs.source_form_template_id lineage (corpus stays corpus-level); lost/withdrawn default skip-on-promotion. Implements T-B20, T-B21 (promotion half). Deps: [9, 11, 13].

{130.16} — Deferred ID-61 wire-field cleanup (subsumed)

Section titled “{130.16} — Deferred ID-61 wire-field cleanup (subsumed)”

FIX 1: OWNS the wire/token surfaces ONLY — NO lib/validation/schemas.ts (its /api/bids JSDoc moved to {130.10}). Clean the deferred ID-61 wire fields where they live: active_bids/bid_summary 4th wire contract (lib/activity/bid-summary.ts + dashboard/reorient/ attention + mcp apps); --bid-* design tokens bl-349 (app/styles/domain-tokens.css); stale /api/bids JSDoc in lib/queue/handlers/procurement-draft-all.ts; bl-370 persisted literals. (The bid_outcome alias is cleaned in {130.7}.) Implements T-B24 (wire-field half). Deps: [9]. Boundary: wire/token cleanup only — no behaviour change; if a rename has blast radius run gitnexus_rename not find-and-replace.


ID-45 timing gate (hard sequencing constraint)

Section titled “ID-45 timing gate (hard sequencing constraint)”

The entire migration spine ({130.5}–{130.8}) + types regen ({130.9}) must land on prod BEFORE ID-45 (first-client full-corpus re-ingest) GO (RESEARCH §8, confirmed against themes/canonical-pipeline/reference/v1-completion-sequence.md). The pipeline writes the form altitude; if ID-45 re-ingests before the form altitude exists, it cements the workspace-centric model by default — the most expensive thing to undo post-launch. The Wave-3 code Subtasks ({130.10}–{130.16}) are buyer-facing UI/IA and may trail the migration, but the schema is the gate. The Orchestrator holds this gate at Wave-4 (prod migration → ID-45 GO), NOT as a Subtask dep (ID-45 is a separate Task — a sibling-only-dep escalation is NOT triggered; it is a release sequencing constraint the Orchestrator enforces).

ID-61 deferred itemFolded intoMechanism
Unit F UI-copy sweep (UNIT-F-SCOPE-MAP){130.13} + {130.14}IA copy decisions (B-17/B-18), NOT a mechanical 62-site rename
active_bids/bid_summary 4th wire contract{130.16}wire-field cleanup where touched
bid_outcome alias in get_content_win_rate{130.7}retired in the win-rate body rewrite
stale /api/bids JSDoc{130.16}doc cleanup
--bid-* design tokens (bl-349){130.16}token cleanup
persisted-literal classes (bl-370){130.16}literal cleanup
5 PJ flags (PJ-1..PJ-5){130.13} (PJ-1 back-nav) + {130.14} (PJ-1 list-noun, PJ-2..PJ-5)IA/copy fold

Out-of-Task follow-ups (expand/contract — target PRE-GO; NOT in-Task Subtasks)

Section titled “Out-of-Task follow-ups (expand/contract — target PRE-GO; NOT in-Task Subtasks)”

These are tracked as out-of-Task expand/contract debt (TECH §Follow-ups), NOT decomposed into {130.x} Subtasks — surfaced here so the Orchestrator can route them to the Curator:

  • Drop deprecated workspaces.domain_metadata per-engagement keys (outcome, deadline, submission_date, status, outcome_recorded_*). Trigger: grep-zero readers across app/+lib/+ supabase/migrations/ (excl. the deprecation migration) AND win-rate parity test green for one release. Target: PRE-GO (must not survive into first-client re-ingest).
  • Drop form_questions.workspace_id once all 34 TS + 7 SQL consumers filter on form_template_id; re-scope UNIQUE(workspace_id, question_text)UNIQUE(form_template_id, question_text) at the same time.
  • Rename form_templates → form_instances (gitnexus_rename-driven, never find-and-replace — it is a misnamed INSTANCE table; the catalogue is form_template_requirements).
  • Remove unused application_types.state_machine_config column (dead infrastructure, AD-3).

Risks and mitigations (decomposition-level)

Section titled “Risks and mitigations (decomposition-level)”
RiskImpactMitigation
{130.7} 42P13 on RETURNS TABLE changeHighExplicit DROP-wrapper→DROP→CREATE→recreate ordering; staging-first
{130.7} win-rate parity unverifiable on live dataHighSynthetic-fixture parity test is the gate (live snapshot is vacuous)
{130.8} ↔ {130.7} transient no-valid-output windowMediumDefer all win-rate validation to after {130.8}; documented in both briefs
{130.13}/{130.14} shared app/procurement/ namespace collisionMediumSplit by exact file: [id]/page.tsx (13) vs page.tsx (14) — disjoint
form_questions.workspace_id retained alongside FKMediumAdditive-only this Task; per-form reads filter form_template_id; drop deferred
ID-45 re-ingest before schema landsHighWave-4 gate: prod migration precedes ID-45 GO (Orchestrator-enforced)

Ratification record (S418 — Liam via Lavish, 2026-06-25)

Section titled “Ratification record (S418 — Liam via Lavish, 2026-06-25)”
  • RATIFIED — proceed: Subtasks added, commit PLAN, begin the migration spine.
  • Priorities: NOT set (Liam). No must/should distinction — all Subtasks are worked through; there is time before the ID-45 re-ingest, and deferring “cleanup” Subtasks only creates confusion in a multi-agent dispatch config.
  • {130.14} Bid* rename: implementer decides per-symbol at build time (TECH T-B18 left this open; ratified to the Executor’s per-symbol judgment).
  • Wave-3 grouping: accept the post-fix sub-wave plan — 3a ({130.10}/{130.11}/{130.12}/{130.14}/{130.16}) parallel on disjoint files; {130.13} then {130.15} serial on the shared query-key file.

End of PLAN — RATIFIED (S418). On ratification, the Orchestrator adds {130.5}–{130.16} via bun scripts/ledger-cli.ts add-subtasks 130 --file - and dispatches Wave 1 (the serial migration spine) first.