docs(spec): amend §8 severity-map + §6 stories-location + §8.X enforcement

- §8 severity table now covers all 21 values of the 5 mirrored enums
  (ShiftAssignment/ArtistEngagement/Payment/Person/MatchStatus); drops
  3 phantom rows (active/inactive/expired, in no enum). Closes the
  silent grey-fallback gap for 11 production values found in the
  Plan 3 brainstorm self-audit.
- §6 stories-placement reworded: custom/wrapper components (incl.
  PrimeVue wrappers) co-locate; only the ~80 PrimeVue catalog +
  Foundations centralize. Plan 2 misread this.
- New §8.X: bidirectional Vitest consistency test
  (apps/app/tests/unit/utils/statusSeverity.consistency.spec.ts),
  added in Plan 3 — fails on any unmapped enum value OR orphan key.

Plan 3 cleanup tasks (tracked in the Plan 3 plan doc, not here):
  (a) migrate Plan 2's 6 centralized stories to co-located.
  (b) refactor AppTopbar to wrap PrimeVue Menubar per RFC AD-3.

Background: discovered during Plan 3 (Tier-1 primitives) brainstorm
self-audit. Fixes spec-vs-reality drift and two Plan 2 deviations
from binding spec/RFC text; prevents recurrence for future enums.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 19:28:03 +02:00
parent 95496ce216
commit ae0bd2da6c

View File

@@ -281,9 +281,28 @@ Custom components are first-class in Storybook exactly like PrimeVue
ones. crewli-starter `ComponentsPage.vue` (~80 PrimeVue components) is
the **source of truth for the standard-component catalog**.
**Story placement:** custom/wrapper stories co-located next to the
`.vue` (moves with the file at cutover); PrimeVue standard catalog and
Foundations centralized under `stories/`.
**Story placement (binding — two disjoint classes):**
1. **Custom/wrapper components.** Every component authored in this repo
— shell pieces (`AppSidebar`, `AppTopbar`, `WorkspaceSwitcher`,
`RightDrawer`, …), Tier-1 primitives (`StatusTag`, `StatCard`,
`StateBlock`, `PageHead`, `TagsInput`, `EnergyDots`, `EnergyPicker`),
`DraggableBlock`, and the template layer — **including components
that internally wrap a PrimeVue component**. Their `.stories.ts` is
**co-located next to the `.vue`** so it moves with the component at
cutover and is deleted with it. This is the default for essentially
all `components-v2/**` work.
2. **PrimeVue standard catalog + Foundations.** The ~80-component
pure-PrimeVue demo gallery (source of truth: crewli-starter
`ComponentsPage.vue`) and the Foundations stories
(Color/Typography/Spacing/Icons/Dark/Density) — neither has an owning
`.vue`. These are **centralized under `stories/`**.
"Wraps a PrimeVue component" does **not** reclassify a component into
the catalog — a wrapper is still custom and co-locates. (Plan 2 misread
the prior wording and centralized 6 shell stories under
`src/stories/v2/`; Plan 3 carries a cleanup task to migrate them — see
commit body.)
**Story tree:** `Foundations/` (Color, Typography, Spacing, Icons, Dark
mode, Density) · `PrimeVue/` (~80, grouped as in ComponentsPage) ·
@@ -395,18 +414,42 @@ knowledge, not a per-page decision** — it lives in one map
| Status value(s) | `severity` | Semantics |
|---|---|---|
| `approved`, `confirmed`, `completed`, `active` | `success` | terminal-good / active |
| `pending`, `pending_approval`, `invited` | `warn` | awaiting action |
| `rejected`, `cancelled`, `declined` | `danger` | terminal-bad |
| `draft` | `secondary` | not yet live |
| `inactive`, `expired` | `secondary` (muted) | dormant |
| (unmapped fallback) | `info` | + dev-warn so gaps surface |
| `approved`, `completed`, `confirmed`, `contracted`, `paid_in_full` | `success` | terminal-good / fully settled |
| `pending_approval`, `pending`, `applied`, `option`, `offered`, `reverted` | `warn` | organizer action required |
| `invited`, `requested`, `deposit_paid` | `info` | awaiting external party / in-progress — no viewer action |
| `none`, `draft`, `dismissed` | `secondary` | muted — absent / not-yet-live / archived |
| `rejected`, `cancelled`, `declined`, `no_show` | `danger` | terminal-bad |
| (unmapped at runtime) | renders `info` + dev console warn | unreachable in a passing build — §8.X consistency test fails on any gap |
Rule: **every** backend status enum mirrored into `src/types/` gets a
row here in the same PR that adds the enum (extends the existing
"mirror backend PHP enums" project rule). `StatusTag` never inlines a
severity; it always resolves through this map.
### 8.X Enforcement (binding)
The §8 severity map is mechanically enforced by a single Vitest unit
test, `apps/app/tests/unit/utils/statusSeverity.consistency.spec.ts`,
added in Plan 3 alongside `statusSeverity.ts`. It imports the live enum
modules (`ShiftAssignmentStatus`, `ArtistEngagementStatus`,
`PaymentStatus`, `PersonStatus`, `MatchStatus`) and asserts **both**
directions:
1. **Completeness** — every value of every listed enum resolves to an
explicit severity in `statusSeverity.ts`, never the dev-fallback.
Guards the failure mode that left 11 values silent-falling to grey
`info`.
2. **No phantoms** — every key in `statusSeverity.ts` corresponds to a
value present in at least one listed enum. Guards the inverse mode
(the original table mapped `active`/`inactive`/`expired`, which exist
in no enum).
Adding a new mirrored enum, or a new value on an existing one, requires
extending **both** `statusSeverity.ts` and this test's enum-list in the
**same commit**. A silent fallback or an orphan key is a test failure
that blocks CI, not a convention. This extends the existing "mirror
backend PHP enums" rule (CLAUDE.md) with a mechanical gate.
**Tier-2 — Smart Filter subsystem (secure generic version now):**
`SmartFilterBar` + `FilterChip` + `FilterPopover` + `AddFilterMenu` +
5 editors (Text / NumberRange / MultiSelect / EnumGrid / Tag) +