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>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
// LinkedText.jsx — roadmap #28 Part 1.
|
||||
//
|
||||
// Renders a backend-provided list of text/rfc-link segments (see
|
||||
// backend/app/rfc_links.py). RFC references in PR descriptions and
|
||||
// comments arrive pre-scanned as structured segments — this component
|
||||
// maps them onto plain text runs and anchor elements. It never renders
|
||||
// HTML from the server (no dangerouslySetInnerHTML), so the surface is
|
||||
// XSS-safe regardless of what a comment author typed.
|
||||
//
|
||||
// `segments` is the enriched array; `text` is the raw fallback used when
|
||||
// the field is absent (an older cached response, or a caller that didn't
|
||||
// pass segments). Either way the visible text is identical — only the
|
||||
// links differ.
|
||||
|
||||
export default function LinkedText({ segments, text }) {
|
||||
if (!Array.isArray(segments) || segments.length === 0) {
|
||||
return <>{text ?? ''}</>
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{segments.map((seg, i) => {
|
||||
if (seg.type === 'rfc') {
|
||||
return (
|
||||
<a
|
||||
key={i}
|
||||
className="rfc-autolink"
|
||||
href={`/rfc/${seg.slug}`}
|
||||
title={seg.title ? `RFC: ${seg.title}` : undefined}
|
||||
>
|
||||
{seg.label}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
return <span key={i}>{seg.text}</span>
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
withdrawPR,
|
||||
} from '../api'
|
||||
import { EVENTS, track } from '../lib/analytics'
|
||||
import LinkedText from './LinkedText'
|
||||
|
||||
export default function PRView({ viewer }) {
|
||||
const { slug, prNumber: prNumberParam } = useParams()
|
||||
@@ -218,7 +219,9 @@ export default function PRView({ viewer }) {
|
||||
<>
|
||||
<h1 className="pr-title">{pr.title}</h1>
|
||||
{pr.description && (
|
||||
<p className="pr-description">{pr.description}</p>
|
||||
<p className="pr-description">
|
||||
<LinkedText segments={pr.description_segments} text={pr.description} />
|
||||
</p>
|
||||
)}
|
||||
{/* #26: the optional ground-truth use case for this change,
|
||||
captured when the PR was opened. Muted "left blank"
|
||||
@@ -440,7 +443,9 @@ function PRConversation({ threads, messagesByThread, threadsByKind, seenMsgId })
|
||||
{isNew && <span className="chat-msg-new-pip" title="New since your last visit">●</span>}
|
||||
</div>
|
||||
{m.quote && <pre className="chat-msg-quote">{m.quote}</pre>}
|
||||
<div className="chat-msg-body">{m.text}</div>
|
||||
<div className="chat-msg-body">
|
||||
<LinkedText segments={m.text_segments} text={m.text} />
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
resolveDiscussionThread,
|
||||
} from '../api'
|
||||
import { EVENTS, track } from '../lib/analytics'
|
||||
import LinkedText from './LinkedText'
|
||||
|
||||
export default function RFCDiscussionPanel({ slug, viewer }) {
|
||||
const [threads, setThreads] = useState([])
|
||||
@@ -279,7 +280,9 @@ function DiscussionMessage({ message }) {
|
||||
{message.quote && (
|
||||
<div className="discussion-message-quote">"{message.quote}"</div>
|
||||
)}
|
||||
<div className="discussion-message-body">{message.text}</div>
|
||||
<div className="discussion-message-body">
|
||||
<LinkedText segments={message.text_segments} text={message.text} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user