Skip to content

feat: the wardrobe workshop M2 — widened font law + the v2 theme editor (F104, T205–T209) - #395

Merged
genwave-radio merged 5 commits into
mainfrom
feat/wardrobe-m2
Aug 7, 2026
Merged

feat: the wardrobe workshop M2 — widened font law + the v2 theme editor (F104, T205–T209)#395
genwave-radio merged 5 commits into
mainfrom
feat/wardrobe-m2

Conversation

@genwave-radio

Copy link
Copy Markdown
Collaborator

👗 What

F104 M2 — the editor half of the wardrobe workshop, built on the released M1 library (v3.1.0/v3.1.1). Five tasks, one commit each:

Task Commit Delivered
T205 f70ec69 ⚖️ ThemeFontProvenanceValidator widens to vendored ∪ installed; ceiling sums across both sets; import 400 names the missing face and the providing pack slug (fail-soft index lookup); catalog CI pinned curated-only
T206 c5ea254 🎨 Editor page — base-theme + display/sans role pickers over a single server-derived assignable set; transient scoped preview (T186 mechanism); remix is client-state only; unassigned roles carry base declarations byte-untouched
T207 98a28f3 💾 Save-as-own — both write routes behind one shared ThemeWriteGate (gate-identical by construction, 7-row parity table); imported_from NULL; fail-closed 409 when a slug holds an imported theme; imported_at CASE fix; /api/fonts/vendored/api/fonts/assignable rename (pre-ship)
T208 6986d4c 🗑️ Uninstall with the guard — the referenced-theme check lives inside the DELETE statement (WHERE NOT EXISTS, no advisory pre-check); refusal names the referencing themes; /fonts stops serving next request; vendored untouched
T209 ecdbaf2 🔍 Disclosure re-audit — self-derived AdminSurface+Settings sweep with public-404 probes + internal positive controls; the one sanctioned spectator change (theme options gain exactly {slug, name}, F102.10a) pinned; GuardedRouteInspector extraction

✅ Gates

  • Every task through builder → independent opus review (FAIL→fix rounds recorded per task) → production smoke → commit. Nothing landed on a first-draft green.
  • Key pins mutation-tested (house standard): parity table, fail-closed 409, uninstall guard, stops-serving, spectator counterexample, positive controls.
  • Production smokes: isolated scratch stacks (fresh Postgres + full migrate.sh + real catalog origin), real Kestrel + production Next build, Playwright browser runs for T206/T207; API both-ways for T208.
  • Final suite: 2,966 passed / 0 failed (Core 140, Orchestration 199, Tts 414, Host 1563, MediaLibrary 650 incl. dockerized-Postgres integration). Zero warnings. Engine/compose zero-diff pins green.

📎 Companion

  • genwave-catalog PR (stacked): themes-stay-curated-only CI gate + red variant (the T205 cross-repo pin) — merge either order, independent.

🧾 Carry-forwards (known, not in this PR)

🚦 Next

T210 🖐️ exit-demo (mix, save, activate on the demo box) closes F104 — after merge → release → pin → --pinned.

ThemeFontProvenanceValidator now accepts a face from GenWave's vendored
curated set OR the station's currently-installed font packs
(InstalledFontCatalog); the per-theme byte ceiling sums recorded bytes
across both sets. Import's 400 names the missing face and, when the
catalog index knows a pack that provides it, the pack's own slug too
(fail-soft: an unreachable/disabled catalog still names the face, just
without the suggestion). Preview widens the same way but never pays for
the catalog-index round trip since nothing it composes persists.
Catalog CI stays curated-only — themes may still reference only the
vendored set, pinned separately in genwave-catalog.

5 new specs in Story285_WidenedFontLaw.cs drive the real production
pipeline (install then import through WebApplicationFactory<Program>),
smoke-tested end to end against a real Postgres + Kestrel + local
catalog origin.
… (T206, STORY-286)

The v2 editor mixes a base theme's palette with a face per role (display/sans),
previewing through the transient scoped compose the T186 mechanism already
established (POST /api/themes/preview); the remix lives in client state only —
nothing persists, nothing is served station-wide at any point.

The assignable face set is derived exactly once, server-side: GET
/api/fonts/vendored now projects vendored ∪ installed into one union, one row
per family, so the client stops re-deriving it with its own heuristic. An
unassigned role carries the base theme's own font declaration through
byte-untouched (weight ranges, italic siblings included); only an explicit
assignment replaces it with the single-face 400/normal shape SPEC F104.11
scopes assignment to. The catalog kill-switch posture is Fact-pinned: GET
/api/fonts/vendored and GET /api/themes stay 200 with the catalog disabled
while POST /api/fonts/{slug}/install still 404s.
…ared write gate (T207, STORY-287)

- ThemeWriteGate: the two station.theme write routes now share one two-phase
  gate pipeline (ValidateSlug + ReadParseAndValidateAsync) instead of two
  hand-copied blocks — gate-identical by construction, proven by a 7-row
  parity table driving both routes with the same bad bodies.
- Fail-closed 409 when a save-as-own targets a slug that already holds an
  imported theme — an authored save can never silently erase another
  theme's provenance; authored-over-authored re-saves stay allowed.
- ThemeRepository.UpsertAsync: imported_at is now a CASE expression, stamped
  only when imported_from is non-null — restores OwnerTheme's own
  "ImportedAt is null exactly when ImportedFrom is" invariant.
- Renamed GET /api/fonts/vendored -> /api/fonts/assignable end to end
  (route, AssignableFaceDto, admin-ui) — the old name promised "vendored
  only" while the response has carried installed faces too since T206.
- Restored catalogSlug's original precedence ahead of the shared gate's
  body-read/parse/font-law phase, pinned by a live-precedence Fact.

Production smoke (isolated scratch stack, Playwright + curl): editor save
flow verified live (Space Grotesk assigned, saved, immediately selectable
with no reload); theme.css resolves the saved slug; import vs save-as-own
byte-identical 400 on the same bad manifest; fail-closed 409 on an
already-imported slug with provenance intact; base theme byte-identical
after the save.
…al (T208, STORY-288)

DELETE /api/fonts/{slug} removes an installed pack unless a saved/imported
station.theme row still references one of its faces. The guard lives inside
the DELETE statement itself (WHERE NOT EXISTS over a shared substring
predicate) rather than as an advisory pre-check a concurrent write could
race past — refusal names every referencing theme by slug; on success,
InstalledFontCatalog.ReloadAsync stops /fonts/{file} serving on the very
next request, vendored faces are untouched, and provenance rows are gone
via ON DELETE CASCADE. A Host-side tripwire pins the exact quoted
"/fonts/<file>" shape the guard depends on, so a future change to how
themes reference faces fails loudly instead of silently blinding the guard.
…tator counterexample pinned (T209, STORY-289)

Consolidated AdminSurface+Settings sweep with per-endpoint public-404 probes
and internal-listener positive controls; the one sanctioned spectator change
(theme options gain exactly {slug,name}, F102.10a) pinned; GuardedRouteInspector
extraction (Story278/283 migrated, coverage byte-identical); FontEndpoints
Last-Modified asymmetry RULED cosmetic and recorded.
@genwave-radio
genwave-radio merged commit 137e43e into main Aug 7, 2026
11 checks passed
@genwave-radio
genwave-radio deleted the feat/wardrobe-m2 branch August 7, 2026 18:51
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant