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:
Ben Stull
2026-05-28 20:10:13 -07:00
parent 019c8a9185
commit 3c9109c392
21 changed files with 1530 additions and 143 deletions
+2 -2
View File
@@ -220,7 +220,7 @@ export default function PRView({ viewer }) {
<h1 className="pr-title">{pr.title}</h1>
{pr.description && (
<p className="pr-description">
<LinkedText segments={pr.description_segments} text={pr.description} />
<LinkedText segments={pr.description_segments} text={pr.description} viewer={viewer} canCreate={viewer?.permission_state === 'granted'} />
</p>
)}
{/* #26: the optional ground-truth use case for this change,
@@ -444,7 +444,7 @@ function PRConversation({ threads, messagesByThread, threadsByKind, seenMsgId })
</div>
{m.quote && <pre className="chat-msg-quote">{m.quote}</pre>}
<div className="chat-msg-body">
<LinkedText segments={m.text_segments} text={m.text} />
<LinkedText segments={m.text_segments} text={m.text} viewer={viewer} canCreate={viewer?.permission_state === 'granted'} />
</div>
</li>
)