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>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
# 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
|
||||
4. **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.
|
||||
5. **Branding cutover** — **hard 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).
|
||||
6. **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).
|
||||
7. **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).
|
||||
8. **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`/`theme` → `config_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.
|
||||
Reference in New Issue
Block a user