# 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/<project>/e/<slug>→
> /p/<project>/c/<default>/e/<slug>. 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/<p>/e/<slug>` 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/<collection>/` 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/<project>/e/<slug>`, `/rfc/<slug>`, `/proposals/<n>` 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/<project>/; and
collection-scoped propose/serve under /p/<project>/c/<collection>/. 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.
```
