feat(projects): M1 — the multi-project schema spine (§22)

Lays the additive foundation for hosting N projects per deployment, with
today's single corpus as the N=1 case. No behavior change: the app runs
exactly as before, single project, with the spine underneath.

- migration 025: `projects` + `project_members` tables; seed the `default`
  project (visibility=public, preserving open-by-default); thread
  `project_id NOT NULL DEFAULT 'default'` onto all 19 slug-bearing tables,
  backfilling existing rows. Additive — no table rebuilds; the slug-keyed
  uniqueness/PK rework is enumerated in the migration header and deferred to
  the slice that activates project #2.
- app/projects.py: §22.13 startup backfill of the default project's
  content_repo from META_REPO (idempotent — never clobbers a value the
  future registry mirror sets).
- config: REGISTRY_REPO wired (optional through M1; consumed by the M3
  mirror), documented in .env.example as META_REPO's successor.
- tests: 6 vertical assertions on the spine (seed, backfill, column shape,
  role CHECK, idempotency, config). Full suite 381 passed.
- docs: align Part C M1/M3 boundaries with the landed code (registry mirror
  + redirect move to M3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-01 18:14:53 -07:00
parent 7aba89655d
commit 6f901e3e2c
7 changed files with 318 additions and 13 deletions
+9
View File
@@ -14,6 +14,15 @@ GITEA_BOT_TOKEN=
GITEA_ORG=wiggleverse
META_REPO=meta
# --- Multi-project registry (§22) ---
# The registry repo (under GITEA_ORG) the framework reads to learn which
# projects this deployment hosts. Optional through Slice M1: the registry
# mirror lands in M3, and until then the single default project's content
# repo is taken from META_REPO above. The repo's name is the deployment's
# choice. When the mirror lands, REGISTRY_REPO becomes required and
# supersedes META_REPO.
# REGISTRY_REPO=wiggleverse-registry
# --- OAuth (Gitea) ---
# In Gitea: Site Administration → Applications → Add OAuth2 Application.
# Redirect URI: {APP_URL}/auth/callback