§22 S3: scope-role enforcement + collection-grain visibility — v0.42.0 (@S3) #1

Merged
benstull merged 24 commits from feat/s3-scope-roles-collection-visibility into main 2026-06-06 01:08:55 +00:00
Owner

§22 S3 — scope-role enforcement + collection-grain visibility (@S3) — v0.42.0

Implements slice S3 of the §22 three-tier refactor (deployment → project → RFC collection), per docs/design/2026-06-05-three-tier-projects-collections.md Part B/C.1/E, plus the operator's mid-session visibility requirements.

What lands

  • Four-layer scope-role resolver (§B.2)auth.effective_scope_role(user, collection) folds global → project → collection grants over the unified {owner, contributor} roles, most-permissive, no negative override.
  • Global-scope grants — migration 030_global_scope.sql extends memberships.scope_type to {global, project, collection} (the "global RFC Contributor" tier; sentinel scope_id='*').
  • Collection-grain visibility — a gated collection is hidden from the public (404, omitted from the directory) yet readable + listed for any scope-role holder reaching it. A collection may be set only as strict or stricter than its project (public < unlisted < gated) — validated at create (422), clamped at the mirror.
  • Write gates re-pointed to the collection axis — propose, discuss, branch read/contribute, mark-reviewed, graduate, PR/discussion/contribution moderation now resolve the entry's collection, so a collection Owner administers exactly their subtree.
  • create-collection authority widened (§B.1) — project/global-scope grant holders (Owner or RFC Contributor) may create collections, not only deployment owners/admins.

Keystone decision (logged in transcript 0076)

§B.1/§B.3's literal "deployment contributor = global RFC Contributor" contradicted the C.1 "hana" scenario and the M2 implicit-public baseline. Resolved as: a plain granted account is a granted account, not a write-everywhere global role; "global RFC Contributor" is an explicit scope_type='global' grant; the implicit-public write baseline is grandfathered onto the migration-seeded default collection only → the N=1 deployment loses no capability. Reinterprets §B.1/§B.3 — flagged for the SPEC merge (S6).

Breaking + upgrade steps

Write standing now requires an explicit scope grant outside the default collection (and beyond the default project). N=1 deployments need no action; multi-collection deployments must grant scope roles. Full upgrade steps in CHANGELOG.md (0.42.0).

Tests

test_s3_scope_roles_vertical.py (8 C.1 scenarios ada/ben/cleo/dan/eve/fay/gil/hana + hidden-collection + strictness + create-authority), test_migration_030_global_scope.py. Full backend suite: 493 passed.

🤖 Generated with Claude Code

