Skip to content

ID-61 bid→procurement rename — Execution Strategy

⚠️ EXECUTED / HISTORICAL (bannered S462). All Units A–F are ledger-done — but Unit F’s VISIBLE-COPY half ({61.12} §1(a) + §(g)) did NOT land despite closure (S462 grep-verified; correcting journal on {61.12}). The workspace-umbrella framing is superseded by DR-038. Outstanding copy sweep is owned by ID-145.

ID-61 bid → procurement/form Rename — Execution Strategy

Section titled “ID-61 bid → procurement/form Rename — Execution Strategy”

Author: Senior Staff (strategy only — no code edits in this session) Repo state: worktree of canonical at origin/main (da009507) Inputs: ID-61.3 decision register (87 identifiers: 61→procurement, 7→form, 5 keep, 18 defer); ID-71 OQ-4 lockstep precedent; ID-64 DB-table rename (live on prod); the ratified two-axis rule (ID-71 {71.12}).


ID-71 already proved the correct shape for this codebase: a coordinated client-contract break lands in ONE PR per contract surface, forced into lockstep by a drift-guard test, aligned with the DDL cutover wave. We reuse that pattern, but ID-61 is broader than ID-71’s MCP-only surface — it spans app UI, TanStack cache, REST response shapes, a generated MCP bundle, DB RPC columns + api-views, and persisted CV/CHECK literals. These do not share a blast radius, so they do not share a PR. The strategy is therefore a sequence of independently-gated, internally-atomic PRs, ordered by data-coupling risk, NOT one mega-break.

The decision rule for “what must be atomic”: a value and every consumer that pattern-matches that exact value must change in the same commit. Pure-rename identifiers (type names, local vars, hook exports) are graph-tracked and can ride together safely; string-literal values (cache keys, runtime discriminants, DB columns, wire fields, persisted CV values) define the atomic boundaries.


Verdict: a sequence of gated PRs, NOT one atomic break. Rationale: ID-71’s single-PR break worked because its whole surface was one contract (the MCP tool surface) guarded by one drift-guard (mcp-fixture-sync.test.ts). ID-61’s surfaces have disjoint guards (typecheck, schema-parity, supabase-types-parity, api-view-coverage, e2e-smoke, jscpd). Forcing them into one PR maximises merge-conflict surface and couples a low-risk pure-rename (61 identifiers, typecheck-guarded) to a high-risk DB migration (prod DDL). Splitting lets each PR close on its own forcing-function gate.

The atomic units (each = one PR, must not be split mid-unit)

Section titled “The atomic units (each = one PR, must not be split mid-unit)”
UnitAtomic contents (all-or-nothing in one commit)Closing gate
A — Pure-rename, no value coupling61 procurement-axis + 7 form-axis pure identifiers (types, interfaces, hook exports, local vars, props, JSDoc). Graph-tracked via gitnexus_rename; verified by ast-dataflow rename-sweep.tsc --noEmit (typecheck job) + bun run test
B — TanStack cache keysqueryKeys.bidsqueryKeys.procurement namespace + the 6 'bids' string tuples (query-keys.ts:328-337) + ~24 accessor sites + the 4 use-draft-stream.test.ts lockstep assertions (1247/1250/1321/1324).bun run test (the 4 assertions ARE the forcing function — they fail loudly on desync)
C — bid_deadline runtime discriminant + generated bundleunion literal (attention.ts:35, reorient.ts:33) + 3 producers (attention.ts:278, reorient.ts:298/311) + icon-map key (unified-attention-section.tsx:85) + zod enum (reorient/route.ts:14) + reorient-me SOURCE (mcp-apps/reorient-me/src/{types.ts,app.ts}) + bun run build:mcp-apps regen of app-bundles.ts + snapshot assertions in __tests__/lib/attention.test.ts.bun run test + mcp-build job (bundle freshness)
D — REST response shape (bids: + failed_bid_ids)producer (procurement/route.ts) + Zod schema + ALL 4 in-repo consumers of bids: (app/procurement/page.tsx, formatters/apps.ts, form-dashboard source, contract test) + failed_bid_idsfailed_procurement_ids (producer + tests only — zero consumers).bun run test + mcp-build (form-dashboard regen)
E — DB co-migration (unique_bids + DB residue)new migration: rename unique_bidsunique_procurements in public + api get_aggregate_win_rate_stats RETURNS TABLE + CTE aliases; bid_response_auto_version()form_response_auto_version() (fn+trigger+grants); get_form_summary param→workspace_id; bid_outcome alias→outcome. Then generate-api-views.ts regen + supabase gen types regen of database.types.ts + TS readers (win-rate/route.ts, content-performance-section.tsx) + tests.db push (foreground) + supabase-types-parity + api-view-coverage --check + schema-parity
F — UI copy + DOM-id/e2e selectors (owner put copy in scope)visible “Bids”/“bid” copy strings (page h1, “Manage bid submissions…”, “No bids match…”, “Active Bids”, etc.) + data-testid="bid-card-${id}" producer (procurement-list-card.tsx:87) + DOM ids (procurement/[id]/page.tsx tabpanel ids) + e2e selector file e2e/helpers/selectors.ts bid: block + e2e copy assertions (bid-pipeline.spec.ts, dashboard.spec.ts, workspaces.spec.ts).e2e-smoke (staging) — copy + selector lockstep
A (pure rename) ──┐
├──► independent, can land first / parallel
B (cache keys) ───┤
C (discriminant) ─┤
D (REST shape) ───┘
E (DB migration) ── independent (DB-led), but land AFTER A
so renamed TS readers exist to consume regen'd types
F (UI copy + e2e) ── land LAST (after A, since it touches the same files A renames;
and after the app is otherwise stable so e2e-smoke is a clean gate)

