diff --git a/ohm/0076/SESSION-0076.0-TRANSCRIPT-2026-06-05T17-25--INPROGRESS.md b/ohm/0076/SESSION-0076.0-TRANSCRIPT-2026-06-05T17-25--INPROGRESS.md index ef984b8..c129098 100644 --- a/ohm/0076/SESSION-0076.0-TRANSCRIPT-2026-06-05T17-25--INPROGRESS.md +++ b/ohm/0076/SESSION-0076.0-TRANSCRIPT-2026-06-05T17-25--INPROGRESS.md @@ -2,19 +2,171 @@ > App: ohm > Start: 2026-06-05T17-25 (PST) +> End: 2026-06-05T18-13 (PST) > Type: coding -> Status: **PLACEHOLDER — claimed at session start; finalized at session end.** +> Status: **FINALIZED.** > -> This file reserves session ID 0076 for ohm. The driver replaces this -> body with the full transcript and renames the file to its final -> SESSION-0076.0-TRANSCRIPT-2026-06-05T17-25--.md form at session end. +> Driver session — shipped framework release **v0.42.0** (§22 slice S3). ## Launch prompt -_(launch prompt not captured at claim time)_ +`/goal S3` — implement §22 slice S3 (scope-role enforcement, `@S3` = +C1.1–C1.8), per `docs/design/2026-06-05-three-tier-projects-collections.md` +Part E. Operator added two requirements mid-session: + +1. Collection-level visibility is settable (public or not); a collection + can be "hidden from existence from public" yet visible to contributors + at the project or global scope. +2. Project scope likewise settable; a collection's visibility may only be + set as strict or stricter than its project's. + +## Pre-state + +`main` at `39ce54f` (S2 / v0.41.0, three mains in sync: origin = benstull = +local). Two-tier shipped @ v0.39.0; S1 (collection grain, mig 029) @ v0.40.0; +S2 (create+navigate a 2nd collection) @ v0.41.0. Backend FastAPI+SQLite under +`backend/`; the §22 three-tier draft spec at +`docs/design/2026-06-05-three-tier-projects-collections.md` (Parts A–E, +`@S`-tagged scenarios). S3 was the next slice: the §B.2 four-layer +most-permissive scope-role resolver, every write gate re-checked on the +collection axis. + +## Plan + +**S3 — scope-role enforcement + collection-grain visibility.** One cohesive +slice (visibility enforcement and the scope resolver share the authz surface). + +- **Visibility model.** Rank `public(0) < unlisted(1) < gated(2)`. A + collection's visibility must be `rank ≥` its project's (validated at + create + mirror). Effective read gate = stricter of the two. `gated` = + "hidden from public existence": 404 to anonymous / non-scope-role, omitted + from the directory, but readable + listed for scope-role holders. +- **Scope-role resolver (§B.2).** Four-layer most-permissive union: global + (deployment owner/admin → Owner; explicit `scope_type='global'` grant) → + project membership → collection membership → per-entry. No negative override. +- **Enforcement.** `can_read_collection` / `can_contribute_in_collection`; + viewer-aware directory enumeration; every RFC-grain write helper re-pointed + at the entry's collection; create-collection authorized by project-scope + Owner; implicit-public baseline grandfathered onto the `default` collection + only (N=1 preserved). +- **Schema.** Migration 030: `memberships.scope_type += 'global'`. +- **Tests.** Part C.1 scenarios (`@S3`) + collection-visibility + strictness. + +## Session arc + +1. **Init.** Claimed session 0076 (no other in-flight). Verified clean pushed + `main` baseline. Read design doc Parts A–E (three-tier model, unified roles + §B, the C.1 `@S3` scenarios, the slicing plan). + +2. **Explore.** Two parallel Explore agents mapped (a) the test harness — S1/S2 + wired as pytest vertical tests, S3 scenarios still prose; the gate runs `cd + backend && pytest`; fixtures `app_with_fake_gitea` / `sign_in_as` / + `provision_user_row`; (b) every authz call site and write gate. + +3. **Keystone contradiction found.** §B.1/§B.3 ("deployment contributor = + global RFC Contributor → writes everywhere") contradicts the C.1 "hana" + scenario ("granted account, no scope role → proposes nowhere") AND the M2 + implicit-public baseline (`test_multi_project_authz_vertical.py:138`, the + live OHM deployment depends on it). Surfaced the three-way conflict and asked + the operator for the keystone (a binding authz/security call the doc didn't + resolve). + +4. **Operator clarification.** Operator declined the multiple-choice and gave + direction in their own words: collection-grain visibility is the primary + control — a collection can be public or "hidden from existence from public" + (visible to project/global-scope contributors), and a collection's + visibility may only be set as strict or stricter than its project's. This + confirmed the scope-role-primary reading and resolved the keystone: a plain + granted account is a granted *account*, not a write-everywhere global role; + "global RFC Contributor" = an explicit `scope_type='global'` grant; the + implicit-public write baseline is grandfathered onto the migration-seeded + `default` collection only (N=1 preserved). + +5. **Implement (TDD-ish, suite green at every step).** + - Migration `030_global_scope.sql` (rebuild memberships, `scope_type += + 'global'`) + `test_migration_030_global_scope.py`. + - `auth.py`: `effective_scope_role`, `collection_visibility`, + `effective_collection_visibility`, `can_read_collection` / + `require_collection_readable`, `is_collection_superuser`, + `_has_collection_write_baseline`, `can_contribute_in_collection` / + `can_discuss_in_collection`, `can_create_collection`, `collection_of_rfc`, + `visibility_rank`; generalized `can_read_project` to recognize scope roles; + re-pointed `can_discuss_rfc` / `can_contribute_to_rfc` / `can_invite_to_rfc` + to the collection axis; fixed `project_member_role` (global+project scope, + `default`-collection back-compat only on the real default project via + `_is_default_project` — the old `default_collection_id` conflation misfired + for multi-collection projects). + - Re-pointed entry-scoped superuser checks to the collection grain across + `api.py` (propose gate, mark-reviewed, collection read routes + + `require_collection_readable`), `api_branches.py`, `api_graduation.py`, + `api_prs.py`, `api_discussion.py`, `api_contributions.py`. + - `api_collections.py`: viewer-aware directory enumeration; `get_col` read + gate; create-collection authority widened (§B.1, `can_create_collection`) + + visibility strictness validation (422 on looser). + - `registry.py`: `_strictest_visibility` clamp at the named-collection mirror. + - Updated two S2 tests to grant explicit scope roles (the new write contract). + - New `test_s3_scope_roles_vertical.py`: all 8 C.1 scenarios (ada/ben/cleo/ + dan/eve/fay/gil/hana) + hidden-collection + strictness + create-authority. + - **Full backend suite: 493 passed.** + +6. **Release.** VERSION + package.json → 0.42.0; CHANGELOG 0.42.0 entry + (breaking, with upgrade steps); design doc Part E marks S3 shipped + records + the keystone reconciliation. Committed (`c2f5665`). + +7. **PR + merge (with a recovered slip).** `git remote -v | head -2` truncated + the remote list and hid `origin`, so the branch was pushed and PR #1 merged + on the **`benstull`** mirror (git.benstull.org) rather than canonical + **`origin`** (git.wiggleverse.org). Caught at finalize; recovered by + fast-forwarding `origin/main` to the same commit (`e6bd69f`). End state: + origin == benstull == local `main`, all at v0.42.0. + +## Cut state + +- **Shipped: v0.42.0**, `main` at `e6bd69f` (origin = benstull = local, all + aligned). S3 / `@S3` (C1.1–C1.8) complete; full backend suite 493 passed. +- Branch `feat/s3-scope-roles-collection-visibility` merged + deleted. +- Frontend untouched (backend-enforcement slice; hidden collections simply + don't appear in API responses the frontend renders). +- **NEXT = S4** — invitation surfaces + role-aware empty states (`@S4` = + C2.1–C2.7 + C3.3–C3.5). An in-app *edit*-visibility control (post-create) is + a natural S4 add; S3 ships visibility as git-truth + create-time setting + + enforcement. ## Deferred decisions -_Autonomous-mode low-confidence calls the driver made and would have -liked operator input on. Appended as the session runs; surfaced at -finalize. Empty if none._ +- **Keystone authz interpretation (low confidence; operator engaged + mid-session and confirmed the visibility half).** The design doc's §B.1/§B.3 + ("deployment contributor = global RFC Contributor → writes everywhere") + contradicts the C.1 "hana" scenario ("granted account, no scope role → + proposes nowhere") and the locked-in M2 implicit-public baseline. Resolved + as: a plain granted account (`role='contributor'`, no membership) is **a + granted account, not a write-everywhere global role**; "global RFC + Contributor" (cleo) is an **explicit** `scope_type='global'` grant; the + implicit-public write baseline is **grandfathered onto the migration-seeded + `default` collection only** so N=1 OHM loses no capability. Operator's + follow-up (collection-grain visibility, hidden=gated, strictness constraint) + confirmed the scope-role-primary reading. Reinterprets §B.1/§B.3 literally — + flag for SPEC merge (S6). +- **PR target slip (recovered, no harm).** `git remote -v | head -2` truncated + the remote list and hid `origin`, so the S3 branch was pushed and the PR + (#1) merged on the **`benstull`** mirror (git.benstull.org) rather than the + canonical **`origin`** (git.wiggleverse.org:ben.stull/rfc-app) where the S1/S2 + PRs (#18/#20) landed. Caught at finalize; recovered by fast-forwarding + `origin/main` to the same commit (`e6bd69f`). End state: `origin` == + `benstull` == local `main`, all at v0.42.0 — fully consistent, non-destructive. + The S3 PR record lives on benstull #1, not origin. Lesson: never `head`-truncate + `git remote -v` when the canonical target matters. + +## Next-session prompt + +``` +/goal S4 — invitation surfaces + role-aware empty states (@S4 = C2.1–C2.7, +C3.3–C3.5), per docs/design/2026-06-05-three-tier-projects-collections.md Part E +``` + +Read `memory/project_rfc_app_multiproject.md` (session-0076 block) first. S4 +builds the Owner-only invite UI (grant Owner/RFC Contributor at a scope or any +scope beneath it, §15 notifications, broader-scope-supersedes) on top of the S3 +`memberships` table + `auth.effective_scope_role` resolver, plus the role-keyed +create-first-collection / propose-first empty states. **Canonical PR flow is on +`origin` (git.wiggleverse.org) — verify `git remote -v` in full before pushing.**