§22 three-tier spec Part E: decided migration-029 strategy (collection grain beneath project)
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/<project>/e/<slug> -> /p/<project>/c/<collection>/e/<slug> with 308s. Re-sloted slices N1-N6. Correction banners updated from pending -> decided. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,9 +19,11 @@
|
||||
> the live `/p/<project>/e/<slug>` 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/<project>/e/<slug>` → `/p/<project>/c/<collection>/e/<slug>` 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/<project>/e/<slug>` 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/<project>/e/<slug>` → `/p/<project>/c/<collection>/e/<slug>`
|
||||
> 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/<project>/` routing + live `/p/<project>/e/<slug>` 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/<collection>/` 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/<project>/` 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/<project>/c/<collection>/e/<slug>`;
|
||||
the deployment directory at `/`, the project (collection) directory at
|
||||
`/p/<project>/`; the single-project / single-collection redirects (§A.3); the
|
||||
308s off `/rfc/<slug>`; runtime branding at both project and collection.
|
||||
- **N4 — frontend three-tier routing.** Add the `/c/<collection>/` segment:
|
||||
`/p/<project>/c/<collection>/e/<slug>`; the project (collection) directory at
|
||||
`/p/<project>/`; the single-collection redirect (§A.3); **308** the shipped
|
||||
`/p/<project>/e/<slug>` → `/p/<project>/c/<default>/e/<slug>`; 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/<project>/e/<slug>` →
|
||||
`/p/<project>/c/<default>/e/<slug>`, and the pre-multi-project `/rfc/<slug>`
|
||||
/ `/proposals/<n>` → their `/p/<project>/c/<default>/…` 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/<slug>` and `/proposals/<n>` 308-redirect to
|
||||
`/p/<default-project>/c/<default-collection>/…`.
|
||||
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user