A, B, C, D have no inter-dependency (disjoint files mostly; A is the only one that overlaps F). E is DB-led and orthogonal. F is last because (i) it shares files with A (app/procurement/page.tsx) so sequencing avoids conflicts, and (ii) e2e-smoke is the slowest, staging-bound gate — run it once on the final shape.

ID-71 precedent applied: ID-71’s “six lanes in one PR forced by mcp-fixture-sync.test.ts” maps to our Unit-by-Unit forcing functions. We inherit its discipline (regen generated artefacts from source, never hand-edit; one cutover wave per contract) but split by guard boundary rather than cramming all into one PR, because our guards are disjoint where ID-71’s were unified.


2. Per-coupled-item handling (the 6 + DB residue + persisted CV/CHECK)

Section titled “2. Per-coupled-item handling (the 6 + DB residue + persisted CV/CHECK)”

Item 1 — unique_bidsunique_procurements (DB RPC column)

Section titled “Item 1 — unique_bids → unique_procurements (DB RPC column)”

CORRECT HANDLING: DB co-migration + regen, in Unit E. NOT a TS-only rename. It is a RETURNS TABLE column on get_aggregate_win_rate_stats (public and api schema; squash baseline 20260617130000 L456/2018 + CTE aliases L2057/2079; and the generated id115 api-views migration L1539). TS reads it positionally as .unique_bids (win-rate/route.ts:90/104/113). Renaming the TS field alone = runtime undefined. Atomic unit: (a) CREATE OR REPLACE both RPCs with the renamed column + CTE aliases in a new migration; (b) re-run generate-api-views.ts to regenerate the api-views migration (guarded by generate-api-views.ts --check + check-api-view-coverage.ts); (c) supabase gen types --schema public,api to regenerate database.types.ts (guarded by supabase-types-parity); (d) rename the TS readers + the unique_bids TS-only sites (content-performance-section.tsx) + tests. schema-parity CI compares public/extensions/auth/storage (NOT api), so the public RPC change must reach prod+staging together; the api layer is generator-guarded. This is a CREATE OR REPLACE (return shape positional) — safe, no DROP needed for the public/api functions (the generator uses DROP+CREATE for the api wrapper, which is fine).

Item 2 — bid_deadlineprocurement_deadline (runtime discriminant + generated bundle)

Section titled “Item 2 — bid_deadline → procurement_deadline (runtime discriminant + generated bundle)”

