Files
session-history/0065/SESSION-0065.0-TRANSCRIPT-2026-06-03T02-30--2026-06-03T05-12.md
T

233 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Session 0065.0 — Transcript
> Date: 2026-06-03
> Goal: Build **M2** of the rfc-app §22 multi-project series — project-scoped
> authorization + the §22.7 resolver — then finalize (merge to main, publish).
>
> Outcome: **M2 shipped. The §22.7 three-tier resolver + §22.5 visibility gate
> landed across all eight API modules, behavior-preserving on the public default
> project (full suite 401 green). Merged to `main` on top of v0.32.0 (retire)
> that had advanced main meanwhile — required a migration renumber and a
> `get_rfc` conflict resolution. `feat/multi-project` deleted; no release/deploy
> (the §22 series releases as one minor at M7).**
>
> Highlights:
> - Two operator design decisions settled mid-build (see Arc 2): *implicit-on-
> public* and *preserve-curation*. Both are a deliberate, documented
> liberalization of §22.5's literal "grant required on public," chosen to keep
> the N=1 case whole and the v0.16.0 per-RFC invite contract intact.
> - A latent **migration-numbering collision** surfaced at merge time: M1's
> `025_projects.sql` vs v0.32.0's `025_retired_state.sql`. See §19.2 candidate 1.
---
## Pre-session state
- **rfc-app** (`~/git/wiggleverse.org/ben.stull/rfc-app`, origin =
`git.wiggleverse.org:ben.stull/rfc-app`): branch `feat/multi-project` carried
the §22 design drafts + **M1** (the schema spine, commit `6f901e3`:
`projects` / `project_members` tables, `project_id` on all 19 slug-bearing
tables, migration `025_projects.sql`). `main` was at `0062510` *locally* but
had in fact advanced on origin (see Arc 3). The §22 slicing plan (M1M7,
`docs/design/multi-project-spec.md` Part C) targets **one pre-1.0 minor at
M7**; intermediate slices accumulate on the feature branch and bump nothing.
- Backend: FastAPI + SQLite (`backend/`), test suite `cd backend && python -m
pytest -q` via the local `.venv`. Baseline at session start: **381 passed**.
- OHM live: untouched this session (no deploy, no pin bump).
---
## Turn-by-turn arc
### Arc 1 — Locating the work
"Build RFC App M2" was terse. Traced it from `ohm-infra` references → the
rfc-app framework → `docs/design/multi-project-spec.md` §22, Part C slicing
plan. M1 was confirmed landed on `feat/multi-project`; **M2** = "Project-scoped
authorization + the §22.7 resolver." Surveyed the existing auth machinery
(`app/auth.py`: deployment roles §6.1, per-RFC helpers §6.3/§12), the M1 schema
(migration 025), and — via an Explore subagent — a full inventory of every
RFC-scoped endpoint and its current gate. Key architectural find: every
RFC-scoped endpoint funnels through a `_require_rfc*()` helper that does
`SELECT ... FROM cached_rfcs WHERE slug = ?`, and M1 already put `project_id` on
`cached_rfcs` — a single, uniform hook point for both the resolver and the
visibility gate.
### Arc 2 — Two design forks (operator decisions)
Entered plan mode. Two genuine forks surfaced that change behavior and so went
to the operator via AskUserQuestion:
1. **Public-write gate.** §22.5/§22.7 literally require an explicit
`project_contributor` grant to write *even on a public project*. But the live
default project is public and the membership UI isn't due until M6 — a
literal reading would break onboarding. **Decision: implicit-on-public** — a
granted deployment contributor keeps its pre-M2 write baseline on a public
project with no membership row (no backfill; N=1 stays whole).
2. **Per-RFC curation.** §22.7 says project roles are *supersets* of per-RFC
roles, which — combined with implicit-on-public — would erase the v0.16.0
per-RFC discussion/contribution invite curation on the public project (and
break 4 shipped tests). **Decision: preserve-curation** — the implicit-public
baseline does NOT override per-RFC owner curation; only an *explicit* project
grant (or deployment owner/admin) does.
Plan approved. These two decisions are what make M2 behavior-preserving on the
public default project while still fully implementing the tier for
gated/unlisted projects and explicit grants.
### Arc 3 — Implementation
Pure app-layer (no migration — M1 shipped the tables; no version bump —
mid-feature-branch).
- **`app/auth.py`** — the §22.7 resolver primitives: `project_visibility`,
`project_member_role`, `project_of_rfc`, `is_project_superuser`,
`can_read_project`, `can_contribute_in_project` / `can_discuss_in_project`
(with the implicit-public `_has_write_baseline`), `require_project_readable`,
`visible_project_ids`. The three existing per-RFC helpers (`can_discuss_rfc` /
`can_contribute_to_rfc` / `can_invite_to_rfc`) were rewritten to compose all
three tiers (signatures unchanged, so ~20 call sites inherited M2 for free),
with the old "no owners yet → open" fallthrough replaced by the
curation-preserving baseline.
- **Read pass** — threaded `viewer` + `require_project_readable` into every
`_require_rfc*` / `_require_super_draft` / `_require_rfc_readable` across
`api.py`, `api_branches`, `api_prs`, `api_graduation`, `api_discussion`,
`api_contributions`, `api_invitations`; filtered the catalog (`GET /api/rfcs`)
and proposals listings by `visible_project_ids`. A gated project's entries now
404 to non-members (indistinguishable from absent, §22.5).
- **Write pass** — folded `is_project_superuser` (project_admin) into the deep
gates (`_can_read_branch`, `_can_contribute`, `_require_branch_owner`,
`_can_resolve_thread`, `_can_edit_metadata`, `_capabilities`, `_can_merge`,
`_can_withdraw`, `_can_graduate`, discussion `_can_resolve`); routed the
branch "any-contributor" mode through `can_contribute_in_project`; gated
`propose` + `claim` on project contribute standing. A self-correction mid-pass:
initially added `can_contribute_to_rfc` to `pr-draft`/`review`/
`resolution-branch`, then reverted it — those were open to any contributor
pre-M2, so per-RFC gating there would *tighten* public behavior; the threaded
visibility gate is the correct M2 change for them. Deployment-level surfaces
(admin idea-PR merge/decline, account notification-mute) left deployment-
scoped after audit.
- **Tests** — `backend/tests/test_multi_project_authz_vertical.py` (9 vertical
assertions: resolver tiers, public-unchanged regression, gated 404 read gate,
gated contribution gate, union + subtractive visibility, revocation).
- **Docs** — Part C M2 marked "(landed)" with both operator decisions recorded.
Result: **390 passed** (381 + 9). Committed as `503689b`
("feat(projects): M2 — project-scoped authorization + the §22.7 resolver").
### Arc 4 — Finalize round 1: the diverged-main surprise
Operator: "you can push/merge." Survey revealed `origin/main` had **advanced**
to `34a65e0` (v0.32.0 — the `retired` soft-delete state + §13 retire/unretire),
3 commits the branch didn't have; local `main` was stale. A trial merge
(`git merge-tree`) reported:
1. A content conflict in `app/api.py` (`get_rfc` — both sides added
`request: Request` and top-of-handler logic).
2. A **migration-number collision**: M1's `025_projects.sql` vs v0.32.0's
`025_retired_state.sql`. Worse than a name clash — `025_retired_state.sql`
*rebuilds* `cached_rfcs` (create-copy-drop-rename to widen a CHECK
constraint) and predates `project_id`, so on a fresh DB (filename-sorted
order: `projects` < `retired`) the rebuild would **drop `project_id`**,
breaking every M2 gate.
Surfaced this to the operator (who confirmed single-user, land it on main).
Resolution, all on `main` via a `--no-ff` merge:
- Renamed **`025_projects.sql` → `026_projects.sql`** so projects' `ADD COLUMN`
runs *after* the retire rebuild (the runner applies `*.sql` in filename order,
tracked in `schema_migrations`); bumped the comment/doc references.
- Resolved `get_rfc`: compute `viewer` once, apply the §22.5 visibility gate,
then v0.32.0's §13.7 retired-entry owner-only check.
- `api_graduation.py` / `api_discussion.py` auto-merged cleanly (M2's threaded
viewer/visibility coexists with the new retire endpoints + state).
Full suite on the merged tree (fresh DB — exercises the migration order):
**401 passed**. Merge committed `1dab24e`; pushed `main` + `feat/multi-project`
to origin.
### Arc 5 — Finalize round 2: housekeeping
Ran the `wgl-coding-session-finalize` flow. Operator chose: delete
`feat/multi-project` (local + origin — fully contained in main; M3 branches
fresh) and leave the secondary `benstull` remote alone. Done. Updated the
project memory note (`project_rfc_app_multiproject.md`) and refreshed it for the
deleted branch.
---
## Cut state (end of session)
| | |
| --- | --- |
| rfc-app | `main` @ `1dab24e` (merge: §22 M1+M2 over v0.32.0), pushed to origin. M2 = `503689b`. Working tree clean. |
| rfc-app branches | `feat/multi-project` deleted (local + origin). |
| Test suite | `backend` 401 passed (fresh DB). |
| Version / release | None — §22 releases as one minor at M7; no `VERSION`/`CHANGELOG` change, no tag. |
| OHM live | Untouched (no deploy this session). |
| ohm-rfc pin | Unchanged. |
| §22 slice ledger | Status |
| --- | --- |
| M1 — schema spine | ✅ landed (on main via merge) |
| M2 — project-scoped authz + §22.7 resolver | ✅ shipped this session |
| M3 — registry mirror + routing + runtime branding | ⏭ next |
| M4M7 | ⏸ pending |
---
## §19.2 candidates surfaced
1. **Migration-number collisions across parallel branches.** M1 and v0.32.0 both
grabbed `025_*`; only a manual rename at merge time kept a fresh DB working.
The numbering scheme is collision-prone whenever two branches add migrations
concurrently. Worth a convention (reserve ranges per workstream, or a
monotonic claim like the session-ID gesture) so the next collision isn't
silent. The immediate guard: **new migrations must be ≥ 027.**
2. **Fold the M2 authz decisions into SPEC at M7.** *implicit-on-public* and
*preserve-curation* currently live only in `docs/design/multi-project-spec.md`
Part C + `app/auth.py` comments. They are deliberate liberalizations of
§22.5's literal text and must be reconciled into the binding SPEC §22 at the
M7 merge, not left as drift.
3. **Per-endpoint contributor-vs-viewer granularity.** `pr-draft`/`review`/
`resolution-branch` are visibility-gated but not contribute-gated (preserving
public behavior). On a gated project a project_viewer can therefore reach
them. Acceptable for M2; revisit in the M4 acceptance pass if it matters.
---
## What lands on the operator's plate
- Nothing blocking. M2 is on `main` and pushed; no secret, deploy, or gitea
admin gesture was required or left open.
- Optional: the `sessions.json` title for 0065 (set by this session at publish).
- When M3 starts: branch fresh from `main` (it now contains M1+M2+v0.32.0).
---
## Prompt the operator can paste into the next Claude Code session
```
Build RFC App M3 (multi-project: registry mirror + routing + runtime branding).
This session reserves its own ID — don't trust any number in this prompt.
Context: ~/git/wiggleverse.org/ben.stull/rfc-app (origin = git.wiggleverse.org,
SSH). §22 M1+M2 are merged to main (see ~/.claude memory
project_rfc_app_multiproject.md). Read that note, then docs/design/
multi-project-spec.md Part C "M3" + §22.2/§22.9/§22.10/§22.13. Branch fresh
from main.
M3 covers: the registry mirror (webhook + reconciler over REGISTRY_REPO
populating `projects`), the default-project bootstrap-id re-stamp (default →
config-derived slug, before any /p/<id>/ URL is public), the /p/<project>/ route
prefix with generic /e/<slug> + 308 redirects off the old corpus-root URLs, GET
/api/deployment + /api/projects/:id, the frontend cut from VITE_APP_NAME to
runtime config + per-project theme, the deployment directory + project switcher,
AND the deferred slug-keyed uniqueness/PK rebuilds enumerated in
026_projects.sql's header (must land before a 2nd project can collide).
New migrations must be numbered >= 027 (M1's projects migration is 026 after a
collision with v0.32.0's 025_retired_state). Tests: cd backend && python -m
pytest -q via the .venv there. Plan before building.
```