# Session 0074.0 — Transcript > App: ohm > Start: 2026-06-05T07-35 (PST) > End: 2026-06-05T08-35 (PST) > Type: coding > Status: **FINALIZED.** > Branch: `feat/s1-collection-grain` → PR #19 (merged to `main`). Released **v0.40.0**. ## Launch prompt > `/goal` Write + execute the S1 plan for the §22 three-tier refactor: migration 029 > (add `collections` table, move per-corpus fields down from `projects`, one default > collection per project, re-key entry-scoped tables (project_id,slug)→(collection_id, > slug) via the 028 rebuild pattern, project_members→memberships) + thread collection_id > through app/auth.py·projects.py·cache.py·api_* + 308 /p//e/→ > /p//c//e/. Acceptance: @S1 scenarios green; N=1 unchanged. > Read docs/design/2026-06-05-three-tier-projects-collections.md (Part E / §A.6) first. > Review/merge PR #18 first if approved. ## Plan Coding session executing slice **S1** of the §22 three-tier refactor (design doc spec'd in session 0072). Write a written implementation plan, then execute it via TDD against the full backend suite as the "N=1 unchanged" regression net. Gate: review/merge the spec PR #18 first. ## Pre-state - Clean, pushed `main` @ **v0.39.0** (two-tier deployment→project model shipped). - The three-tier design doc was on branch `spec/three-tier-projects-collections`, open as **PR #18 (unmerged)** on `git.wiggleverse.org/ben.stull/rfc-app`. - Memory flagged PR #18 as open and "NEXT = write+execute the S1 plan." ## The arc (turn-by-turn) 1. **Session gate / init.** Classified as a **coding** session; claimed ID **0074** (`--type coding`), no concurrent sessions, clean `main` baseline. 2. **PR #18 triage.** The memory's "PR #18" wasn't on the `benstull` remote (`git.benstull.org`) — found it on the canonical `origin` (`git.wiggleverse.org/ben.stull/rfc-app`, PR #18 open + mergeable). Reviewed the 637-line design doc in full: internally consistent, stale-memory premise corrected (two-tier shipped @ v0.39.0), migration-029 strategy decided, slices `@S1`-tagged. **Verdict: approved** → merged PR #18 to `main` (the design doc is now on main; the SPEC.md merge itself rides S6). 3. **Codebase exploration.** Read migration 028 (the rebuild pattern to replicate), 026/027 (current `projects`/`project_members` schema; `type`/`initial_state` live on `projects`), `projects.py`/`auth.py`/`cache.py`/`api_deployment.py`/`App.jsx`. Three parallel Explore agents mapped the `project_id` blast radius, the routing/308 surface, and the test/migration tooling (key finding: **no Gherkin runner** — `@S1` is realised as pytest verticals; the full suite is the N=1 net). 4. **Decision: default collection id = literal `'default'`.** Established that a stable literal (not the project id) keeps the collection id identical across fresh-vs- upgraded deploy histories, matches the spec's `/c/default/` URLs, and lets the existing `restamp` keep renaming only the *project* grain. The re-key maps each entry to its project's default collection via a JOIN (robust to project_id values). 5. **Wrote the S1 plan** (`docs/superpowers/plans/2026-06-05-s1-three-tier-collection-grain.md`) via the writing-plans skill — phased: migration 029 → backend threading → API surface → redirects → `@S1` acceptance → release. 6. **Executed (TDD, branch `feat/s1-collection-grain`).** - **Migration 029** (`029_collections.sql`): `collections` table; `type`/ `initial_state` moved down off `projects`; one default collection per project (`id='default'`, repo root); the 13 entry-corpus tables re-keyed `(project_id,slug)`→`(collection_id,slug)` via the 028 rebuild pattern; `project_members`→`memberships(scope_type∈{project,collection},…)`, role enum collapsed to `{owner,contributor}`. Shape test green first. - **Threading.** Renaming the column broke the whole suite (the intended TDD signal). Fixed module-by-module against the regression net: `collections.py` helpers; the registry mirror (writes project group-fields + collection corpus-fields, type immutability on the collection); `auth.project_of_rfc` (collections join); `auth.project_member_role` (reads memberships, maps back to legacy role strings); `cache`/`api.py`/`api_prs`/`api_branches`/`api_notifications`/`api_contributions`/ `api_invitations`/`api_graduation`/`api_discussion`/`funder` (13-table reads/writes → `collection_id`; `cached_prs` + denormalised `project_id` tags left unchanged); `projects.py` restamp + initial_state; `api_deployment` reads type/initial_state from the default collection. A missed `project_of_rfc` edit (500 on every entry read) was the single biggest fix — caught by the suite. - **Redirects.** Backend `/rfc/`+`/proposals/` 308s now target `/c/default/`. Frontend: `/c/:collectionId/` route layer; `DefaultCollectionRedirect` (C3.7); `LegacyCorpusRedirect` (v0.35.0 `/p/