CORRECT HANDLING: atomic lockstep across union/producers/zod/consumer/SOURCE+regen, in Unit C. This is the highest-coupling pure-string item. The value flows: union literal (attention.ts:35, reorient.ts:33) → produced (type:'bid_deadline' at attention.ts:278, reorient.ts:298/311) → validated (zod enum reorient/route.ts:14) → consumed as r.type==="bid_deadline" discriminant in two places: the React icon-map (unified-attention-section.tsx:85) AND the generated app-bundles.ts (L343, L349). The generated bundle’s source is mcp-apps/reorient-me/src/app.ts:279/306 + types.ts:22edit the SOURCE, then bun run build:mcp-apps; never hand-edit the minified bundle. Renaming the union literal alone silently breaks: icon falls back to Info, and the bundle’s ==="bid_deadline" branches never fire (the “Help me draft” / “Show bid detail” buttons vanish). All of it changes in one commit; snapshot assertions in __tests__/lib/attention.test.ts (the attention-bid-${id} id prefix) update in lockstep. Note: the attention-bid-${id} synthetic id prefix (attention.ts:277) is register-mapped to attention-procurement-${id} — fold into Unit C with the snapshot update.

Item 3 — failed_bid_ids / failedBidIdsfailed_procurement_ids (public API field)

Section titled “Item 3 — failed_bid_ids / failedBidIds → failed_procurement_ids (public API field)”

CORRECT HANDLING: CLEAN BREAK, rename producer + tests atomically (Unit D). No deprecation, no dual-emit. Investigation verdict: zero consumers, in-repo or external. The field is emitted by GET /api/procurement (procurement/route.ts:51 schema, 170/177-178 emit) only when the stats-RPC fallback fails (H13 “absent when empty”). No UI, hook, MCP tool, plugin, contract-test, or OpenAPI schema reads it — only the producer route and __tests__/api/procurement.test.ts. The MCP layer queries Supabase directly, not this REST endpoint. A dual-emit/deprecation window would be pure ceremony for a field nobody reads. Rename producer + Zod + the 4 test assertions in one commit; tests fail loudly on a miss.

Item 4 — source_bid_id / source_bid_namesource_procurement_* (write-only JSONB)

Section titled “Item 4 — source_bid_id / source_bid_name → source_procurement_* (write-only JSONB)”

CORRECT HANDLING: LEAVE-WITH-RATIONALE (do not rename this session); if renamed later, write-side-only — NO backfill. These are written into content_items.metadata (JSONB) at outcome/integrate/route.ts:247-248, alongside source_question_id/integrated_at. Zero readers across TS/Python/SQL. Because nothing reads them, renaming the write keys creates harmless historical key-drift (old rows source_bid_id, new rows source_procurement_id) with no functional consequence — but also delivers zero value since no consumer benefits. Recommendation: leave as-is with a one-line code comment documenting the legacy key (cheapest correct option). If a future feature reads these keys, that feature’s ticket owns the rename + (only if it must query historical rows) a jsonb_set backfill. Do not spend a migration on an unread audit tag now.

Item 5 — queryKeys.bids + 'bids' tuples + 4 draft-stream assertions (TanStack cache)

Section titled “Item 5 — queryKeys.bids + 'bids' tuples + 4 draft-stream assertions (TanStack cache)”

CORRECT HANDLING: atomic in-memory rename, Unit B. The 4 test assertions ARE the forcing function. Pure in-memory cache keys — no persistence, no bookmarkable contract, no DB. But the value 'bids' is pattern-matched: query-keys.ts:329-337 defines the tuples, ~24 accessor sites build keys, and use-draft-stream.test.ts:1247/1250/1321/1324 assert queryKey: ['bids','questions','bid-1'] by literal. A half-rename desyncs the writer’s invalidation key from the reader’s cache key → silent stale UI (invalidate fires on ['procurement',…], cache still keyed on ['bids',…]). Use gitnexus_rename for the queryKeys.bids symbol (graph-tracked), then ast-dataflow string-literal-uses to sweep the 'bids' literal tuples, then update the 4 assertions — all one commit. The assertions deliberately encode the contract; keep them as literal-string assertions (do not soften to expect.anything()), they are the desync alarm.

Item 6 — Persisted CV values + DB CHECK literals (the DEFER list)

Section titled “Item 6 — Persisted CV values + DB CHECK literals (the DEFER list)”

