Commit Graph

6 Commits

Author SHA1 Message Date
Ben Stull 1dab24eef0 Merge feat/multi-project (§22 M1+M2) into main
Integrates the multi-project work so far — the §22 design drafts, M1 (the
schema spine), and M2 (project-scoped authorization + the §22.7 resolver) —
on top of the v0.32.0 retire/§13 changes that landed on main meanwhile.

Integration decisions:
- Renumbered the M1 projects migration 025_projects.sql -> 026_projects.sql.
  v0.32.0 shipped 025_retired_state.sql, which rebuilds cached_rfcs and
  predates the project_id column; running projects *after* the retire rebuild
  is required so project_id survives on a fresh database (the runner applies
  *.sql in filename order). Comment/doc references bumped to match.
- Resolved the get_rfc conflict in api.py by composing both gates: compute the
  viewer once, apply the §22.5 visibility gate, then v0.32.0's §13.7
  retired-entry owner-only check.
- api_graduation.py / api_discussion.py auto-merged cleanly (M2's threaded
  viewer/visibility gate coexists with the new retire endpoints + retired
  state).

Full suite 401 passed on a fresh DB.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 04:56:39 -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 49ba06e0c2 §13: make graduation's RFC number optional; add retire (soft delete)
Optional number (§13.2/§13.3): GraduateBody.rfc_id is now optional.
A blank/absent id graduates to `active` with `id: null`, leaving the
slug as the canonical identifier (§2.3). /graduate/check treats a blank
id as valid (ok:true); /graduate only validates the RFC-NNNN regex +
collision check when a number is supplied. The Graduate dialog allows an
empty number and renders number-less entries by slug (no RFC-undefined).

Retire (§3, §3.1, §13.7): new `retired` soft-delete state. An RFC's own
owners (frontmatter) and site `owner`-role holders — not app admins —
retire via POST /api/rfcs/<slug>/retire, an auto-merged meta-repo flip
PR (graduation machinery reused). Retired entries leave every browsing
surface: catalog, get_rfc (404 except site owner), discussion/branch
reads. Un-retire is site-owner-only (POST .../unretire), discoverable
via the owner-gated GET /api/admin/retired-rfcs ("Retired" admin tab).
Migration 025 widens the cached_rfcs.state CHECK to include 'retired'
(table rebuild, all columns preserved).

Tests: graduation no-number cases (active+null id by slug; check accepts
blank) added to test_graduation_vertical.py; new test_retire_vertical.py
covers perms (owner/site-owner allowed, admin/contributor 403),
catalog/read exclusion, and a graduate→retire→unretire round-trip. Full
backend suite 386 passing; frontend builds clean. SPEC §3/§3.1/§13
updated; CHANGELOG + VERSION → 0.33.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 22:27:46 -07:00
Ben Stull e794523079 v0.26.0: auto-link referenced RFCs in PR text + comments (roadmap #28 Part 1)
Part 1 of item #28: references to existing accepted RFCs inside PR
descriptions and comment text now render as inline links to the
referenced RFC. Parts 2 (offer-to-create) and 3 (offer-to-contribute-
to-pending) are deliberately deferred — Part 1 ships first as the easy
win, per the roadmap row.

Shipped in parallel with the v0.25.0 security-hardening session; this
took the next free version slot (0.26.0) per the roadmap's
"claims the next available version number" rule. Expect a top-of-file
CHANGELOG/VERSION merge with 0.25.0 — distinct concerns, trivial to
resolve.

Backend:
- rfc_links.py (new): builds a term index from the live accepted
  (state='active') RFC corpus and segments plain text into text /
  rfc-link segments. Conservative matching — links only rfc_id tokens
  (RFC-0001), multi-word titles (Open Human Model), and hyphenated
  slugs (open-human-model); a single common-word title/slug is NOT
  linked (would turn every prose "human" into a link). Case-insensitive,
  word-boundary-anchored, longest-match-wins, self-reference suppressed.
- api_prs.py get_pr(): enriches the PR description (description_segments)
  and every PR comment (text_segments).
- api_discussion.py: enriches PR-less discussion comments (text_segments).

Read-time, not submit-time: the roadmap says "at submit time" but the
intent it names is "not as live compose preview", which read-time
honors. Chosen for correctness (links track the live active set —
newly-accepted RFCs start linking, withdrawn ones stop), zero migration,
and cheapness (small cache-resident corpus). Recorded as a §19.3-rule-2
note in the session transcript.

Frontend:
- LinkedText.jsx (new): maps backend segments onto React text nodes +
  anchors. No dangerouslySetInnerHTML — XSS-safe by construction,
  independent of any HTML-sanitization layer. Falls back to raw text
  when segments are absent.
- PRView.jsx: description + PR conversation comment bodies render via
  LinkedText.
- RFCDiscussionPanel.jsx: discussion comment bodies render via LinkedText.
- App.css: .rfc-autolink (subtle accent + dotted underline, tokenized).

Tests: 12 new (test_rfc_links_vertical.py) — 9 scanner units + 3
end-to-end (PR description / review comment / discussion comment all
surface *_segments; self-reference suppression). Full suite 363 green;
frontend builds clean.

No upgrade steps: additive, no migration, no secret, no config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 16:17:01 -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
Ben Stull c92730a737 Release 0.5.0: PR-less per-RFC discussion (contribution still requires PR)
Roadmap item #3. An RFC's main view now carries a discussion surface
distinct from PR comments and from branch chat. The substrate is the
existing threads/thread_messages tables — rows with branch_name IS NULL
scope to the RFC's main view; the schema already permitted that shape,
v0.5.0 is the first build to write it. Five new endpoints under
/api/rfcs/<slug>/discussion/..., a new RFCDiscussionPanel right-column
component used when branchParam === main, SPEC §10.10 settling
discussion-vs-contribution, and §17 listing the new routes. Notification
routing reuses the existing chat_message_in_participated_thread /
chat_reply_to_my_message event kinds with branch_name=null on the
fan-out row; a distinct event_kind is a §19.2 candidate. Anonymous
viewers can read; writes require contributor — v0.6.0's item #4 will
harden adjacent gates. No schema migration; minor bump, no operator
action required beyond rebuild and restart.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 23:04:52 -07:00