Files
rfc-app/docs/superpowers/specs/2026-06-03-m3-design.md
Ben Stull 7703fa233a docs(design): M3 implementation design — decomposition, decisions, test strategy (§22)
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>
2026-06-03 21:02:40 -07:00

8.1 KiB
Raw Permalink Blame History

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 — bdd is an unlisted project at /p/bdd/; the "token" is just the shareable link (the unguessable slug). No token-gate feature is built; unlisted visibility already exists (gated in M2).

Decisions

Already resolved upstream (commit ad2ece1, §22.13/§22.10/§22.4)

  1. Default project id = config-derived slug (DEFAULT_PROJECT_ID > slug(deployment name) > default). M1's default bootstrap id is re-stamped to it in M3 before any /p/ URL is public.
  2. 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.
  3. Legacy RFC-NNNN = frozen, read-only display label in frontmatter; never used for routing/lookup; never assigned to new entries.

Resolved in this brainstorm

  1. Plan slicing — M3 is delivered as a foundation plan (M3-0) + four sequential feature sub-plans (M3aM3d), 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.
  2. Branding cutoverhard cut + loud failure. Frontend reads the deployment/project name only from GET /api/deployment; VITE_APP_NAME is removed from the frontend; REGISTRY_REPO becomes required at startup (per CLAUDE.md's loud-failure rule).
  3. 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).
  4. 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).
  5. 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_gitea double 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 when BASE_URL is set to ppe.<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 after 026): create-copy-drop-rename for the 12 tables enumerated in the 026_projects.sql header, folding project_id into each PK/UNIQUE key and adding the project_id FK → projects(id). (cached_prs needs no rebuild — already globally unique.)
  • Additive type + initial_state columns on projects.
  • Restamp (§22.13 step 1): default project id default → config-derived slug, cascaded across every project_id FK. 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_REPO required at startup (loud failure if unset).
  • refresh_registry() in cache.py: read projects.yaml from REGISTRY_REPO via the Gitea API; upsert projects rows (id, name, content_repo, type, visibility, initial_state, enabled_models/themeconfig_json, registry_sha); cache deployment name/tagline. Rows never written from user actions.
  • Webhook branch in webhooks.py on push to REGISTRY_REPO; Reconciler.sweep() calls refresh_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 off VITE_APP_NAME to runtime branding; per-project theme token overlay; deployment directory at /; project switcher.

M3d — Landing-state + review behavior

  • initial_state=active creation path: land a new entry active, stamp unreviewed, skip the graduate gate when the project says so.
  • unreviewed frontmatter fields mirrored into cached_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 → projects rows) on the Docker stub Gitea.
  • M3c — backend functional (/api/deployment, /api/projects/:id, 308 redirects, /p scoping) + 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 (bdd scenario/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-init read 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.