CORRECT HANDLING: stay DEFERRED this session; each is a value-coupled own-ticket. Per-item disposition below. These carry data/URL/persisted-state coupling that a rename would silently break without a migration — exactly the class ID-61.3 correctly deferred. Decisions for the future tickets:

LiteralPersistence surfaceFuture correct handling
bid_writing (primary_focus persona)Supabase Auth user_metadata.primary_focus (written profile-section.tsx:133)Legacy-value-with-display-label (label already “Procurement writing”). Renaming the value needs an Auth-metadata backfill across all users — high cost, low value. Keep value, display label is already correct. Only backfill if a hard product need arises.
bid_management (feature toggle)Verifier-overturned → rename-procurement (file-based static toggle, NOT persisted)Goes into Unit A as procurement_management — it is NOT a persisted CV. (Register §1 row.)
bid_discovered (lifecycle)content_items.lifecycle_type (varchar, no CHECK in current DDL) + compared in TS (freshness.ts)Legacy-value-with-label or backfill. Plain varchar → no DB constraint break, but TS value-compares against it. Own ticket: backfill UPDATE … SET lifecycle_type='procurement_discovered' + rename TS literals atomically, OR keep value + label. Backfill preferred (small table, value-coupled reads).
prior_bid_response (extractor_kind)q_a_extractions.extractor_kind CHECK constraint (squash L949) — strict enum, zero TS refsBackfill migration mandatory if ever renamed (CHECK rejects mismatched rows). Lowest priority — no TS coupling, purely a DB-cosmetic. Likely “leave-with-rationale” indefinitely.
bid_detail/LAYER_BID_DETAIL, bid_librarylayer_vocabulary CV (seed.sql) + ingestion_source; TS constant mirror (layer-inference.ts)Backfill migration (CV seed row rename + content_items data backfill + TS constant) — own ticket; label already “Procurement Detail”. Dual-read during migration window.
from_bid (URL param) + fromBidId/fromBidWorkspace/fromBidBookmarkable URL query keyOwn ticket with redirect shim. Renaming breaks live bookmarks; needs a back-compat read (searchParams.get('from_procurement') ?? searchParams.get('from_bid')) for a deprecation window. e2e bid-session.spec.ts asserts the param.
--bid-* design tokens + bg/text/border-bid-*CSS @theme inline + Tailwind utility classesOwned by bl-349. Rename requires CSS def + @theme RHS + grep-sweep of utility classnames in TSX + the app-bundles.ts token (regen). Self-contained ticket.
kb-bid-view (localStorage)Browser localStorage key (use-view-mode.ts)Own ticket with migration read (localStorage.getItem('kb-procurement-view') ?? getItem('kb-bid-view')) or accept a one-time silent view-preference reset. Low stakes.
legacy workspaces.type==='bid' aliasDB still emits the wire alias (args.type==='bid'?'procurement')Own ticket gated on a DB data migration retiring the alias. Until the DB stops emitting 'bid', the TS comparison is load-bearing — KEEP.
  • bid_response_auto_version()form_response_auto_version(): CREATE new fn + repoint trigger form_response_set_version + mirror GRANT/REVOKE + DROP old. Sole version-setter on form_responses; zero TS callers (trigger fns are internal). Safe.
  • get_form_summary(bid_workspace_id)workspace_id param: CREATE OR REPLACE with renamed param + body (6 sites) + grants. No TS named-arg callers (zero rpc('get_form_summary', {bid_workspace_id}) in TS) → PostgREST positional calls unaffected. Safe.
  • bid_outcome internal alias → outcome: cosmetic CTE alias, NOT in RETURNS TABLE. Body-only edit. Safe.

