Files
session-history/ohm/0076/SESSION-0076.0-TRANSCRIPT-2026-06-05T17-25--2026-06-05T18-13.md
T

9.5 KiB
Raw Blame History

Session 0076.0 — Transcript

App: ohm Start: 2026-06-05T17-25 (PST) End: 2026-06-05T18-13 (PST) Type: coding Status: FINALIZED.

Driver session — shipped framework release v0.42.0 (§22 slice S3).

Launch prompt

/goal S3 — implement §22 slice S3 (scope-role enforcement, @S3 = C1.1C1.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 AE, @S<n>-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 AE (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.1C1.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.1C2.7 + C3.3C3.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

  • 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.1C2.7,
C3.3C3.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.