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>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
-- v0.29.0 / roadmap #28 Part 3 — offer-to-contribute-to-a-pending-RFC.
|
||||
--
|
||||
-- When the #28 scanner matches a term in submitted PR/comment text to a
|
||||
-- *pending* RFC (a super-draft: accepted-as-an-idea but not yet graduated
|
||||
-- to an active RFC), the reader is offered a "ask to contribute" popover.
|
||||
-- Submitting it lands a row here AND a notification in each owner's §15
|
||||
-- inbox; the owner can accept (which fires #12's owner-invite flow with
|
||||
-- the requester as the invitee) or decline (the requester is notified and
|
||||
-- the request closes).
|
||||
--
|
||||
-- A "pending RFC" is scoped to a super-draft (cached_rfcs.state =
|
||||
-- 'super-draft'): it is in cached_rfcs (so the rfc_invitations FK that the
|
||||
-- accept path reuses resolves), it carries owners (owners_json) to route
|
||||
-- the request to, and it already has a discussion/contribution surface to
|
||||
-- open. Pre-merge idea PRs (not yet in cached_rfcs, no contribution
|
||||
-- surface) are deliberately out of scope — see backend/app/rfc_links.py.
|
||||
--
|
||||
-- The request row is the persistent record; the inbox notification is the
|
||||
-- owner-facing actionable surface keyed back to it via `notification_id`.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contribution_requests (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rfc_slug TEXT NOT NULL
|
||||
REFERENCES cached_rfcs(slug) ON DELETE CASCADE,
|
||||
requester_user_id INTEGER NOT NULL
|
||||
REFERENCES users(id) ON DELETE CASCADE,
|
||||
-- The term in the PR/comment text that surfaced the offer (e.g. the
|
||||
-- super-draft's title). Carried for the owner's context line and the
|
||||
-- requester's "what RFC" anchor; not a foreign key.
|
||||
matched_term TEXT NOT NULL,
|
||||
-- The three contribute-request fields (§15 / #26 vocabulary).
|
||||
-- `who_i_am` and `why` are required; `use_case` mirrors #26's
|
||||
-- optional ground-truth field.
|
||||
who_i_am TEXT NOT NULL,
|
||||
why TEXT NOT NULL,
|
||||
use_case TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN ('pending', 'accepted', 'declined')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
decided_at TEXT,
|
||||
decided_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
-- The rfc_invitations row minted on accept (the #12 reuse), and the
|
||||
-- owner-facing notification row that carries the Accept/Decline action.
|
||||
invitation_id INTEGER REFERENCES rfc_invitations(id) ON DELETE SET NULL,
|
||||
notification_id INTEGER REFERENCES notifications(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_contribution_requests_rfc
|
||||
ON contribution_requests(rfc_slug, status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_contribution_requests_requester
|
||||
ON contribution_requests(requester_user_id, status);
|
||||
|
||||
-- At most one open (pending) request per (RFC, requester): a second ask
|
||||
-- while one is still pending is a 409, not a duplicate row. A decided
|
||||
-- request (accepted/declined) does not block a fresh ask later.
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_contribution_requests_one_open
|
||||
ON contribution_requests(rfc_slug, requester_user_id)
|
||||
WHERE status = 'pending';
|
||||
Reference in New Issue
Block a user