Per-edit-class tool selection:

  • Graph-tracked symbols (type/interface/hook/fn/var names — Unit A, the queryKeys.bids symbol): gitnexus_rename (understands the call graph; never find-and-replace). Run gitnexus_impact({target, direction:'upstream'}) before each high-blast-radius symbol (ActiveBidSummary ~26 importers, invalidateBidData cross-hook contract) and report HIGH/CRITICAL before proceeding.
  • String-literal values (cache-key tuples, bid_deadline, unique_bids TS sites, CV literals): gitnexus_rename does NOT cover these → ast-dataflow string-literal-uses to enumerate exact sites, then targeted edits. Follow with the ast-dataflow rename-sweep skill (Q1 string-literal sites, Q2 import-path sweep, Q3 new-symbol references) to produce a categorised VERDICT that the rename is complete.
  • SQL + Python: ast-dataflow is TS-only → grep sweep of supabase/migrations/*.sql and scripts/**/*.py for every value being renamed (already done for unique_bids, bid_response, bid_deadline).
  • Generated artefacts (app-bundles.ts, api-views migration, database.types.ts, plugin-bundle.ts): regenerate from source, never hand-edit. bun run build:mcp-apps (bundle), generate-api-views.ts (api migration), supabase gen types (types). app-bundles.ts and plugin-bundle.ts are sandbox read-denied — confirms they are generated, not source.
  • gitnexus_detect_changes({scope:'compare', base_ref:'main'}) before each commit to confirm the diff touches only expected symbols/flows.

Lockstep test updates (the forcing functions):

  • Unit B: the 4 use-draft-stream.test.ts literal-key assertions.
  • Unit C: __tests__/lib/attention.test.ts id-prefix snapshots.
  • Unit D: __tests__/api/procurement.test.ts failed_bid_ids assertions + the bids: contract test (mcp-app-contracts.test.ts).
  • Unit E: __tests__/api/analytics-win-rate.test.ts + content-performance-section.test.tsx unique_bidsunique_procurements.
  • Unit F: e2e copy assertions + e2e/helpers/selectors.ts bid: block.

Migration discipline (Unit E — from supabase/CLAUDE.md): supabase migration new + foreground supabase db push (interactive, hangs background shells); verify supabase/.temp/project-ref before push (main repo can silently target prod); all new fns SET search_path = public, extensions; explicit REVOKE … FROM anon on new fns; regen types --schema public,api (deterministic order). Never use MCP apply_migration/execute_sql for DDL.

CI gates that close each unit: quality-test (typecheck tsc -p tsconfig.ci.json + bun run test) for A/B/C/D; mcp-build for bundle freshness (C/D); supabase-types-parity + type-drift-parity + api-view-coverage + schema-parity for E; e2e-smoke (staging) for F; dupe-check (jscpd) runs over all. Draft PRs skip CI — keep PRs non-draft once the unit is internally complete.


4. Risk register (silent-failure modes + guard per phase)

Section titled “4. Risk register (silent-failure modes + guard per phase)”
RiskFailure modeGuarded by
Cache-key desync (Unit B)Invalidate writes ['procurement',…]; reads still keyed ['bids',…] → stale UI, no errorThe 4 literal-key use-draft-stream.test.ts assertions fail on any half-rename. Keep them literal.
Runtime discriminant mismatch (Unit C)Union renamed but emitted value or ==="bid_deadline" consumer not → icon falls to Info, bundle buttons vanish, no throwbun run test (icon-map + snapshot) + zod enum rejects bad value at API boundary + mcp-build bundle freshness. Edit reorient-me SOURCE + regen.
Generated-artifact staleness (C/D/E)Hand-edit minified bundle / forget regen → source says procurement, shipped bundle says bidmcp-build job rebuilds + diffs app-bundles.ts; generate-api-views.ts --check + supabase-types-parity diff generated vs committed. Never hand-edit.
API contract break (Unit D)Rename bids: wire field, miss a consumer → data.bids undefinedZero external consumers (verified); 4 in-repo consumers + contract test all in one commit; mcp-app-contracts.test.ts asserts shape.
DB column runtime break (Unit E)Rename TS .unique_bids without SQL, or SQL without type regen → positional read returns undefined / type mismatchsupabase-types-parity (committed types must match live schema) + api-view-coverage + foreground db push to prod+staging together. schema-parity catches public-schema drift between envs.
Historical-data key drift (Item 4, deferred CVs)Rename persisted value/JSONB key without backfill → old rows orphaned from new readsDEFERRED — each own ticket carries the backfill (or leave-with-rationale where no reader). NOT touched this session.
URL/bookmark break (from_bid, deferred)Rename param → live bookmarks 404 the filter contextDEFERRED — own ticket with back-compat ?? read shim + e2e update.
e2e copy/selector drift (Unit F)Rename UI copy/DOM-id, miss the e2e assertion → red smoke on staginge2e-smoke is the forcing function; selector file + copy assertions land with the DOM-id producer.

