From 87279fc54510e6ded3f302ced548c6b0e9c8f1fc Mon Sep 17 00:00:00 2001 From: Ben Stull Date: Fri, 5 Jun 2026 03:46:51 -0700 Subject: [PATCH] =?UTF-8?q?=C2=A722=20three-tier=20spec=20Part=20E:=20deci?= =?UTF-8?q?ded=20migration-029=20strategy=20(collection=20grain=20beneath?= =?UTF-8?q?=20project)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator chose (session 0072) to add the collection grain beneath today's project: projects table stays the group tier (keeps content_repo), a new collections table holds the per-corpus fields, entries re-key to (collection_id, slug), one default collection per project on migrate, breaking /p//e/ -> /p//c//e/ with 308s. Re-sloted slices N1-N6. Correction banners updated from pending -> decided. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...6-06-05-three-tier-projects-collections.md | 154 ++++++++++-------- 1 file changed, 82 insertions(+), 72 deletions(-) diff --git a/docs/design/2026-06-05-three-tier-projects-collections.md b/docs/design/2026-06-05-three-tier-projects-collections.md index b71d70a..aebdd7d 100644 --- a/docs/design/2026-06-05-three-tier-projects-collections.md +++ b/docs/design/2026-06-05-three-tier-projects-collections.md @@ -19,9 +19,11 @@ > the live `/p//e/` URLs; v0.37.0/0.38.0 shipped per-project > read + propose. Inserting the third tier is therefore an **evolution of a > shipped system**, not a revision of unshipped designs. The migration strategy -> (Part E) is **pending re-decision** with correct facts; the structural model -> (Parts A–D) is unaffected. Target release: a further pre-1.0 minor with -> breaking changes + upgrade steps (§20.2). +> (Part E) was **re-decided on these corrected facts** (session 0072): a new +> **migration 029** adds a *collection* grain *beneath* today's project, plus a +> breaking `/p//e/` → `/p//c//e/` URL +> change with 308s. The structural model (Parts A–D) is unaffected. Target +> release: a further pre-1.0 minor with breaking changes + upgrade steps (§20.2). --- @@ -41,9 +43,9 @@ the deployment level is unchanged. ⚠️ The two-tier model is **already shipped** (v0.39.0): migration 028 rebuilt the slug PK to `(project_id, slug)`, and `/p//e/` URLs are live -(v0.35.0). So inserting the third tier evolves a shipped system — see the -corrected Part E for the real migration cost (a new migration 029 + a breaking -URL change with 308s), pending re-decision. +(v0.35.0). So inserting the third tier evolves a shipped system — see Part E +for the decided strategy (a new migration 029 adding a collection grain beneath +today's project, + a breaking URL change with 308s). --- @@ -479,83 +481,91 @@ Applied in place when §22 is rewritten; listed here as the change surface. # Part E — Revised slicing plan (the roadmap re-slot) -> ⚠️ **PENDING RE-DECISION (session 0072).** The strategy below assumed Plan B -> and M3-frontend were unshipped. They are shipped (v0.35.0–0.39.0). The "fold -> into not-yet-shipped work" framing is void; the actual work is a **new -> migration 029** that evolves the shipped two-tier schema to three tiers, plus -> a **breaking `/p//e/` → `/p//c//e/` -> URL change with 308s**. The two live mapping options (relabel today's -> project → collection + insert a group tier above, vs. add a collection -> sub-grain beneath today's project) are being put back to the operator. The -> text below is retained only as the structural target, not the sequencing. +**Strategy (session 0072, decided on corrected facts).** The two-tier model is +shipped end-to-end (v0.39.0): migration 028 keyed entries `(project_id, slug)`; +v0.35.0 shipped `/p//` routing + live `/p//e/` URLs; +v0.37.0/0.38.0 shipped per-project read + propose. Inserting the third tier is +therefore an **evolution of a shipped system**. The chosen mapping **adds a +collection grain *beneath* today's project** — the shipped `projects` table +stays the grouping tier (it already owns `content_repo`, where §A.2 wants it), +a new `collections` table holds the per-corpus fields, and entries re-key to the +finer `(collection_id, slug)`. The migrated default keeps every deployment +running; the new `/c//` URL segment is added with 308s off the +now-legacy two-tier URLs. -Strategy (ORIGINAL, premise now false): fold the third tier into the -not-yet-shipped work rather than ship two-tier and migrate again. M1, M2, and -M3-backend Plan A are additive and already merged (v0.39.0); they keep running. -The tier goes in at the **first breaking migration (Plan B / 028)** and the -**first public routing (M3-frontend)**, before either ships. +- **Landed, unchanged (v0.39.0):** M1–M2, M3-backend Plan A **and** Plan B + (read+propose, mig 028), M3-frontend (`/p//` routing), §22.13 + re-stamp. None of this is rebuilt; it is *evolved* by migration 029 below. -- **Landed, unchanged (v0.39.0):** M1 (additive `project_id` spine, mig 026), - M2 (authz resolver), M3-backend Plan A (registry mirror, APIs, propose / - mark-reviewed, mig 027). These read fine under the new model: today's single - `projects` row is re-read as the **default collection** after the §E migration - relabels it. +- **N1 — migration 029: add the collection grain.** A new + `029_collections.sql` that: (1) **adds a `collections` table** — + `(id, project_id, type, subfolder, initial_state, visibility, name, + registry_sha)`; (2) **moves the per-corpus fields** (`type`, `initial_state`, + visibility) from the shipped `projects` table **down** to `collections`, + leaving `projects` as `(id, content_repo, visibility, name, tagline, theme, + enabled_models, …)`; (3) **creates one default collection per existing + project** (id `default`, `subfolder` = repo root, inheriting that project's + `type`/`initial_state`); (4) **re-keys every entry-scoped table** from + `(project_id, slug)` to `(collection_id, slug)` via the same SQLite + rebuild procedure migration 028 used (the `028_project_scoped_keys.sql` + pattern — `__new` table, copy, drop, rename, FK-off + `foreign_key_check`), + stamping the default `collection_id`; (5) **generalizes `project_members` → + `memberships(scope_type ∈ {project, collection}, scope_id, …)`**, migrating + existing rows to `scope_type='collection'` on the default collection and + collapsing the role enum to `{owner, contributor}` (§B.3). -- **M3-backend Plan B → "insert the project tier + re-key to per-collection"** - (revised). Migration 028 now: (1) add the `projects` (grouping) table with - `content_repo`; (2) rename the landed `projects` table to `collections`, drop - its `content_repo`, add `project_id` + `subfolder`; (3) re-key every - entry-scoped `project_id` → `collection_id`; (4) the slug-PK rebuild targets - `(collection_id, slug)`; (5) generalize `project_members` → the polymorphic - `memberships` table and collapse the role enum (§B.3); (6) the §22.13 default - project wraps the default collection (§A.6). One breaking migration, one slug - rebuild. +- **N2 — backend collection scoping.** Thread `collection_id` through the + resolution gates that migration 028/Plan B threaded `project_id` through + (`app/auth.py`, `app/projects.py`, `app/cache.py`, the `api_*` writers); + generalize the §22.7 union to the four-layer resolver (§B.2); add the + `GET /api/projects/:id/collections` and + `GET /api/projects/:id/collections/:cid` surfaces; scope propose/serve/PR + endpoints to `(project, collection)`. -- **M3-backend Plan B+ — manifests & in-app create.** Teach the mirror to read - `.collection.yaml`; add the bot-commit-wrapped **create-project** and - **create-collection** endpoints; mirror new collections. +- **N3 — manifests & in-app create.** Teach the registry mirror to read + `.collection.yaml` from each project's content repo; add the + bot-commit-wrapped **create-collection** (project Owner / RFC Contributor) + and **create-project** (global Owner) endpoints (§A.2); mirror new + collections into `collections` rows. -- **M3-frontend → three-tier routing.** `/p//c//e/`; - the deployment directory at `/`, the project (collection) directory at - `/p//`; the single-project / single-collection redirects (§A.3); the - 308s off `/rfc/`; runtime branding at both project and collection. +- **N4 — frontend three-tier routing.** Add the `/c//` segment: + `/p//c//e/`; the project (collection) directory at + `/p//`; the single-collection redirect (§A.3); **308** the shipped + `/p//e/` → `/p//c//e/`; per-collection + chrome layered under the existing per-project chrome. -- **M4 — second-collection + second-project acceptance.** End-to-end pass: a - second collection in a project, and a second project, each with disjoint - membership, full propose→graduate lifecycle under the per-collection slug - namespace. +- **N5 — roles surface (this pass's scope).** The `{owner, contributor}` × + `{global, project, collection}` grants (Part B) with the invite surfaces and + empty states of Part C as acceptance tests. (Richer roles deferred, §B.1.) -- **M5 — type modules (unchanged in intent).** Per-type frontmatter + surfaces, - now selected on the **collection's** `type`. +- **N6 — type modules / membership lifecycle / hardening.** Carries the original + §22 M5–M7 forward, now selecting on the **collection's** `type`: per-type + frontmatter + surfaces; request-to-join + cross-collection inbox; per-collection + `enabled_models`; the registry + manifest format in `docs/DEPLOYMENTS.md`; + two-project / multi-collection e2e; the §20.4 changelog + upgrade-steps; the + SPEC merge (Part A applied, Part D amendments in place). -- **M6 — membership lifecycle.** Invite + request-to-join at all three scopes - (Part C.2); the cross-collection inbox; the roster admin surface. The Part C - scenarios are this slice's acceptance tests. +## E.1 (= §A.6) Migration — the default collection (the N=1 case) -- **M7 — hardening + SPEC merge.** Per-collection `enabled_models`; the registry - + manifest format in `docs/DEPLOYMENTS.md`; two-project / multi-collection e2e; - the §20.4 changelog + upgrade-steps; the SPEC merge (Part A applied, Part D - amendments in place). +A deployment on the shipped two-tier schema (v0.39.0) is migrated by 029 so it +keeps running unchanged: -## E.1 (= §A.6) Migration — the default project and default collection +1. The existing `projects` row **stays as the project** (it already owns + `content_repo` and its config-derived `id` from §22.13 step 1). +2. A **default collection** (`id='default'`, `subfolder` = repo root) is created + per project, inheriting that project's `type` / `initial_state` / visibility; + those fields are then dropped from `projects`. +3. Every entry-scoped `project_id` row is re-keyed with the default + `collection_id` (PK `(project_id, slug)` → `(collection_id, slug)`). +4. `project_members` rows migrate to `memberships(scope_type='collection')` on + the default collection, role-collapsed (§B.3). +5. **308 redirects:** the shipped `/p//e/` → + `/p//c//e/`, and the pre-multi-project `/rfc/` + / `/proposals/` → their `/p//c//…` equivalents. -A pre-three-tier deployment (already on v0.39.0's single `projects` row, or -pre-multi-project) is migrated so it keeps running: - -1. The landed single `projects` row becomes the **default collection** - (`type=document`, its `initial_state`/`unreviewed` preserved). -2. A **default project** is generated to wrap it: `content_repo` moves from the - (old project / now collection) onto the project; `id` is the config-derived - slug already re-stamped in §22.13 step 1 (v0.39.0). -3. Every entry-scoped `project_id` is re-keyed to the default `collection_id`. -4. M2's `project_members` rows migrate to `memberships(scope_type='collection')` - on the default collection, role-collapsed (§B.3). -5. `/rfc/` and `/proposals/` 308-redirect to - `/p//c//…`. - -Until a second project or collection is added, the deployment is functionally -identical to before, with two extra path segments. This is the §20.4 -upgrade-steps content for the release. +Until a second collection is added, the deployment is functionally identical to +before, with one extra path segment. This is the §20.4 upgrade-steps content for +the release. ## E.2 Scope of the first implementation pass