## §22 S3 — scope-role enforcement + collection-grain visibility (`@S3`) — v0.42.0 Implements slice **S3** of the §22 three-tier refactor (deployment → project → RFC collection), per `docs/design/2026-06-05-three-tier-projects-collections.md` Part B/C.1/E, plus the operator's mid-session visibility requirements. ### What lands - **Four-layer scope-role resolver (§B.2)** — `auth.effective_scope_role(user, collection)` folds global → project → collection grants over the unified `{owner, contributor}` roles, most-permissive, no negative override. - **Global-scope grants** — migration `030_global_scope.sql` extends `memberships.scope_type` to `{global, project, collection}` (the "global RFC Contributor" tier; sentinel `scope_id='*'`). - **Collection-grain visibility** — a `gated` collection is *hidden from the public* (404, omitted from the directory) yet readable + listed for any scope-role holder reaching it. A collection may be set only **as strict or stricter** than its project (`public` < `unlisted` < `gated`) — validated at create (422), clamped at the mirror. - **Write gates re-pointed to the collection axis** — propose, discuss, branch read/contribute, mark-reviewed, graduate, PR/discussion/contribution moderation now resolve the entry's *collection*, so a collection Owner administers exactly their subtree. - **create-collection authority widened (§B.1)** — project/global-scope grant holders (Owner or RFC Contributor) may create collections, not only deployment owners/admins. ### Keystone decision (logged in transcript 0076) §B.1/§B.3's literal "deployment contributor = global RFC Contributor" contradicted the C.1 "hana" scenario and the M2 implicit-public baseline. Resolved as: a plain granted account is a granted *account*, not a write-everywhere global role; "global RFC Contributor" is an explicit `scope_type='global'` grant; the implicit-public write baseline is **grandfathered onto the migration-seeded `default` collection only** → the N=1 deployment loses no capability. Reinterprets §B.1/§B.3 — flagged for the SPEC merge (S6). ### Breaking + upgrade steps Write standing now requires an explicit scope grant **outside the default collection** (and beyond the default project). N=1 deployments need no action; multi-collection deployments must grant scope roles. Full upgrade steps in `CHANGELOG.md` (0.42.0). ### Tests `test_s3_scope_roles_vertical.py` (8 C.1 scenarios ada/ben/cleo/dan/eve/fay/gil/hana + hidden-collection + strictness + create-authority), `test_migration_030_global_scope.py`. **Full backend suite: 493 passed.** 🤖 Generated with [Claude Code](https://claude.com/claude-code)
benstull added 24 commits 2026-06-06 01:08:38 +00:00
Splits the original §22 two-tier model (deployment → project=corpus) into
three tiers (deployment → project → RFC collection). Project owns one
content repo; collections are typed subfolders declared by .collection.yaml
manifests (git-truth). Reconciles the accumulated role vocabulary onto one
{owner, contributor} enum attached at {global, project, collection}, with
downward additive inheritance and no negative override. Adds BDD scenarios
(Part C) for role usage, invitation, and empty states. Re-slots the roadmap
to fold the tier into the not-yet-shipped Plan B (mig 028) + M3-frontend.

Session 0072 (spec). Revises docs/design/multi-project-spec.md §22.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-checked code vs the stale memory: migration 028 (slug PK -> (project_id,
slug)), v0.35.0 /p/<project>/ routing, and v0.37/0.38 per-project read+propose
are all shipped to main. The 'fold into not-yet-shipped Plan B + M3-frontend'
premise is false. Neutralize the wrong claims in §0/§A.3 and flag Part E's
sequencing as pending re-decision; structural model (Parts A-D) unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Operator chose (session 0072) to add the collection grain beneath today's
project: projects table stays the group tier (keeps content_repo), a new
collections table holds the per-corpus fields, entries re-key to
(collection_id, slug), one default collection per project on migrate, breaking
/p/<project>/e/<slug> -> /p/<project>/c/<collection>/e/<slug> with 308s.
Re-sloted slices N1-N6. Correction banners updated from pending -> decided.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per operator (session 0072): every slice must end in a usable deployment and
declare which Part C scenarios it makes pass. Tag all 23 Gherkin scenarios with
@S<n> (the slice that completes them) and re-cut Part E from layer-by-layer
(N1-N6) to usable increments (S1-S6) with a slice->scenario index table. S1
bundles the coupled migration 029 + threading + redirect as one right-sized
first session; S2 second collection; S3 role enforcement; S4 invitation;
S5 create-project + directory; S6 types + SPEC merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- collections.py resolution helpers (default_collection_id, type, initial_state)
- registry mirror writes project grouping fields + default-collection corpus fields
- auth.project_of_rfc joins collections; project_member_role reads memberships
- cache/api_*/funder writers+readers re-keyed to collection_id (cached_prs + denormalised
  project_id tags unchanged); api_deployment reads type/initial_state from the default collection
