3c9109c392
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>
60 lines
3.1 KiB
SQL
60 lines
3.1 KiB
SQL
-- 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';
|