/e/` bookmarks → `/c/default/`, query preserved); path builders carry the segment (serving stays project-scoped — collection=default; S2 owns collection-aware serving). - **Tests.** Added `test_migration_029_collections.py`, `test_s1_collection_grain_vertical.py` (`@S1`), `entryPaths.test.js`. Updated the fixtures/assertions that seeded the dropped columns (`test_api_deployment`, `test_registry`, `test_registry_wiring`, `test_migration_027`, `test_initial_state_landing`, `test_restamp_default_project`, the multi-project + project-scoped verticals). **Retired `test_migration_028`** (its keying is superseded by 029; invariants now covered at the collection grain). 7. **Verification + release.** Backend **458 passed**, frontend **18 passed**, build green (the N=1-unchanged gate + `@S1`). Bumped **VERSION/package.json → 0.40.0** (parity checked), wrote the CHANGELOG entry with a breaking-URL upgrade-steps block. Pushed; opened **PR #19**; self-reviewed (fully tested) and merged to `main`; deleted the branch. ## Cut state - `main` @ **v0.40.0**, clean and pushed. Migration `029_collections.sql` on `main`. - Slice **S1 shipped**: the collection grain exists as the invisible default; N=1 deployments run unchanged with one extra `/c//` path segment. - Full backend suite 458 green; frontend 18 green + build green. ## Operator plate (what shipped) - The three-tier model (deployment → project → RFC collection) now has its collection grain in code. A deployment upgrading 0.39.0 → 0.40.0 runs migration 029 automatically (no config change); existing entries re-key into the default collection with no data loss; old `/p//e/`, `/rfc/`, `/proposals/` links keep working. ## Deferred decisions - **Default collection id = literal `'default'`** (not the project id): chosen for cross-deploy-history stability + spec-URL fidelity; the multi-project-at-migration case uses a `CASE` fallback to the project id to avoid a PK collision (non-standard pre-S5). Worth a glance from the spec author, but it honors the spec's structural invariants. - **`test_migration_028` retired** rather than rewritten — its `(project_id,slug)` keying is overwritten by 029, so the test asserted a state the runner can no longer reach; the collection-grain invariants live in `test_migration_029`. - Denormalised `project_id` tags (`cached_prs`/`threads`/`changes`/`notifications`/`actions`/ `pr_resolution_branches`) intentionally left as `project_id` (project grain) for S1. ## Next-session prompt ``` /goal Execute slice S2 of the §22 three-tier refactor (create & navigate a second collection): teach the registry mirror to read `.collection.yaml` manifests; add the bot-commit-wrapped create-collection endpoint (authorized by existing deployment owner/admin for now); the project collection-directory at /p//; and collection-scoped propose/serve under /p//c//. Acceptance: @S2 scenario (C3.6, anonymous reader of an empty collection catalog) green; N=1 unchanged. Read docs/design/2026-06-05-three-tier-projects-collections.md (Part E "S2") first. ```