- projects.py restamp detects bootstrap via collections; initial_state via collection
- tests updated to the three-tier schema; test_migration_028 retired (superseded by 029)
- add @S1 acceptance test (collection grain + N=1 serving)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- entryPaths builders carry the /c/<collection>/ segment (default collection in S1)
- /p/:projectId/* gains c/:collectionId/ corpus routes; serving stays project-scoped
- DefaultCollectionRedirect (C3.7: project landing -> default collection)
- LegacyCorpusRedirect (v0.35.0 /p/<p>/e/<slug> bookmarks -> /c/default/, query preserved)
- entryPaths unit test; build + vitest green (18 tests)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan for slice S2 of the three-tier (deployment→project→collection) refactor.
Completes acceptance @S2 (C3.6). See
docs/design/2026-06-05-three-tier-projects-collections.md Part E.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Discover named collections by walking each project's content-repo root for
<subdir>/.collection.yaml; parse + upsert with immutable-type enforcement
(§22.4a) and project-visibility inheritance. The default collection still
flows from projects.yaml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refresh_meta_repo now iterates a project's collections and keys cached_rfcs by
collection_id; the default collection (subfolder '') keeps the shipped rfcs/
root path. N=1 default path unchanged (469 green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refactor the project-scoped serve/propose internals into collection-grained
helpers (_list_rfcs_for_collection, _get_rfc_for_collection,
_propose_into_collection); add routes under
/api/projects/<id>/collections/<cid>/rfcs[/<slug>|/propose]. Propose writes
the entry under the target collection's <subfolder>/rfcs via a new rfcs_dir
param on bot.open_idea_pr. Default-collection routes preserved as wrappers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
POST /api/projects/<id>/collections, owner/admin-gated, commits a
.collection.yaml to the content repo main via bot.create_collection, then
re-mirrors the registry so the collections row appears (§22.2). Adds GET
list/one collection routes. Extends FakeGitea to model directory listings so
the mirror's content-repo walk is exercised end to end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
useCollectionId() hook; listRFCs/getRFC/proposeRFC take an optional collection
id and target the /collections/<cid>/ routes; add listCollections +
createCollection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Catalog reads the /c/:collectionId/ segment via useCollectionId and fetches the
collection-scoped catalog, building entry/proposal links with the active
collection; the propose modal threads the active collection so a propose from a
named collection targets it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace DefaultCollectionRedirect with a CollectionDirectory that lists the
project's visible collections, or redirects into the sole one when there is
exactly one (preserving the S1 C3.7/C3.8 single-collection UX). The
create-first-collection empty state is S4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Anonymous reader of an empty public collection gets a 200 empty catalog and no
propose action (the propose route rejects anonymous); the Catalog footer's
'Sign in to propose' prompt is the existing anonymous affordance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Minor, non-breaking: named collections via .collection.yaml, create-collection
endpoint, collection-scoped serve/propose, and the /p/<project>/ collection
directory. Completes acceptance @S2 (C3.6). 478 backend + 26 frontend green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement slice S3 of the §22 three-tier refactor: the four-layer
most-permissive scope-role resolver (§B.2) over {owner, contributor}
grants at {global, project, collection}, with the §22.5 visibility gate
enforced at the collection grain.

- migration 030: memberships.scope_type += 'global' (the global RFC
  Contributor tier; sentinel scope_id '*').
- auth.effective_scope_role folds global → project → collection,
  most-permissive, no negative override; can_read_collection /
  can_contribute_in_collection / is_collection_superuser /
  can_create_collection gate reads, writes, admin, and create.
- collection-grain visibility: a gated collection is hidden from the
  public (404, omitted from the directory) yet visible+listed for a
  scope-role holder; a collection may be set only as strict or stricter
  than its project (public < unlisted < gated), validated at create and
  clamped at the mirror.
- entry-scoped authority (mark-reviewed, graduate, branch read/contribute,
  PR/discussion/contribution moderation) re-pointed from the project grain
  to the entry's collection.
- create-collection authority widened to a project/global-scope grant
  holder (§B.1), not only a deployment owner/admin.

Keystone reconciliation (session 0076): a plain granted account is a
granted *account*, not a write-everywhere global role; the implicit-public
write baseline is grandfathered onto the migration-seeded `default`
collection only, so the N=1 deployment loses no capability. Reinterprets
§B.1/§B.3 literally — flagged for the SPEC merge (S6).

Completes @S3 (C1.1–C1.8). Tests: test_s3_scope_roles_vertical.py (8 C.1
scenarios + visibility/strictness), test_migration_030_global_scope.py.
Full backend suite 493 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
benstull merged commit e6bd69f132 into main 2026-06-06 01:08:55 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: benstull/rfc-app#1