Brainstormed design for §22 slice M3 (registry mirror + routing + runtime branding). Decomposes M3 into a §10.3 Tier-1 test/local-env foundation (M3-0) plus four sequential feature sub-plans (M3a migration+restamp, M3b registry mirror, M3c routing+redirects+branding, M3d landing-state/review). Records resolved decisions: hard-cut branding, §4.1 reconciler pattern, Stage-1 forward-only migration latitude with a future expand/contract note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.1 KiB
M3 — Registry mirror + routing + runtime branding — design
Date: 2026-06-03
Spec basis: docs/design/multi-project-spec.md §22, Part C slice M3
Status: design, pending plan
Goal
Carry §22 slice M3 end-to-end: turn the framework from single-project
(the N=1 default at the legacy corpus root) into genuinely multi-project —
projects declared in a registry, reachable at /p/<project>/, branded at
runtime. M3 is the slice that makes a second, named project reachable by
URL at all.
Driving objective: stand up the deployment's bdd "planner" project,
reachable at https://rfc.wiggleverse.org/p/bdd/ as an unlisted
project (readable by direct link; not in the directory). After M3c the
planner is navigable; after M3d its entries land active (the behavior a
planner wants). The planner's bdd-specific scenario/coverage surface is
M5, out of scope here.
Note on the original URL ask
/bdd/{PLANNER_TOKEN}/: resolved to the existing §22 model —bddis anunlistedproject at/p/bdd/; the "token" is just the shareable link (the unguessable slug). No token-gate feature is built;unlistedvisibility already exists (gated in M2).
Decisions
Already resolved upstream (commit ad2ece1, §22.13/§22.10/§22.4)
- Default project
id= config-derived slug (DEFAULT_PROJECT_ID> slug(deployment name) >default). M1'sdefaultbootstrap id is re-stamped to it in M3 before any/p/URL is public. - Entry segment = generic
/p/<project>/e/<slug>for every type; the noun (RFC/Spec/Feature) is a type-driven UI label, not in the path. - Legacy
RFC-NNNN= frozen, read-only display label in frontmatter; never used for routing/lookup; never assigned to new entries.
Resolved in this brainstorm
- Plan slicing — M3 is delivered as a foundation plan (M3-0) + four
sequential feature sub-plans (M3a–M3d), each its own branch off
main, its own review checkpoint, its own merge. Lower risk for the live OHM deployment than one long-running branch. - Branding cutover — hard cut + loud failure. Frontend reads the
deployment/project name only from
GET /api/deployment;VITE_APP_NAMEis removed from the frontend;REGISTRY_REPObecomes required at startup (per CLAUDE.md's loud-failure rule). - Registry mirror mechanism — follow the existing §4.1 cache pattern
(
Reconciler.sweep()+ webhook writer, reading via the Gitea API), not a new mechanism (no local clone, no separate service). - Test strategy — adopt the handbook's §10.3 two-tier testing standard. Build the Tier-1 local-Docker suite first as M3-0; each feature sub-plan adds unit/integration/functional/e2e coverage on top. Tier-2 (PPE) is a separate flotilla/Stage-2 task (below).
- Deployment stage — OHM is Stage 1 (pre-v1, single prod VM, forward-only migrations). So M3a's table-rebuild migration is allowed as one forward-only migration. The §10.2 expand/contract rule is recorded as a future Stage-2 obligation, not a constraint on M3a today.
Decomposition
Order is dependency-driven: M3-0 → M3a → M3b → M3c → M3d.
M3-0 — Test & local-env foundation (the §10.3 Tier-1 suite)
Greenfield; lands before feature work so every sub-plan can be verified at
all levels. Reuses the existing in-process app_with_fake_gitea fixture seam
where possible.
- Docker local stack (
docker compose): backend (FastAPI) + built frontend + fresh SQLite + Mailpit (mail sink for OTC/invite flows)- a disposable/stub Gitea for content and registry repos.
- Open design point for the M3-0 plan: promote the in-process
fake_giteadouble into a small standalone HTTP stub vs. run real Gitea in a container. Decide in M3-0's own plan.
- Frontend unit: add Vitest + testing-library to
frontend/package.json(none today). - E2E: add Playwright, suite environment-agnostic —
parameterized by
BASE_URL+ the mail-sink API URL (§10.3). Default target = localhost Docker; PPE host whenBASE_URLis set toppe.<host>. One smoke spec: load/directory → open a project → view an entry. - Tier-2 / PPE is NOT built here. Standing up
rfc-app-ppe.<base>(its own micro VM, own gcloud project, own isolated Gitea, always-pass Turnstile keys) is a flotilla operator task in the deployment tooling, tracked separately. The suite is written to point at it once it exists.
M3a — Schema migration + default-id restamp (foundation; runs against live OHM data)
- Migration
027(next number after026): create-copy-drop-rename for the 12 tables enumerated in the026_projects.sqlheader, foldingproject_idinto each PK/UNIQUE key and adding theproject_idFK →projects(id). (cached_prsneeds no rebuild — already globally unique.) - Additive
type+initial_statecolumns onprojects. - Restamp (§22.13 step 1): default project
iddefault→ config-derived slug, cascaded across everyproject_idFK. Idempotent. - Live-data safety (Stage 1): back up prod SQLite; rehearse on a copy of prod; assert per-table row-count parity; FK enforcement on. Forward-only; brief restart blip acceptable per §9.
- Gate: until this lands, a second project can collide with the first.
M3b — Registry mirror
REGISTRY_REPOrequired at startup (loud failure if unset).refresh_registry()incache.py: readprojects.yamlfromREGISTRY_REPOvia the Gitea API; upsertprojectsrows (id,name,content_repo,type,visibility,initial_state,enabled_models/theme→config_json,registry_sha); cache deploymentname/tagline. Rows never written from user actions.- Webhook branch in
webhooks.pyon push toREGISTRY_REPO;Reconciler.sweep()callsrefresh_registry().
M3c — Routing + redirects + runtime branding (planner becomes navigable)
- Backend:
GET /api/deployment(name, tagline, visible projects per §22.5),GET /api/projects/:id(name, tagline, type, philosophy pointer, theme);/p/<project>scoping on RFC routes. - Frontend:
/p/<project>/prefix +/e/<slug>segment; 308 redirects off old corpus-root URLs (/rfc/<slug>→/p/<default-id>/e/<slug>, etc.); hard cut offVITE_APP_NAMEto runtime branding; per-projectthemetoken overlay; deployment directory at/; project switcher.
M3d — Landing-state + review behavior
initial_state=activecreation path: land a new entryactive, stampunreviewed, skip the graduate gate when the project says so.unreviewedfrontmatter fields mirrored intocached_rfcs; owner/admin mark-reviewed action; §7 catalog unreviewed filter querying the cached column; type-driven entry noun in chrome.
Test coverage per sub-plan (all four levels)
- M3a — migration unit/integration: fresh DB and copy-of-prod fixture; row-count parity; FK enforcement; restamp idempotence.
- M3b — reconciler unit (yaml parse, upsert,
registry_sha) + integration (webhook →projectsrows) on the Docker stub Gitea. - M3c — backend functional (
/api/deployment,/api/projects/:id, 308 redirects,/pscoping) + frontend unit (branding/theme from API) + e2e (directory → project → entry nav; old-URL redirect resolves). - M3d — backend functional (active landing,
unreviewed, mark-reviewed, filter) + e2e (unreviewed filter, mark-reviewed action).
Out of scope / dependencies
- M4 (second-project acceptance pass), M5 (
bddscenario/coverage surface — the planner's type-specific UI), M6/M7. - PPE standup (
rfc-app-ppe.<base>) — flotilla operator task; prerequisite to running the suite's Tier-2 gate, not framework code. - Org-wide testing-standard activation — §10.3 already is the canonical
standard; it should not be duplicated into per-repo memory. Recommended
follow-up (separate, in the engineering repo + dev plugin): have
wgl-coding-session-initread app.json and, for apps that deploy the §8 standard stack, surface §10.3 + check whether the Tier-1 suite exists. Not part of M3.