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>
Lands the table rebuilds migration 026 deferred until a second project exists,
shipped separately from per-project serving for migration hygiene. No behavior
change (deployments stay single-project 'default').
- migration 028_project_scoped_keys.sql: folds project_id into the slug-keyed
PK/UNIQUE of 13 tables (cached_rfcs PK (slug)->(project_id,slug); the
UNIQUE/PK on cached_branches, branch_visibility, branch_contribute_grants,
stars, watches, pr_seen, branch_chat_seen, funder_consents, rfc_collaborators,
contribution_requests, proposed_use_cases gain project_id) and makes the FKs
to cached_rfcs on rfc_invitations/rfc_collaborators/contribution_requests
composite (project_id, rfc_slug) -> cached_rfcs(project_id, slug).
- db.run_migrations: a `-- migrate:no-foreign-keys` migration runs with
PRAGMA foreign_keys toggled OFF around it (required by SQLite's table-rebuild
procedure) + foreign_key_check after, failing loudly on dangling refs.
- ON CONFLICT upsert targets for the rebuilt tables gain project_id so they
match the new composite indexes (value still defaults to 'default').
- test_migration_028_project_scoped_keys.py: proves two-project same-slug
coexistence, within-project uniqueness, composite-FK enforcement. 442 pass.
- design doc §2 marked shipped.
Per docs/superpowers/specs/2026-06-04-m3-backend-planb-design.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds the three-tier authorization resolver on M1's schema spine: the
most-permissive union of deployment role (§6.1), project role (§22.6), and
per-RFC authority (§6.3/§12), with the §22.5 visibility gate subtractive on
top (§22.7). Pure app-layer — no migration (M1 shipped the tables), no
behavior change on the public default project. Verifiable on the single
default project by flipping its visibility and granting/revoking roles.
- app/auth.py: the §22.7 resolver primitives — project_visibility,
project_member_role, project_of_rfc, is_project_superuser, can_read_project,
effective write/discuss standing (can_contribute_in_project /
can_discuss_in_project), require_project_readable, visible_project_ids.
The three per-RFC capability helpers (can_discuss_rfc / can_contribute_to_rfc
/ can_invite_to_rfc) now compose all three tiers, so the ~20 call sites
inherit M2 unchanged.
- Read pass: the §22.5 visibility gate (404 to non-members) threaded into
every RFC-resolution helper across api.py / api_branches / api_prs /
api_graduation / api_discussion / api_contributions / api_invitations, plus
the catalog + proposals listings filtered by visible_project_ids.
- Write pass: the deep gates (branch read/contribute/owner, PR merge/withdraw/
edit, graduation, discussion resolve) fold in project_admin via
is_project_superuser; the "any-contributor" branch mode routes through
can_contribute_in_project; propose + claim gate on project contribute
standing. Deployment-level surfaces (admin idea-PR merge/decline, account
notification-mute) stay deployment-scoped.
- Operator decisions: implicit-on-public (a granted deployment contributor
keeps its pre-M2 write baseline on a public project, no membership row, so
the N=1 case stays whole) and preserve-curation (that baseline does not
override per-RFC owner curation — only an explicit project grant or a
deployment owner/admin does), keeping the v0.16.0 per-RFC invite contract
intact on public.
- tests: test_multi_project_authz_vertical.py — 9 vertical assertions on the
resolver tiers, public-unchanged regression, the gated 404 read gate, the
gated contribution gate, union + subtractive visibility, and revocation.
Full suite 390 passed.
- docs: mark Part C M2 "(landed)" with the two operator decisions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Retire the per-RFC-repo model. RFCs now live in their meta-repo entry
(rfcs/<slug>.md) for their whole life; graduation is an in-place
super-draft → active state flip that keeps the body in the entry — no
repo creation, no body-strip, no five-step transaction, no rollback.
SPEC: §1 topology rewritten (one meta/content repository, no per-RFC
repos) with a deployer-facing "single content repository" framing; §2
(repo: always-null), §3 (active is in-place), §4, §9.8 (handoff
frictions dissolve), and §13 fully rewritten (two-field dialog, "The
flip", §13.6 RFC-0001 fold-back record).
Code: graduation collapses to open+merge one frontmatter PR; branch/PR/
chat dispatch re-keyed on meta-residency (repo IS NULL) so active RFCs
edit on the meta repo exactly as super-drafts do; the two "RFC has no
repo" 409 guards removed; promote-to-branch slug-embeds an active RFC's
auto-branch (edit-<slug>-<hex>) for shared-repo cache attribution;
refresh_meta_branches + hygiene branch-resolution include meta-resident
actives; the §9.8 read-only guard + pre_graduation_history scoped to
legacy per-repo only; dead bot primitives + GraduateDialog repo field +
blocking-PR popover removed. The repo: frontmatter field and the
/blocking-prs endpoint are retained (schema stability / informational).
Tests: graduation suite rewritten to the flip model; e2e + hygiene
updated. Full backend suite 375 passed; frontend builds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wave 5 / Track B. Roadmap item #12. Folds in #21 Part C Amplitude
wiring inline (operator ask: "best practices from the very get-go").
RFC owners can invite specific users to one of two per-RFC roles:
contributor (open PRs + join discussion) or discussant (discussion
only). Non-invited users keep the v0.6.0 anonymous-read contract.
The per-RFC write gate layers on top of the existing
require_contributor gate; a super-draft with no owners yet falls
through to the platform-granted contract, preserving the
v0.6.0/v0.7.0/v0.8.0 contracts in their domains.
Backend: migration 018_rfc_invitations.sql (auto-applied — two
tables: rfc_invitations + rfc_collaborators); api_invitations.py
with five endpoints + transactional email; auth.py helpers
(is_rfc_owner / is_rfc_collaborator / can_discuss_rfc /
can_contribute_to_rfc / can_invite_to_rfc); api_discussion +
api_branches + api_prs gate composition; api_admin.py additive
rfc_invitations[] per user. 237 backend tests pass (18 new in
test_rfc_invitations_vertical.py).
Frontend: InvitationsModal.jsx (owner surface), AcceptInvitation.jsx
(/invitations/accept route), api.js helpers, RFCView.jsx
Invitations button, App.jsx route registration.
Amplitude wiring (inline, #21 Part C):
- INVITATION_SENT from InvitationsModal { rfc_slug, role_in_rfc }
- INVITATION_ACCEPTED from AcceptInvitation { rfc_slug, role_in_rfc }
- identify() BEFORE the accept event with properties: invited_at
(setOnce), last_invited_to_rfc, last_invite_role_in_rfc,
claim_method: 'rfc-invite'
- EVENTS taxonomy extended with INVITATION_SENT + INVITATION_ACCEPTED
No new secrets, no new overlay keys, no operator gesture beyond the
v0.15.0 overlay-set + restart. Frontend build verified green.
Subagent ν shipped the feature on feature/v0.16.0-owner-invite
(a51beec). Driver-side integration squash-merged into main,
hand-resolved VERSION + package.json + CHANGELOG (strict-descending
0.16.0 → 0.15.0), and added the inline Amplitude wiring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second §19.2 settlement after v1. New §6.7 alongside §6.6: optional
`funder:` frontmatter field names a single gitea_login; a
`funder_consents` app-db row records funder-side opt-in; both halves
required for the binding to activate (two-key rule). Funder universe
replaces — does not augment — the operator universe per-RFC for
attribution-clean resolution. Funder role grants zero §6.1/§6.3
authority. Three revocation paths each restore the operator-credentials
status quo.
§19.2's credential-delegation entry is split: lighter half marked
settled with a pointer to §6.7; operational-realities half (mid-call
failure, rotation, billing, rate-limit attribution) lives on as its
own entry. Test suite is 125/125 green (106 prior + 19 new).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First §19.2 candidate settled after v1. The heavier per-RFC-model
topic subdivided into UX (this) and credential delegation + funder
role (still §19.2). New §6.6 carries the rule: an optional `models:`
frontmatter field on the meta-repo RFC entry; absent inherits the
operator universe, populated narrows the picker to the intersection
with provisioned providers, `[]` opts the RFC out of AI entirely.
The first resolved entry is the RFC default. §18's ENABLED_MODELS is
reframed as the operator universe.
Code: migration 009 adds nullable cached_rfcs.models_json (NULL ≠ []
is load-bearing); entry.py grows the optional field with absent-vs-
empty round-tripping in parse/serialize; new models_resolver module
holds the rule; api_branches replaces /api/models with the slug-aware
/api/rfcs/{slug}/models and threads the chat + reask paths through
the resolver; api_prs §10.2 uses the resolver and extends the stub
fallback to the opt-out case; frontend passes slug to listModels.
Tests 106/106 green (96 prior + 10 in test_per_rfc_models.py). No
behavioral change for entries without `models:` — operator universe
preserved as default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add §12 30/90 hygiene scheduler in hygiene.py, mirroring the
DigestScheduler shape; wires next to digest in main.py with the
same start/stop/run_tick test seam.
- Extend bot.delete_branch to accept actor=None for system gestures,
per §15.9 (actor_user_id=NULL, on_behalf_of=bot_login).
- Convert every branches/{branch} route in api_branches.py and
api_prs.py to {branch:path}; move the bare GET to the bottom of
the router so deeper GETs match before greedy-path swallow.
- Extend api_prs.py's _require_pr to accept pr_kind='meta_metadata'
so the §9.5 metadata-pane PRs land an in-app merge.
- Graduation rollback now deletes the graduate-<slug>-<6hex> branch
after closing the PR — §19.2 candidate that lands here.
- Email-bounce webhook gains a WEBHOOK_EMAIL_BOUNCE_SECRET seam.
- FakeGitea grows a DELETE /branches/{branch:path} handler and a
slashed-branch read; integration tests for the hygiene vertical
cover the 30d close, 90d delete, post-merge delete, pinned
exemption, per-user cursor preservation, no-notification rule,
and the graduation-rollback cleanup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The §13.3 transactional sequence flips a super-draft to active —
five steps with paired undoes, an in-process orchestrator fed by
an asyncio.Queue, the §17 SSE endpoint streaming step transitions
to the dialog. Each step is a new bot primitive that logs an
`actions` row, bracketed by `graduate_start` / `graduate_complete`
for the linkable audit sequence. Rollback runs the undoes in
reverse from the last completed step; merge_pr has no undo by
design per §13.5.
The §9.8 precondition gate is enforced server-side at the top of
POST /graduate so the §13.3 rollback complexity does not grow.
The §13.4 chat migration is a database semantic no-op — the
(slug, branch_name='main') threads keep their identity, only the
interpretation changes. The §9.8 pre-graduation history surfaces
via a new _is_meta_target(rfc, branch) dispatch helper and lands
as pre_graduation_history on /main.
§13.1 claim flow landed alongside since it's the prerequisite for
non-admin graduation — bot.open_claim_pr plus broadening
api_prs._require_pr to accept meta_claim.
45/45 tests green; ten new integration tests cover the validator,
the §9.8 precondition refusal, happy path with audit verification,
mid-sequence rollback at steps 2 and 3, concurrent refusal,
chat-survives-without-data-movement, pre-graduation history, and
the §13.1 claim PR cycle.
SPEC.md §19.1 rewritten for Slice 6 (notifications); §19.2 grew
four candidates surfaced during the slice.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The §17 routing-collapse rule lands in api_branches.py and
api_prs.py — every branches/<branch>/... and prs/<n>/... route
dispatches on the entry's state to pick the right Gitea repo, and
the body extracted from the entry's frontmatter envelope is what
the editor and the diff see. The bot grows open_metadata_pr;
cache grows refresh_meta_branches. Two §17 routes added:
start-edit-branch and metadata. The §9.4 super-draft view replaces
RFCView.jsx's Slice 2 placeholder; a metadata pane modal opens
from the breadcrumb. Branch naming uses edit-<slug>-<6hex> to
dodge the §19.2 path-routing candidate while preserving §9.5's
structural shape.
Covered by tests/test_super_draft_vertical.py (10 tests). The
full Slices 1-4 suite is 35/35 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per the §19.1 brief: the three-column shape (§8.1) opens on main
in discuss mode (§8.2), supports the §8.3 discuss-vs-contribute
flip on non-main branches, hosts §8.4's per-branch chat with AI
participation (§18's <change> protocol → §8.14 changes rows), the
§8.8 change-card panel with §8.9 accept/decline/edit-before-accept,
the §8.10 tracked-change markup + DiffView toggle, the §8.11
manual-edit flushes with the stale-change mechanic, the §8.12
range and paragraph sub-threads, the §8.13 flag affordance, and
the §8.14 discuss-mode buffer.
Backend: bot.py grew per-RFC-repo write ops (cut_branch_from_main,
commit_accepted_change with the structured original/proposed/reason
body and Change-Id + Source-Message-Id + On-behalf-of trailers,
commit_manual_flush, ensure_rfc_repo_seed). cache.py grew
refresh_rfc_repo and the webhook dispatches on repository.full_name.
providers.py and chat.py port the §18 carryovers — multi-provider
LLM abstraction and SSE-streaming chat against the §5 threads /
thread_messages / changes schema. api_branches.py mounts the §17
branches/<branch>/* and threads/<thread_id>/* routes with the §6
/ §11 permission checks inline.
Frontend: RFCView.jsx rebuilt as the §8 surface; Editor.jsx,
ChatPanel.jsx, ChangePanel.jsx, PromptBar.jsx, SelectionTooltip.jsx,
DiffView.jsx, ModelPicker.jsx, modelStyles.js lifted from the
prototype and adapted to the canonical schema.
Covered by `backend/tests/test_rfc_view_vertical.py` — eleven new
integration tests against an extended FakeGitea (PUT contents,
POST orgs/{org}/repos, seed_rfc_repo): main-view read,
promote-to-branch, accept (with and without edit-before-accept),
decline, manual flush + system message, flag creation, visibility
flip, anonymous read-but-no-contribute, stale-change refusal, and
the chat-streaming path with a fake provider injected. The 5
Slice 1 tests continue to pass alongside.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>