TECH.md — ID-21: TanStack workspace-types migration (Path c)
TECH.md — ID-21: TanStack workspace-types migration (Path c)
Section titled “TECH.md — ID-21: TanStack workspace-types migration (Path c)”Status: ratified-ready · awaiting Subtask PLAN.md breakdown Audience: Knowledge Hub engineering (Liam + future Executor agent) Scope: Refactor only — no behavioural change. No PRODUCT.md required (Liam ratification S251).
Source-of-truth pointers
Section titled “Source-of-truth pointers”| Reference | Locator |
|---|---|
| Backlog item ID-21 (full notes + Path c) | docs/reference/product-backlog.json (search "id": "ID-21") |
| S248 T4 procurement umbrella rename closeout | 99dfc5fd 51b6cc8c f9261904 (the trio that landed the rename + workspace-types comment) |
| S249 GitNexus impact investigation | 95b660ef (refactor(s249-id-23) — same commit notes the 132-symbol HIGH-risk finding) |
| T2 application_types migration | supabase/migrations/20260520120828_t2_combined_pr_intel_shape_b_form_type_split.sql §1.1-1.2 |
| Generated DB types (source of truth) | supabase/types/database.types.ts (application_types row at L17) |
| Static registry being retired | lib/workspace-types.ts |
| Sync constraint site (preserved) | lib/validation/schemas.ts:535 |
| Reference TanStack pattern | hooks/intelligence/use-company-profiles.ts + app/api/intelligence/profiles/route.ts |
| CLAUDE.md gotchas referenced | anon-EXECUTE pattern (S250 W1b), SET search_path = public, extensions, sandbox CLI rule |
§1 Scope
Section titled “§1 Scope”In scope
Section titled “In scope”- Author
hooks/workspaces/use-application-types.tsexporting auseApplicationTypes()TanStack Query hook against theapplication_typestable. - Author a server-side fetcher route or extend an existing one so the hook has a typed JSON contract to consume (sibling-only — does not depend on schema mutability work).
- Migrate the 3 UI consumer files to the hook:
app/workspaces/workspaces-content.tsxcomponents/workspace/workspace-card.tsxcomponents/workspace/workspace-create-dialog.tsx
- Delete the static registry surface in
lib/workspace-types.ts:WORKSPACE_TYPE_REGISTRY,registerType(), the 3 inlineregisterType({…})call blocks,getWorkspaceType(),getAllWorkspaceTypes(),getLauncherTypes(),formatTypeCount(), and theWorkspaceTypeConfiginterface (moved into the hook module per §4). - Resolve the metadata-location open question per §4 (recommendation: Option C).
- Align
__tests__/lib/workspace-types.test.tsto the new shape and add__tests__/hooks/use-application-types.test.ts.
Out of scope
Section titled “Out of scope”APPLICATION_TYPE_KEYSsync tuple (lib/workspace-types.ts:166-173) — kept exactly as-is. This is the sync constraint and is the whole point of Path c.getValidTypeValues()— kept exactly as-is. Sync-callable, returns the hardcoded tuple.lib/validation/schemas.ts:535— kept exactly as-is. Continues constructingz.enum(getValidTypeValues())at module load.- Admin UI for mutating
application_typesrows — deferred per Q-OQR1-13 to v1.1. This refactor reads from the table; it does not gate it behind an admin editor. - Adding new application types to the seed — none of the 6 existing seed rows change. Future type additions are a separate change (sync tuple + application_types INSERT in lockstep, same constraint as today).
- Dead-code at
app/api/workspaces/route.ts:107legacy'bid' → 'procurement'ternary — already removed S248 T4 (Liam ratification S251 verified).
§2 Background
Section titled “§2 Background”lib/workspace-types.ts predates the T2 application_types table (S246
migration). It hardcodes UI metadata (label, icon, route, default colour,
features) for 3 types in a client-side WORKSPACE_TYPE_REGISTRY populated
via inline registerType({…}) calls at module-init time.
T2 introduced the canonical 6-row application_types table. T4 (S248) and
ID-23 (S249) completed the bid → procurement rename and updated the static
registry comment to note that a full TanStack Query migration of UI helpers
is a backlog item rather than a T4 blocker.
A naive “migrate everything to TanStack” framing failed S249 impact analysis:
gitnexus_impact on getValidTypeValues returned HIGH risk + 132 impacted
symbols, because the value of getValidTypeValues() is consumed
synchronously at module-load time by Zod schema construction
(lib/validation/schemas.ts:535). TanStack useQuery is async; you cannot
build a Zod enum from useQuery() data.
Path c — decouple sync from metadata — preserves the sync seam.
Two distinct surfaces, two distinct contracts:
| Surface | Contract | Mechanism | Path c posture |
|---|---|---|---|
| Identity (the closed set of valid keys) | Sync, module-load-time | APPLICATION_TYPE_KEYS hardcoded tuple | Kept as-is |
| Metadata (label, icon, route, colour) | Async, render-time, may need data | useApplicationTypes() TanStack Query hook (new) | Migrated |
The constraint that previously coupled them — both surfaces hanging off a
single static WORKSPACE_TYPE_REGISTRY — is what made the naive framing
non-viable. Path c keeps the tuple and migrates only the registry side.
§3 Proposed changes
Section titled “§3 Proposed changes”Each P-N maps to a verifiable acceptance criterion in §5 testing. Numbered to match the dispatch brief.
P-1 — useApplicationTypes() hook signature + cache key + fetcher contract
Section titled “P-1 — useApplicationTypes() hook signature + cache key + fetcher contract”New file: hooks/workspaces/use-application-types.ts
import { useQuery } from '@tanstack/react-query';import { queryKeys } from '@/lib/query/query-keys';import { fetchJson } from '@/lib/query/fetchers';import type { LucideIcon } from 'lucide-react';
/** Shape returned by GET /api/application-types. Mirrors WorkspaceTypeConfig * pre-refactor with one delta: `icon` arrives as a string name (DB column) * and is resolved to a LucideIcon at hook layer per §4 Option C. */export interface ApplicationTypeRow { readonly key: string; // = workspaces.application_types.key readonly label: string; readonly labelPlural: string; readonly description: string; readonly defaultIcon: string; // DB column application_types.default_icon readonly defaultColour: string; // DB column application_types.default_colour}
export interface WorkspaceTypeConfig extends ApplicationTypeRow { /** Resolved LucideIcon (looked up from defaultIcon via a static * icon-name → LucideIcon map maintained in the hook module). */ readonly icon: LucideIcon; /** Routing target — static client config per §4 Option C. */ readonly route: string | null; /** Whether the type is renderable today — static client config per §4. */ readonly available: boolean; /** Whether the type has a custom creation flow — static client config. */ readonly hasCustomCreation: boolean; /** Feature flags — static client config per §4. */ readonly features: { readonly hasStatus: boolean; readonly hasContentAssignment: boolean; readonly hasDomainMetadata: boolean; };}
export function useApplicationTypes() { return useQuery({ queryKey: queryKeys.applicationTypes.list, queryFn: () => fetchJson<ApplicationTypeRow[]>('/api/application-types'), select: (rows) => rows.map(toWorkspaceTypeConfig), // join static config + Lucide icon staleTime: 5 * 60_000, // 5 min — closed-list reference data });}New API route: app/api/application-types/route.ts — GET only.
Authenticated (any role). Reads application_types via getAuthenticatedClient()
with the order-by label. Returns ApplicationTypeRow[] JSON.
Cache key: new applicationTypes namespace in lib/query/query-keys.ts:
applicationTypes: { all: ['application-types'] as const, list: ['application-types', 'list'] as const,},P-2 — Hook return shape (strongly typed)
Section titled “P-2 — Hook return shape (strongly typed)”The hook returns UseQueryResult<WorkspaceTypeConfig[]>. Three sync helpers
replace the three deleted static functions; each is a pure-function
selector over the array (no new state, no new lookup primitives):
| Deleted static function | Replacement (in use-application-types.ts) |
|---|---|
getWorkspaceType(t) | useWorkspaceType(type: string) — wraps useApplicationTypes(), .find(c => c.key === type) |
getLauncherTypes() | useLauncherTypes() — wraps useApplicationTypes(), filters `route !== null |
formatTypeCount(t, n) | formatTypeCount(config: WorkspaceTypeConfig | undefined, n: number) — sync utility, no hook |
getAllWorkspaceTypes() | useApplicationTypes() itself — data is already the full list |
getWorkspaceType and formatTypeCount are called from non-hook contexts
in the current code (the dialog useEffect, the card render path) — the
hook-based replacement keeps these in render contexts only; the
formatTypeCount utility accepts a WorkspaceTypeConfig | undefined so the
caller’s useQuery result feeds it directly.
P-3 — UI consumer migration patterns
Section titled “P-3 — UI consumer migration patterns”The 3 consumer files migrate as follows:
3a. app/workspaces/workspaces-content.tsx
Section titled “3a. app/workspaces/workspaces-content.tsx”Today (L4, L11, L28):
import { getLauncherTypes, formatTypeCount } from '@/lib/workspace-types';// …const launcherTypes = getLauncherTypes();// …const countText = formatTypeCount(wt.type, count);Target:
import { useLauncherTypes, formatTypeCount } from '@/hooks/workspaces/use-application-types';// …const { data: launcherTypes = [], isLoading } = useLauncherTypes();// In the render path, accept the loading state (render the existing skeleton/empty grid).// …const countText = formatTypeCount(wt, count); // pass the config directlyLoading-state handling: the current page is a server component
(app/workspaces/page.tsx) that fetches counts on the server and hands them
to the 'use client' WorkspacesContent. The hook adds a render-time async
fetch (max 1 round-trip on first paint, served from cache afterwards). On
first paint, render an empty <div> (same layout container) until the hook
resolves. Acceptance criterion gates this in §5 (AC-3a).
3b. components/workspace/workspace-card.tsx
Section titled “3b. components/workspace/workspace-card.tsx”Today (L18, L46):
import { getWorkspaceType } from '@/lib/workspace-types';// …const typeConfig = workspace.type ? getWorkspaceType(workspace.type) : null;Target:
import { useWorkspaceType } from '@/hooks/workspaces/use-application-types';// …const { data: typeConfig = null } = useWorkspaceType(workspace.type ?? '');useWorkspaceType returns WorkspaceTypeConfig | undefined; the existing
null-coalesce + typeConfig && JSX guard handles the loading frame the same
way it handles the “joined row absent” case today.
3c. components/workspace/workspace-create-dialog.tsx
Section titled “3c. components/workspace/workspace-create-dialog.tsx”Today (L19, L50, L54):
import { getWorkspaceType } from '@/lib/workspace-types';// …const typeConfig = getWorkspaceType(type);// …if (open && typeConfig?.hasCustomCreation) { … }Target:
import { useWorkspaceType } from '@/hooks/workspaces/use-application-types';// …const { data: typeConfig } = useWorkspaceType(type);// …// useEffect deps unchanged — typeConfig identity will change when data// resolves, which already triggers the effect.if (open && typeConfig?.hasCustomCreation) { … }Hook caveat: the dialog opens via a controlled open prop. On first
open it may render once with typeConfig === undefined before the hook
resolves. The dialog’s existing dialog-shell renders fine in this frame
(the typeConfig?.label ?? 'Workspace' fallback at L126 covers it).
P-4 — Static registry deletion
Section titled “P-4 — Static registry deletion”After the 3 consumer migrations land, delete the following from
lib/workspace-types.ts:
| Symbol | Lines (current) | Deletion gate |
|---|---|---|
WorkspaceTypeConfig interface | 18-61 | Moved into use-application-types.ts |
WORKSPACE_TYPE_REGISTRY const | 64 | Zero remaining importers |
registerType() | 67-73 | Zero remaining callers (all 3 inline) |
Inline registerType({…}) ×3 | 81-136 | Zero remaining callers |
getWorkspaceType() | 141-145 | Replaced by useWorkspaceType() hook |
getAllWorkspaceTypes() | 148-150 | Subsumed by useApplicationTypes().data |
getLauncherTypes() | 153-157 | Replaced by useLauncherTypes() hook |
formatTypeCount() | 186-192 | Re-exported from use-application-types.ts |
logger import | 3 | Deleted with registerType() |
Lucide icon imports + LucideIcon | 1-2 | Moved into icon-name map in hook module |
Retained verbatim:
APPLICATION_TYPE_KEYStuple (L166-173)getValidTypeValues()(L181-183)- Top-of-file comment block (L1-11) — updated to reflect Path c completion rather than “follow-up backlog item”
Post-deletion, lib/workspace-types.ts is ~25 lines of sync constraint
plumbing — no UI metadata. The file’s purpose collapses to “the sync source
of truth for application_types keys, paired with the DB seed.”
Zero-callers verification gate: before deleting any symbol, the
Executor must run an ast-dataflow rename-sweep equivalent or a grep -rn
across app/, components/, lib/, hooks/, contexts/, __tests__/
confirming zero remaining call sites for each deleted symbol. This is the
same posture as S248 T4 and S249 ID-23.
P-5 — Test alignment
Section titled “P-5 — Test alignment”Modified: __tests__/lib/workspace-types.test.ts
Today the file tests the 5 deleted public functions. Post-refactor it should
test only what lib/workspace-types.ts retains: APPLICATION_TYPE_KEYS
shape + getValidTypeValues() tuple invariants. Delete the
describe('getWorkspaceType'), describe('getAllWorkspaceTypes'),
describe('getLauncherTypes'), describe('formatTypeCount') blocks; keep
the describe('getValidTypeValues') block.
New: __tests__/hooks/use-application-types.test.ts
Mirror the existing TanStack hook test patterns in __tests__/hooks/ (e.g.
how use-company-profiles is exercised — look at __tests__/hooks/intelligence/
or __tests__/api/intelligence/ for the closest existing pattern).
Coverage targets:
- Hook returns 6 rows when
application_typestable is mocked with 6 rows. useWorkspaceType('procurement')resolves to a config withlabel === 'Procurement',route === '/procurement',available === true,hasCustomCreation === true,icon === Briefcase(resolved from string).useWorkspaceType('unknown_key')resolves toundefined(preserves the currentgetWorkspaceType()contract).useLauncherTypes()filters out types whereroute === null && available === true(preserves currentgetLauncherTypes()semantics).formatTypeCount(undefined, 0)falls back to'0 active workspaces'(preserves current undefined-config branch informatTypeCount()).formatTypeCount(intelligenceConfig, 3)returns'3 active intelligence streams'(preserves singular/plural logic).
Mock strategy: createMockSupabaseClient() from
__tests__/helpers/mock-supabase.ts, returning the 6 seed rows verbatim
with the Option C metadata grafted on (matches what the
select: (rows) => rows.map(toWorkspaceTypeConfig) selector produces).
§4 Metadata location — Option C (hybrid) recommendation
Section titled “§4 Metadata location — Option C (hybrid) recommendation”The 8 metadata fields decompose into two groups by editability requirement:
| Field | Editable by client admin? | Lives where (Option C) |
|---|---|---|
key | No (immutable identity) | DB: application_types.key (today) |
label | Yes (rebranding) | DB: application_types.label (today) |
defaultIcon | Yes (admin theming) | DB: application_types.default_icon (column exists; NULL) |
defaultColour | Yes (admin theming) | DB: application_types.default_colour (column exists; NULL) |
labelPlural | Yes (rebranding) | DB: NEW column application_types.label_plural (per §6) |
description | Yes (admin copywriting) | DB: NEW column application_types.description (per §6) |
route | No (code-side routing) | Static client map in use-application-types.ts |
available | No (code-side feature flag) | Static client map in use-application-types.ts |
hasCustomCreation | No (code-side dispatch) | Static client map in use-application-types.ts |
features.* | No (code-side feature flag) | Static client map in use-application-types.ts |
Why Option C over A or B:
| Option | Pros | Cons |
|---|---|---|
| A — all DB | Single source of truth | Schema migration adds 5 columns; route/available/features are code-side concerns the DB cannot enforce; admin UI gate (Q-OQR1-13) |
| B — all static client | No migration, no admin UI gate | Static registry survives in a thinner form — not a meaningful step forward from today |
| C — hybrid | Editable copy/branding in DB (label, labelPlural, description, defaultIcon, defaultColour); code-side concerns (route, available, hasCustomCreation, features) stay in code where they can be PR-reviewed; matches the existing default_icon / default_colour precedent | Two-source registry, but the boundary is clean (admin-editable ↔ developer-editable) |
Rationale for the split:
routeis a Next.js path — the static client map is the only place it can meaningfully be enforced.availableandfeatures.*gate code paths (e.g.available: falsesurfaces “Coming soon”); they are code-side feature flags that should land via PR review, not via an admin UI mutation.label,labelPlural,description,defaultIcon,defaultColourare user-visible strings — they are exactly the surface a future client-admin UI would want to edit (Q-OQR1-13 v1.1 ratification).
The static client map in use-application-types.ts is keyed by
application_types.key and contributes only the 4 code-side fields. Joining
it with the DB row happens in the select: selector — the hook output is a
single flat WorkspaceTypeConfig indistinguishable from today’s shape.
§5 Implementation order + dependencies
Section titled “§5 Implementation order + dependencies”Sequential land order (each step blocks the next):
- Land migration (§6) — adds
label_plural,descriptioncolumns toapplication_types, backfills the 6 seed rows with values matching the current static registry’s labels forprocurement,intelligence,proposaland reasonable defaults (or NULL fallback handled in code) for the 3 other seed rows that don’t have a current UI surface (sales_proposal,product_guide,competitor_research,training_onboarding). Also backfillsdefault_icon+default_colourfrom the current static registry values for the 3 rendered types. - Regen DB types —
bun run … supabase gen types typescript …per CLAUDE.md command table; the regen extendsapplication_typesRow/Insert/Update with the 2 new column triples. - Land hook + route + queryKey namespace —
useApplicationTypes()+GET /api/application-types+queryKeys.applicationTypes.list. At this point the hook coexists with the static registry; no consumer uses it yet. - Migrate 3 UI consumers in any order (independent files).
- Delete static registry surface in
lib/workspace-types.tsper §3 P-4. Run the zero-callers verification gate before each deletion. - Land test changes per §3 P-5 (modify existing test + add new test).
- Run regression suite —
bun run test+bun lint+ an E2E smoke on the/workspaceslauncher page.
Chicken-and-egg note: the hook (step 3) cannot return useful data until the migration (step 1) lands. The Path-c-correct sequence is migration → regen → hook → consumers → deletion. Reversing 1↔3 would either ship a hook returning NULL-filled rows or require code-side fallback logic that this spec specifically avoids.
§6 Migration plan (Option C)
Section titled “§6 Migration plan (Option C)”Migration filename: supabase/migrations/<timestamp>_id_21_application_types_metadata_columns.sql
-- =============================================================================-- ID-21 — application_types metadata columns (label_plural, description)-- =============================================================================-- Adds the two metadata columns that currently live in the static-- `WORKSPACE_TYPE_REGISTRY` (lib/workspace-types.ts). Backfills the 6 seed-- rows. No data loss — the static registry remains the source of truth-- until §5 step 5 deletes it; this migration writes the same values the-- registry holds so the hook output matches today's UI output byte-for-byte-- for the 3 currently-rendered types.---- Path c posture: see docs/specs/id-29-tanstack-workspace-types/TECH.md-- =============================================================================
SET search_path = public, extensions;
BEGIN;
ALTER TABLE public.application_types ADD COLUMN label_plural text NULL, ADD COLUMN description text NULL;
-- Backfill from current static registry values.-- procurement / intelligence / sales_proposal (= "proposal" in registry)-- get exact labels. Other 3 seed rows take reasonable defaults derived from-- the `label` column; the UI does not render them today so cosmetic drift-- is acceptable (Q-OQR1-13 admin UI will let clients fix in v1.1).UPDATE public.application_types SET label_plural = 'Procurements', description = 'Manage bid responses and tender submissions using your knowledge base', default_icon = 'briefcase', default_colour = '#d4880f'WHERE key = 'procurement';
UPDATE public.application_types SET label_plural = 'Intelligence Streams', description = 'Sector and competitor news feeds tailored to your company profile.', default_icon = 'newspaper', default_colour = '#059669'WHERE key = 'intelligence';
UPDATE public.application_types SET label_plural = 'Sales Proposals', description = 'Draft and manage sales proposals drawing on your knowledge base', default_icon = 'file-signature', default_colour = '#0d9488'WHERE key = 'sales_proposal';
UPDATE public.application_types SET label_plural = label || 's', description = labelWHERE key IN ('product_guide', 'competitor_research', 'training_onboarding') AND label_plural IS NULL;
-- Post-check: verify the 3 currently-rendered types are fully backfilled.DO $$DECLARE v_unbackfilled int;BEGIN SELECT count(*) INTO v_unbackfilled FROM public.application_types WHERE key IN ('procurement', 'intelligence', 'sales_proposal') AND (label_plural IS NULL OR description IS NULL OR default_icon IS NULL OR default_colour IS NULL); IF v_unbackfilled <> 0 THEN RAISE EXCEPTION 'ID-21 backfill incomplete: % rows in render-set still NULL', v_unbackfilled; END IF;END $$;
COMMIT;Why no REVOKE EXECUTE block
Section titled “Why no REVOKE EXECUTE block”This migration adds columns and runs an UPDATE — no new public.*()
functions are created. The CLAUDE.md S250 W1b gotcha (anon-EXECUTE auto-grant)
applies only to CREATE OR REPLACE FUNCTION public.*. The anonymous DO $$
block executes inline and does not register as a callable function. No
REVOKE/GRANT is required.
RLS / grants
Section titled “RLS / grants”The application_types table already has RLS enabled and the
application_types_select_all FOR SELECT USING (true) policy from T2
(migration 20260520120828 §1.1). New columns inherit the policy without
modification. The GET /api/application-types route reads via
getAuthenticatedClient() (any role); anonymous reads are blocked by the
proxy not being public.
CLI apply discipline
Section titled “CLI apply discipline”Per CLAUDE.md Gotchas:
- Sandbox-disable required for
supabase migration new,db push,gen types. cat supabase/.temp/project-refbefore each push (staging-first apply).- Staging-apply first (turayklvaunphgbgscat), Liam ratification gate, then prod (rovrymhhffssilaftdwd).
- Regenerate
supabase/types/database.types.tspost-apply.
§7 Risk + open questions
Section titled “§7 Risk + open questions”Risks (ranked)
Section titled “Risks (ranked)”| # | Risk | Mitigation |
|---|---|---|
| R-1 | Loading-state flicker on /workspaces launcher between server-rendered count grid and client-resolved metadata | First paint renders the count grid skeleton (existing layout container) until useApplicationTypes() resolves (~50ms) |
| R-2 | Hook returns undefined for a workspace whose application_types.key is unknown (cross-env drift) | The 3 consumer files already null-coalesce — current getWorkspaceType() returns undefined for unknown keys, same posture |
| R-3 | Icon-name → LucideIcon resolution may drift if a new icon-name is seeded that the static map doesn’t carry | toWorkspaceTypeConfig() selector falls back to a default Folder icon when the icon-name map misses (matches today’s card-render path L40) |
| R-4 | application_types table is read on every workspace render — N+1 risk if uncached | staleTime: 5 * 60_000 (5 min) on the hook; closed-list reference data, no invalidation triggers exist |
| R-5 | Test mocks for the hook need a Supabase client mock; the __tests__/helpers/mock-supabase.ts pattern is well-trodden | Use createMockSupabaseClient(); mirror use-company-profiles test setup |
Open ratification gates (for Liam before PLAN.md)
Section titled “Open ratification gates (for Liam before PLAN.md)”| Q-N | Question | Default if not answered |
|---|---|---|
| Q-1 | Recommendation in §4 is Option C (hybrid). Ratify, or request A (all DB) / B (all static client)? | Option C |
| Q-2 | The migration backfills sales_proposal description verbatim from the current proposal registry entry (“proposal” key in registry maps to sales_proposal DB key — verified S246 T2 §1.2 seed). OK? | Yes — text matches static today |
| Q-3 | Should useApplicationTypes() be authenticated (current spec) or unauthenticated (closed-list reference data, no PII)? Authenticated is the safer default per pattern. | Authenticated (any role) |
| Q-4 | Do we need a feature flag / kill-switch to roll back to the static registry if the hook misbehaves in prod? | No — the deletion of the static registry is the point of the work; rollback is a git revert |
| Q-5 | Should the icon-name → LucideIcon static map live in use-application-types.ts (per §4) or in lib/workspace-types.ts (next to APPLICATION_TYPE_KEYS)? | In use-application-types.ts (UI metadata, hook scope) |
S251 Liam ratification (21/05/2026)
Section titled “S251 Liam ratification (21/05/2026)”| Q-N | Ratification |
|---|---|
| Q-1 | Option C ratified, with the additional stipulation that docs/specs/id-31-0.9-canonical-pipeline/PRODUCT.md Q-OQR1-13 bullet is amended to record: (a) what v1.0 ships via this spec (the hybrid DB-columns + static-client-map split), and (b) what v1.1 must close to restore a single source of truth (collapse the static map into DB columns once admin UI mutation is safe). Cross-ref edit landed alongside this TECH.md cherry-pick. |
| Q-2 | Yes — verbatim backfill of sales_proposal description from the current static proposal registry entry. |
| Q-3 | Authenticated (any role) — standard auth middleware pattern; no need to add to publicRoutes in proxy.ts. |
| Q-4 | No — feature flag / kill-switch not required; rollback is git revert of the registry-deletion commit. |
| Q-5 | hooks/use-application-types.ts — icon-name → LucideIcon static map co-located with the consumer of the icon. |
§8 Sources cited
Section titled “§8 Sources cited”- Backlog item
ID-21—docs/reference/product-backlog.json(Path c reframe + GitNexus 132-symbol HIGH-risk finding). - S248 T4 procurement umbrella rename — commits
99dfc5fd,51b6cc8c,f9261904; specifically the header-comment block added tolib/workspace-types.ts:1-11. - S249 ID-23 closeout — commit
95b660ef; referenced as thegitnexus_impactinvestigation that ratified Path c. - T2 application_types migration —
supabase/migrations/20260520120828_t2_combined_pr_intel_shape_b_form_type_split.sql§1.1-1.2 (table + 6 seed rows), §1.3 (FK on workspaces), §application_types_select_all policy. - Generated DB types —
supabase/types/database.types.tsL15-52 (application_typesRow/Insert/Update withdefault_icon/default_colouralready nullable). - Existing TanStack hook pattern —
hooks/intelligence/use-company-profiles.ts(L33-47 = single-fetch hook shape mirrored here). - Existing API route pattern —
app/api/intelligence/profiles/route.ts(GET-only authenticated read). - TanStack query-key pattern —
lib/query/query-keys.ts(intelligence / workspaces / changeReports namespaces as the shape exemplar). - CLAUDE.md gotchas: (a) anon-EXECUTE auto-grant pattern (S250 W1b), (b)
SET search_path = public, extensions, (c) sandbox-disable for Supabase CLI, (d)cat supabase/.temp/project-refbefore push, (e) RLS auto-enable event trigger (P-1 RLS already coversapplication_typesvia T2).