§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>
This commit is contained in:
@@ -109,20 +109,25 @@ export default function GraduateDialog({ slug, entry, onClose, onCompleted }) {
|
||||
<p className="modal-intro">
|
||||
§13: graduate the super-draft to active. This is an in-place
|
||||
state flip — the entry keeps its body and stays in the meta
|
||||
repo; only the frontmatter changes (state, integer ID, and the
|
||||
graduation stamps). No new repository is created.
|
||||
repo; only the frontmatter changes (state, the optional
|
||||
integer ID, and the graduation stamps). No new repository is
|
||||
created.
|
||||
</p>
|
||||
|
||||
<div className="form-row">
|
||||
<label>Integer ID</label>
|
||||
<label>Integer ID <span className="field-optional">(optional)</span></label>
|
||||
<input
|
||||
type="text"
|
||||
value={rfcId}
|
||||
onChange={(e) => setRfcId(e.target.value.trim())}
|
||||
placeholder="RFC-NNNN"
|
||||
placeholder="RFC-NNNN — or leave blank"
|
||||
disabled={phase !== 'idle'}
|
||||
/>
|
||||
<p className="field-help">Pre-filled as the next free integer; editable to reserve gaps.</p>
|
||||
<p className="field-help">
|
||||
Pre-filled as the next free integer; editable to reserve gaps,
|
||||
or <strong>clear it to graduate without a number</strong> — the
|
||||
slug stays the canonical identifier.
|
||||
</p>
|
||||
{idError && <p className="field-error">{idError}</p>}
|
||||
</div>
|
||||
|
||||
@@ -186,8 +191,10 @@ export default function GraduateDialog({ slug, entry, onClose, onCompleted }) {
|
||||
<div className="graduation-complete">
|
||||
<h3>Graduation complete</h3>
|
||||
<p>
|
||||
`{slug}` is now active as <strong>{streamState?.rfc_id}</strong>.
|
||||
The catalog and the RFC view reflect the new state.
|
||||
{streamState?.rfc_id
|
||||
? <>`{slug}` is now active as <strong>{streamState.rfc_id}</strong>.</>
|
||||
: <>`{slug}` is now active (no number — identified by its slug).</>}
|
||||
{' '}The catalog and the RFC view reflect the new state.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user