diff --git a/ohm/0083/SESSION-0083.0-TRANSCRIPT-2026-06-06T01-49--2026-06-06T02-17.md b/ohm/0083/SESSION-0083.0-TRANSCRIPT-2026-06-06T01-49--2026-06-06T02-17.md new file mode 100644 index 0000000..4d4c7c7 --- /dev/null +++ b/ohm/0083/SESSION-0083.0-TRANSCRIPT-2026-06-06T01-49--2026-06-06T02-17.md @@ -0,0 +1,140 @@ +# Session 0083.0 — Transcript + +> App: ohm +> Start: 2026-06-06T01-49 (PST) +> End: 2026-06-06T02-17 (PST) +> Type: coding +> Mode: autonomous (yolo) +> Driver: Ben Stull (+ Claude Opus 4.8) + +## Launch prompt + +``` +Finish §22 S6 remainder: request-to-join + cross-collection inbox (§22.8), then +per-type surfaces (§22.4a items 1&3). Build by analogy to shipped §28 +contribution_requests + S4 memberships.grant. +``` + +## Outcome + +The §22 S6 remainder is complete; the deployment→project→collection three-tier +model is now realized in both code and `SPEC.md`. Two landings: + +1. **§22.8 request-to-join + the cross-collection inbox — shipped v0.46.0** + (PR #24, `origin/main` = `1c17fec`). +2. **Per-type surfaces (§22.4a items 1 & 3) — spec pass done** (PR #25, + `origin/main` = `b7e23a0`), the discovery/spec pass the goal called for, + leaving implementation to slices S7a–S7c. + +## Pre-state + +- `origin/main` = `0140150` (S6 core, v0.45.0): the SPEC merge, per-collection + `enabled_models` (mig 031), the type-driven entry noun (§22.4a item 2). +- Two S6 items were deferred at v0.45.0 as "wanting a discovery/spec pass first": + the per-type surfaces (§22.4a items 1 & 3) and request-to-join (§22.8). +- Clean tree on `main`, fully pushed. Three other ohm sessions (0079/0080/0081) + in flight (unfinalized placeholders) — noted, proceeded concurrently per the + autonomous default. (A concurrent session's `e86fc65 Discovery spec: + configurable collection metadata` landed on `main` mid-session; merges stayed + clean.) + +## The arc + +**1. Understand (read the spec + the analogues).** Read `SPEC.md` §22.8 / §22.11 +(request-to-join surfaces to "the scope's Owners across the subtree", accepting +writes the `memberships` row; one deployment-wide inbox) and the design doc Part +E S6 framing. Mapped the two analogues: §28 `contribution_requests` +(`api_contributions.py`, migration 024, the per-owner notification fan-out + +accept/decline lifecycle) and S4 `memberships` (`api_memberships.py` / +`memberships.py` — `grant` + the broader-scope-supersedes prune, the +`can_invite_at_{project,collection}` gates). Confirmed the engine seam: a join +request targets a `(scope_type, scope_id)` pair (the `memberships` vocabulary +minus `global`), and accept grants a scope role instead of minting an RFC invite. + +**2. §22.8 implementation (backend).** +- Migration `032_join_requests.sql` — additive table (no rebuild), `scope_type ∈ + {project, collection}`, `requested_role`/`granted_role`/`status` enums, a + one-open-per-`(scope, requester)` partial unique index. +- `auth.effective_role_at_scope(user, scope_type, scope_id)` — the scope-grain + twin of `effective_scope_role` (which only keys on a collection), folding + global→project for a project target. Drives the dup-member gate + the + `can_request_join` flag. +- `notify`: `_scope_owner_user_ids` (upward Owner enumeration = the + cross-collection inbox), `fan_out_join_request`, `notify_join_decided`, three + `render_summary` cases. +- `api_join_requests.py` — `GET join-target` / `POST join-requests` / `POST + {id}/accept` / `{id}/decline` under `/api/scopes/{scope_type}/{scope_id}/`. + Key decision: the request POST does **not** require the scope be readable — + that is precisely how one joins a *gated* scope they were told about (§22.8). + Registered the router in `api.py`. + +**3. §22.8 implementation (frontend).** `api.js` join verbs; `JoinRequestModal`; +the `viewer.can_request_join` flag added to the project + collection viewer +blocks in `api_collections.py`; a "Request to join" affordance in +`CollectionDirectory` (project scope) and the `Catalog` footer (collection +scope); the actionable `JoinRequestRow` in `Inbox.jsx` (the cross-collection +inbox surface, which is automatic once the fan-out reaches the right Owners). + +**4. Tests + release.** `test_join_requests_vertical.py` (11: cross-collection +fan-out, project vs collection reach, accept-writes-membership, role-narrowing, +decline, dup-409, existing-member-409, non-owner-403, sibling-collection-403, +unknown-scope-404, eligibility) + `test_migration_032_join_requests.py` (5). +Frontend `api.joinrequests.test.js` (4) + two `CollectionDirectory` cases. +**Backend 546 / frontend 36 green; frontend build clean.** Bumped VERSION + +`package.json` → 0.46.0, CHANGELOG 0.46.0 entry ("no upgrade steps" — additive). +Branch `feat/s6-join-requests` → Gitea-API PR #24 → merged → `main`. + +**5. Per-type surfaces — the spec pass.** The goal flagged these as wanting a +discovery/spec pass first (no BDD scenarios existed in Part C, which is +role-focused). Grounded in the current type plumbing (`registry.VALID_TYPES`, +the lenient type-agnostic `entry.py`) and wrote +`docs/design/2026-06-06-per-type-surfaces.md`: Part A (item 1 — the per-type +frontmatter schema via a new `entry_schema.py` validator), Part B (item 3 — the +`specification` Releases view + the `bdd` scenario/coverage views), Part C (BDD +acceptance scenarios `@S7a/@S7b/@S7c`), Part D (a 3-slice additive, +engine-preserving delivery plan + open product questions). Added a +forward-pointer + shipped/spec'd status to the three-tier doc's S6 bullet. +Branch `spec/s7-per-type-surfaces` → Gitea-API PR #25 → merged → `main`. + +## Cut state + +- `origin/main` = `b7e23a0`; local `main` clean and synced; no open PRs; both + feature branches deleted on merge. +- VERSION 0.46.0 (mirrored in `frontend/package.json`). +- Backend 546 / frontend 36 green; frontend build clean. + +## Deferred decisions + +Low-confidence / autonomous calls made without operator input: + +- **Request POST is not readability-gated.** Deliberate per §22.8 ("a user who + knows a *gated* scope exists requests to join it") — you cannot require read on + a scope that is invisible to the requester. Guarded instead by signed-in + + granted + not-already-a-member + dup-pending. Alternative considered: require + readability (would make gated joins impossible via the API). Confident. +- **`join_requests.scope_type` excludes `global`.** A deployment isn't a thing + one "discovers and joins" (§22.8 names a project/collection). Confident. +- **Route shape `/api/scopes/{scope_type}/{scope_id}/…`.** Clean and mirrors the + `memberships` scope vocabulary; §17 is illustrative and doesn't pin the + contribution/membership routes, so no SPEC change. Reasonable. +- **Accept may narrow the requested role** (optional `role` body, default = + requested). Gives the Owner control rather than forcing the asked-for role; + within the Owner's grant reach. Minor. +- **Treated the per-type surfaces as a *spec-doc* deliverable, not code.** The + goal said they "want a discovery/spec pass first"; I produced the spec pass + (design doc + BDD scenarios + slicing) and left implementation to S7a–S7c + rather than coding surfaces that still carry open product questions. This is + the call most worth the operator's eye — if the intent was "implement them + too," S7a (schemas) is unblocked and ready; S7b/S7c need the D.1 questions + settled first (a discovery session may fit). + +## Next session + +``` +/goal implement S7a — per-type frontmatter schema (§22.4a item 1), per docs/design/2026-06-06-per-type-surfaces.md Part A + Part C @S7a: a backend/app/entry_schema.py validator, the new optional Entry fields (specification: spec_version/lifecycle/supersedes; bdd: feature/verifies), write-boundary enforcement on propose/edit/PR-merge, and the §7 catalog "malformed frontmatter" flag — additive, non-breaking, document/N=1 unchanged. +``` + +S7b (specification releases) + S7c (bdd surfaces) follow once their Part D.1 open +questions (release identity vs `spec_version` pinning; `verifies` ref grammar / +anchors; whether `lifecycle` enters the state machine) are settled — possibly a +discovery session. diff --git a/ohm/0083/SESSION-0083.0-TRANSCRIPT-2026-06-06T01-49--INPROGRESS.md b/ohm/0083/SESSION-0083.0-TRANSCRIPT-2026-06-06T01-49--INPROGRESS.md deleted file mode 100644 index 380ddb5..0000000 --- a/ohm/0083/SESSION-0083.0-TRANSCRIPT-2026-06-06T01-49--INPROGRESS.md +++ /dev/null @@ -1,23 +0,0 @@ -# Session 0083.0 — Transcript - -> App: ohm -> Start: 2026-06-06T01-49 (PST) -> Type: coding -> Status: **PLACEHOLDER — claimed at session start; finalized at session end.** -> -> This file reserves session ID 0083 for ohm. The driver replaces this -> body with the full transcript and renames the file to its final -> SESSION-0083.0-TRANSCRIPT-2026-06-06T01-49--.md form at session end. - -## Launch prompt - -``` -Finish §22 S6 remainder: request-to-join + cross-collection inbox (§22.8), then per-type surfaces (§22.4a items 1&3). Build by analogy to shipped §28 contribution_requests + S4 memberships.grant. - -``` - -## 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._