Skip to content

refactor(cms): single TS source of truth for document_type and stage enums#72

Open
RickCogley wants to merge 1 commit intomainfrom
refactor/cms-enum-source-of-truth
Open

refactor(cms): single TS source of truth for document_type and stage enums#72
RickCogley wants to merge 1 commit intomainfrom
refactor/cms-enum-source-of-truth

Conversation

@RickCogley
Copy link
Copy Markdown
Member

Summary

Collapses document type and lifecycle stage from 7 / 5 hand-synced locations down to a single TS source of truth. Each UI picker now imports derived constants; the Zod enum drives value set; a unit test guards against SQL CHECK drift.

Addresses exactly the failure mode that the pre-0036 `runbook` gap exhibited: something present in SQL CHECK + UI dropdowns + TS union but silently missing from the Zod enum.

Changes

`src/lib/schemas.ts` — new single source

  • `documentType` Zod enum stays authoritative for the value set
  • New `DOCUMENT_TYPE_LABELS: Record<DocumentTypeValue, string>` — exhaustive, so adding a Zod value without a label is a TS error
  • New `DOCUMENT_TYPES: ReadonlyArray<{value,label}>` derived from the Zod enum's `.options` + the labels Record, in enum order
  • Same three-part pattern for `stageSchema` / `LIFECYCLE_STAGE_LABELS` / `LIFECYCLE_STAGES`
  • `DocumentTypeValue` and `StageValue` types exported

4 Svelte files — import + loop

  • `documents/new/+page.svelte`, `documents/[id]/+page.svelte`, `templates/new/+page.svelte`, `templates/[id]/+page.svelte`
  • Local `documentTypes` / `lifecycleStages` arrays deleted
  • Inline `` lists replaced with `{#each DOCUMENT_TYPES as type (type.value)}` loops
  • Net: -191 lines across the 4 files

`templates/[id]/+page.server.ts`

  • 15-arm inline TS union for `document_type` replaced with imported `DocumentTypeValue`
  • 7-arm inline stage union replaced with `StageValue`

Drift guard — `src/lib/schemas.test.ts` (new)

  • Asserts `DOCUMENT_TYPES` value set matches the Zod enum (both directions)
  • Asserts every Zod value has a label and vice versa
  • Asserts order is preserved
  • Same three for `LIFECYCLE_STAGES`
  • SQL CHECK drift guard: reads all migration files via Vite's `import.meta.glob` (no `@types/node` dep), extracts the most-recent `templates.document_type` CHECK list, asserts it equals `DOCUMENT_TYPES.map(t => t.value)`. Next time someone adds a type to Zod but forgets the migration, CI fails.

Minor UX change

The template-list type filter on `/documents/new` previously used plural labels ("Proposals", "Reports"). It now uses the canonical singular labels from `DOCUMENT_TYPES` ("Proposal", "Report"). Deliberate — keeping them separate would preserve one of the drift surfaces this PR is meant to eliminate. The "All Types" default option is prepended in place.

Test Plan

  • `npm run check` — 0 errors (31 pre-existing warnings unchanged)
  • `npx vitest run src/lib/schemas.test.ts` — all 6 tests pass
  • Pre-commit hooks (oxlint + eslint + prettier) pass
  • Spot-check: open `/templates/new` and `/documents/new` — same 15 types + 7 stages in same order
  • Edit an existing document, change type and stage, save — round-trip still works

Out of scope

  • Codegen for SQL migrations from the TS constant (drift guard test covers the same risk with less machinery)
  • Dropping the SQL CHECK constraint entirely (belt-and-suspenders wins)
  • Fragment or sensitivity enum consolidation (same pattern could apply; out of scope for this PR)

InfoSec

No security impact — pure TS restructuring of existing values. Parameterized D1 binds unchanged (OWASP A03 preserved). Drift guard strengthens defense-in-depth against schema/code divergence.

Closes #71

…source

The document_type enum previously lived in 7 places (SQL CHECK, Zod enum,
4 UI dropdown arrays, 1 inline TS union). stage lived in 5. That drift
surface is exactly how the pre-0036 'runbook' gap happened: present in
SQL CHECK + UI + TS union, missing from Zod.

This refactor makes the Zod enum the single source of values. UI pickers
import DOCUMENT_TYPES / LIFECYCLE_STAGES constants derived from the Zod
enum + a labels Record, so adding a type in one place propagates.

- schemas.ts: DOCUMENT_TYPES, DOCUMENT_TYPE_LABELS, LIFECYCLE_STAGES,
  LIFECYCLE_STAGE_LABELS exports. Labels Records are typed exhaustively
  (Record<DocumentTypeValue, string>) so TS errors if a new enum value
  doesn't get a label. DocumentTypeValue and StageValue types exported.
- 4 Svelte files: local arrays / inline <option> lists replaced with
  imported constants + {#each} loops.
- templates/[id]/+page.server.ts: 15-arm inline TS union replaced with
  DocumentTypeValue / StageValue.
- Minor UX change: the template-list filter in documents/new uses the
  canonical singular labels (Proposal) rather than the previous plural
  (Proposals). One less drift surface.

Drift guard test (schemas.test.ts): reads all migration files via
Vite's import.meta.glob, extracts the most recent templates.document_type
CHECK list, asserts it matches DOCUMENT_TYPES. Fails CI if a future
migration forgets a type. No @types/node dependency needed.

Closes #71

InfoSec: no security impact — pure TS restructuring of values that
already existed. Parameterized D1 binds unchanged (OWASP A03 preserved).
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
esolia-hanawa f791d64 Commit Preview URL

Branch Preview URL
Apr 17 2026, 11:00 PM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(cms): collapse document_type and stage enums to a single TS source of truth

1 participant