5. Explicit recommendation — concrete phased plan

Section titled “5. Explicit recommendation — concrete phased plan”

Phase 1 (THIS session) — Unit A: pure-rename, zero value-coupling. Scope: the 61 procurement-axis + 7 form-axis pure identifiers (incl. the verifier-overturned bid_managementprocurement_management and 'bid' ClaudePrompt.category) via gitnexus_rename + ast-dataflow rename-sweep. Explicitly EXCLUDES every string-value boundary (cache keys, bid_deadline, unique_bids, REST fields, DB, UI copy) and the entire DEFER list. Gate: tsc --noEmit green + bun run test green + gitnexus_detect_changes shows only expected symbols + ast-dataflow rename-sweep VERDICT = complete. Why first: largest, lowest-risk slice; typecheck-guarded; unblocks E (renamed readers) and reduces F’s conflict surface.

Phase 2 — Unit B: TanStack cache-key lockstep. Scope: queryKeys.bidsprocurement namespace + 'bids' tuples + ~24 accessors + 4 use-draft-stream.test.ts assertions, one commit. Gate: bun run test (the 4 assertions).

Phase 3 — Unit C: bid_deadline discriminant + reorient-me source regen. Scope: union + producers + zod + icon-map + attention-bid- id prefix + reorient-me SOURCE edit + bun run build:mcp-apps + attention snapshot. Gate: bun run test + mcp-build.

Phase 4 — Unit D: REST response shape. Scope: bids:→ renamed wire field + 4 in-repo consumers + form-dashboard source regen; failed_bid_idsfailed_procurement_ids (clean break, producer+tests). Gate: bun run test + mcp-build + mcp-app-contracts.test.ts.

Phase 5 — Unit E: DB co-migration + regen. Scope: one migration renaming unique_bids (public+api RPC + CTE), bid_response_auto_version, get_form_summary param, bid_outcome alias; then generate-api-views + supabase gen types regen + TS readers + tests. Gate: foreground db push (prod+staging) + supabase-types-parity + api-view-coverage --check + schema-parity + bun run test.

Phase 6 — Unit F: UI copy + DOM-id/e2e (owner put copy in scope). Scope: visible “Bids”/“bid” copy → “Procurement”; data-testid="bid-card-${id}" producer + DOM tab ids; e2e/helpers/selectors.ts bid: block; e2e copy assertions. Gate: e2e-smoke (staging) green.

Deferred to future own-tickets (NOT this migration): the entire ID-61.3 DEFER list — from_bid+derived (redirect-shim ticket), --bid-* tokens (bl-349), bid_writing/bid_library/bid_detail persona+layer CVs (backfill tickets), bid_discovered/prior_bid_response CHECK/lifecycle literals (backfill-or-leave), kb-bid-view localStorage, legacy workspaces.type==='bid' wire-alias (DB-data-migration-gated), source_bid_* JSONB (leave-with-rationale). Each is value-coupled to persisted data/URL/localStorage and must carry its own backfill or back-compat shim.

KEEP untouched (ratified): VALID_BID_STATUSES/TERMINAL_BID_STATUSES (const+values), the bid_response reorient union tag (types/reorient.ts + its emit/validator network incl. reorient-me source — decoupled from the DB), bid as a form_types CV value, the _s224..._bid_draft_all migration-filename JSDoc.


Unit E (the DB column rename) shipping its SQL and its TS/type-regen out of lockstep across the prod+staging cutover. unique_bids is read positionally as .unique_bids in live runtime code AND lives in both public and api schemas with disjoint guards (schema-parity covers public, api-view-coverage+generator covers api, supabase-types-parity covers the committed types). If the migration reaches prod but database.types.ts/the api-views migration/the TS reader don’t land together — or staging and prod diverge — the win-rate endpoint silently returns unique_procurements: 0 (positional ?? 0 swallows the undefined) with no thrown error. Mitigation: treat Phase 5 as a single atomic PR that includes migration + both regens + TS readers + tests, push DDL foreground to prod AND staging in the same cutover, and rely on all four parity gates before merge. This is the one phase where a partial landing is both silent and prod-facing.