Commit Graph

4 Commits

Author SHA1 Message Date
Ben Stull 9ca07a3f81 §22 S1: thread collection_id through backend + update tests (N=1 unchanged, 454 green)
- 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>
2026-06-05 08:26:14 -07:00
Ben Stull 503689bf1a feat(projects): M2 — project-scoped authorization + the §22.7 resolver (§22)
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>
2026-06-02 20:02:34 -07:00
Ben Stull 3c9109c392 v0.29.0: #28 Parts 2+3 — create-RFC offers + contribute-to-pending requests
Extends the v0.26.0 (#28 Part 1) read-time scanner into three buckets in
one pass — active link (Part 1), pending-RFC contribute offer (Part 3),
create-RFC offer (Part 2) — precedence active > pending > candidate. The
backend still emits only structured segments (never HTML), so the surface
stays XSS-safe by construction.

Part 2 — create-RFC offers: a multi-word tag from the #27 taxonomy with no
defining RFC renders, for a create-rights viewer, as an inline "+ create
RFC" affordance that opens the propose modal pre-filled (?propose=<term>;
ProposeModal gained initialTitle). Conservative multi-word gate; broader
heuristics + the Haiku path are deferred.

Part 3 — contribute-to-pending offers: a term matching a super-draft
renders, for a signed-in non-owner, an "ask to contribute" affordance with
the owner's display name. It opens a 3-field request form (who/why/optional
use-case); submitting lands a contribution_requests row (migration 024) and
one actionable §15 notification per owner (new kind
contribution_request_on_pending_rfc, personal-direct). The owner's inbox
shows who/why/use-case inline with Accept/Decline. Accept fires #12's
owner-invite flow with the requester as invitee and echoes a notification
back; decline notifies the requester. Pre-merge idea PRs are out of scope.

New endpoints: GET /api/rfcs/{slug}/contribution-target,
POST /api/rfcs/{slug}/contribution-requests,
.../{id}/accept, .../{id}/decline. The invite issue path was refactored
into one reusable api_invitations.issue_invitation(...) chokepoint shared
by the manual invite endpoint and Part 3's accept.

Tests: 9 new (3 scanner-bucket unit + 6 e2e). Full suite 374 passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 20:10:13 -07:00
Ben Stull ee4925b6ac Release 0.16.0: owner-only invite for per-RFC contribution + discussion (+ #21 Part C Amplitude wiring)
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>
2026-05-28 05:06:55 -07:00