docs(rfc): define and enforce a uniform RFC format; adopt it across the corpus
Define the in-file RFC contract in docs/rfc/README.md § The file format:
the header block (`# RFC: <title>` plus a dateless Status enum
cross-checked against the lifecycle folder), the per-lifecycle body
skeleton (a Problem opener everywhere; Proposal/Alternatives considered/
Acceptance criteria/Risks in proposed/; present-tense Decision/
Consequences with proposal-era headings banned in implemented/; the
frozen proposal shape in rejected/), and a mandatory Alternatives
considered section with a date-fenced grandfather comment for pre-format
RFCs whose alternatives are not reconstructible from the record.
Enforce it with a new doc-sync gate, scripts/verify-rfc-format.ts, and
normalize all 112 RFCs to it: ~15 Status-line spellings collapse to the
enum, 29 Context openers become Problem, the 39 legacy-format XXX debt
markers are resolved and banned from reappearing, proposal-era sections
in implemented RFCs are rewritten to shipped reality (including the
web/fs/subagent seam RFCs' migration plans and test checklists, closing
the doc-tiers deferred-work item on the web seam), every RFC gains an
Alternatives considered section or the grandfather comment, and the
bilingual pair is re-mirrored and re-recorded.
Move the generated index tables out of README.md into a fully generated
docs/rfc/INDEX.md — gen-rfc-index now writes the whole file, and
verify-rfc-classification checks its freshness and rejects index-shaped
rows in the curated README — which makes room for the format contract to
live in the README front door instead of a separate FORMAT.md.
The decision record, and the first RFC written in the new format, is
docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.md.
2026-07-05 22:58:25 +08:00
/ * *
* Doc - sync gate : enforce the RFC in - file format
* ( [ README . md § The file format ] ( . . / docs / rfc / README . md ) , the contract ; rationale in
* [ the uniform - format RFC ] ( . . / docs / rfc / implemented / process / 2026 - 07 - 05 - uniform - rfc - format . md ) ) .
* The classification gate owns WHERE a file sits and how it is named ; this gate
* owns what is INSIDE : the header block , the per - lifecycle body skeleton , and
* the Alternatives - considered mandate .
*
* Per English RFC ( ` .zh.md ` counterparts are the pairing gate ' s concern ) :
*
* 1 . HEADER — line 1 is ` # RFC: <title> ` , line 2 blank , line 3 the one
* ` Status: ` line in the file , line 4 blank . The status is the dateless enum
* matching the lifecycle folder : ` Status: proposed ` , ` Status: implemented ` ,
* or ` Status: rejected — <reason> ` .
* 2 . SKELETON — the first ` ## ` section is ` ## Problem ` ; the lifecycle ' s
* required sections are present under their canonical names ( ` proposed/ ` :
* Proposal , Acceptance criteria , Risks ; ` implemented/ ` : Decision ,
* Consequences ; ` rejected/ ` : Proposal ) ; ` implemented/ ` must not carry the
* proposal - era headings ( Proposal , Plan , Migration plan , Acceptance
* criteria ) that the docs standard ' s slop checklist outlaws there .
* 3 . ALTERNATIVES — ` ## Alternatives considered ` is present , or the file is a
* pre - format RFC ( dated before the format landed ) carrying the exact
* grandfather comment instead . Carrying both , or grandfathering a
* post - format RFC , fails .
* 4 . DEBT MARKER — the retired legacy - format debt comment may not reappear .
*
* Checker , not fixer : it reports and never rewrites .
* Run : ` tsx scripts/verify-rfc-format.ts ` .
* /
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { rfcRoot , walkRfcTree } from './rfc-index.ts'
/** The date the format contract landed; the grandfather comment is valid only before it. */
const FORMAT_ADOPTED = '2026-07-05'
/** The exact comment a pre-format RFC carries in place of `## Alternatives considered`. */
const GRANDFATHER = '<!-- rfc-format: alternatives-not-recorded (pre-format RFC) -->'
/** The retired debt marker that flagged pre-format bodies; banned so it cannot creep back. */
const LEGACY_MARKER = 'XXX: legacy ADR/RFC body format'
/** Status-line grammar per lifecycle folder. */
const STATUS : Record < string , RegExp > = {
proposed : /^Status: proposed$/ ,
implemented : /^Status: implemented$/ ,
rejected : /^Status: rejected — .+$/ ,
}
/** Required `##` headings per lifecycle, beyond the universal `## Problem` opener. */
const REQUIRED : Record < string , string [ ] > = {
proposed : [ '## Proposal' , '## Acceptance criteria' , '## Risks' ] ,
implemented : [ '## Decision' , '## Consequences' ] ,
rejected : [ '## Proposal' ] ,
}
/** Headings banned in `implemented/` — proposal-era spec-speak per the slop checklist. */
const BANNED_IMPLEMENTED = /^## (?:Proposal\b|Plan\b|Migration plan\b|Acceptance criteria\b)/i
const { rfcs , errors } = walkRfcTree ( )
for ( const rfc of rfcs ) {
const fail = ( msg : string ) : void = > {
errors . push ( ` format: ${ rfc . rel } — ${ msg } ` )
}
const lines = readFileSync ( resolve ( rfcRoot , rfc . rel ) , 'utf8' ) . split ( '\n' )
docs(rfc): address Codex review — fence-aware format gate, exact corpus counts
Two findings from the pre-ready review, both verified:
- verify-rfc-format scanned raw lines, so an RFC quoting a Status line,
a banned heading, or the grandfather comment inside a fenced example
would false-positive. The content scans (duplicate Status, H2 headings,
banned headings, grandfather, legacy marker) now ignore fenced blocks;
the positional header-block checks stay raw. Verified: a fenced
'Status: implemented' + '## Plan' + grandfather quote inside a walked
RFC no longer trips the gate.
- The companion RFC's pre-format corpus counts were imprecise: 27
distinct Status spellings (reasons collapsed), not 'some fifteen';
nineteen implemented files carrying thirty proposal-era heading
occurrences, not 'over twenty files'; three English files (plus one zh
counterpart) with no status, not four.
2026-07-05 23:45:34 +08:00
// Content scans ignore fenced code blocks: an RFC may legitimately QUOTE a
// status line, a banned heading, or the grandfather comment inside a fence
// (the README's own format section does), and only real prose counts.
let inFence = false
const prose = lines . filter ( ( l ) = > {
if ( l . startsWith ( '```' ) ) {
inFence = ! inFence
return false
}
return ! inFence
} )
docs(rfc): define and enforce a uniform RFC format; adopt it across the corpus
Define the in-file RFC contract in docs/rfc/README.md § The file format:
the header block (`# RFC: <title>` plus a dateless Status enum
cross-checked against the lifecycle folder), the per-lifecycle body
skeleton (a Problem opener everywhere; Proposal/Alternatives considered/
Acceptance criteria/Risks in proposed/; present-tense Decision/
Consequences with proposal-era headings banned in implemented/; the
frozen proposal shape in rejected/), and a mandatory Alternatives
considered section with a date-fenced grandfather comment for pre-format
RFCs whose alternatives are not reconstructible from the record.
Enforce it with a new doc-sync gate, scripts/verify-rfc-format.ts, and
normalize all 112 RFCs to it: ~15 Status-line spellings collapse to the
enum, 29 Context openers become Problem, the 39 legacy-format XXX debt
markers are resolved and banned from reappearing, proposal-era sections
in implemented RFCs are rewritten to shipped reality (including the
web/fs/subagent seam RFCs' migration plans and test checklists, closing
the doc-tiers deferred-work item on the web seam), every RFC gains an
Alternatives considered section or the grandfather comment, and the
bilingual pair is re-mirrored and re-recorded.
Move the generated index tables out of README.md into a fully generated
docs/rfc/INDEX.md — gen-rfc-index now writes the whole file, and
verify-rfc-classification checks its freshness and rejects index-shaped
rows in the curated README — which makes room for the format contract to
live in the README front door instead of a separate FORMAT.md.
The decision record, and the first RFC written in the new format, is
docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.md.
2026-07-05 22:58:25 +08:00
if ( ! /^# RFC: \S/ . test ( lines [ 0 ] ? ? '' ) ) fail ( 'line 1 must be `# RFC: <title>`' )
if ( lines [ 1 ] !== '' ) fail ( 'line 2 must be blank' )
const status = STATUS [ rfc . lifecycle ]
if ( status !== undefined && ! status . test ( lines [ 2 ] ? ? '' ) ) {
fail ( ` line 3 must match the ${ rfc . lifecycle } status grammar ( ${ String ( status ) } ) ` )
}
if ( lines [ 3 ] !== '' ) fail ( 'line 4 must be blank' )
docs(rfc): address Codex review — fence-aware format gate, exact corpus counts
Two findings from the pre-ready review, both verified:
- verify-rfc-format scanned raw lines, so an RFC quoting a Status line,
a banned heading, or the grandfather comment inside a fenced example
would false-positive. The content scans (duplicate Status, H2 headings,
banned headings, grandfather, legacy marker) now ignore fenced blocks;
the positional header-block checks stay raw. Verified: a fenced
'Status: implemented' + '## Plan' + grandfather quote inside a walked
RFC no longer trips the gate.
- The companion RFC's pre-format corpus counts were imprecise: 27
distinct Status spellings (reasons collapsed), not 'some fifteen';
nineteen implemented files carrying thirty proposal-era heading
occurrences, not 'over twenty files'; three English files (plus one zh
counterpart) with no status, not four.
2026-07-05 23:45:34 +08:00
const statusLines = prose . filter ( l = > l . startsWith ( 'Status:' ) && l !== lines [ 2 ] )
if ( statusLines . length > 0 || prose . filter ( l = > l === lines [ 2 ] ) . length > 1 ) {
fail ( 'the line-3 `Status:` line must be the only one in the file' )
}
docs(rfc): define and enforce a uniform RFC format; adopt it across the corpus
Define the in-file RFC contract in docs/rfc/README.md § The file format:
the header block (`# RFC: <title>` plus a dateless Status enum
cross-checked against the lifecycle folder), the per-lifecycle body
skeleton (a Problem opener everywhere; Proposal/Alternatives considered/
Acceptance criteria/Risks in proposed/; present-tense Decision/
Consequences with proposal-era headings banned in implemented/; the
frozen proposal shape in rejected/), and a mandatory Alternatives
considered section with a date-fenced grandfather comment for pre-format
RFCs whose alternatives are not reconstructible from the record.
Enforce it with a new doc-sync gate, scripts/verify-rfc-format.ts, and
normalize all 112 RFCs to it: ~15 Status-line spellings collapse to the
enum, 29 Context openers become Problem, the 39 legacy-format XXX debt
markers are resolved and banned from reappearing, proposal-era sections
in implemented RFCs are rewritten to shipped reality (including the
web/fs/subagent seam RFCs' migration plans and test checklists, closing
the doc-tiers deferred-work item on the web seam), every RFC gains an
Alternatives considered section or the grandfather comment, and the
bilingual pair is re-mirrored and re-recorded.
Move the generated index tables out of README.md into a fully generated
docs/rfc/INDEX.md — gen-rfc-index now writes the whole file, and
verify-rfc-classification checks its freshness and rejects index-shaped
rows in the curated README — which makes room for the format contract to
live in the README front door instead of a separate FORMAT.md.
The decision record, and the first RFC written in the new format, is
docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.md.
2026-07-05 22:58:25 +08:00
docs(rfc): address Codex review — fence-aware format gate, exact corpus counts
Two findings from the pre-ready review, both verified:
- verify-rfc-format scanned raw lines, so an RFC quoting a Status line,
a banned heading, or the grandfather comment inside a fenced example
would false-positive. The content scans (duplicate Status, H2 headings,
banned headings, grandfather, legacy marker) now ignore fenced blocks;
the positional header-block checks stay raw. Verified: a fenced
'Status: implemented' + '## Plan' + grandfather quote inside a walked
RFC no longer trips the gate.
- The companion RFC's pre-format corpus counts were imprecise: 27
distinct Status spellings (reasons collapsed), not 'some fifteen';
nineteen implemented files carrying thirty proposal-era heading
occurrences, not 'over twenty files'; three English files (plus one zh
counterpart) with no status, not four.
2026-07-05 23:45:34 +08:00
const h2s = prose . filter ( l = > l . startsWith ( '## ' ) ) . map ( l = > l . trimEnd ( ) )
docs(rfc): define and enforce a uniform RFC format; adopt it across the corpus
Define the in-file RFC contract in docs/rfc/README.md § The file format:
the header block (`# RFC: <title>` plus a dateless Status enum
cross-checked against the lifecycle folder), the per-lifecycle body
skeleton (a Problem opener everywhere; Proposal/Alternatives considered/
Acceptance criteria/Risks in proposed/; present-tense Decision/
Consequences with proposal-era headings banned in implemented/; the
frozen proposal shape in rejected/), and a mandatory Alternatives
considered section with a date-fenced grandfather comment for pre-format
RFCs whose alternatives are not reconstructible from the record.
Enforce it with a new doc-sync gate, scripts/verify-rfc-format.ts, and
normalize all 112 RFCs to it: ~15 Status-line spellings collapse to the
enum, 29 Context openers become Problem, the 39 legacy-format XXX debt
markers are resolved and banned from reappearing, proposal-era sections
in implemented RFCs are rewritten to shipped reality (including the
web/fs/subagent seam RFCs' migration plans and test checklists, closing
the doc-tiers deferred-work item on the web seam), every RFC gains an
Alternatives considered section or the grandfather comment, and the
bilingual pair is re-mirrored and re-recorded.
Move the generated index tables out of README.md into a fully generated
docs/rfc/INDEX.md — gen-rfc-index now writes the whole file, and
verify-rfc-classification checks its freshness and rejects index-shaped
rows in the curated README — which makes room for the format contract to
live in the README front door instead of a separate FORMAT.md.
The decision record, and the first RFC written in the new format, is
docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.md.
2026-07-05 22:58:25 +08:00
if ( h2s [ 0 ] !== '## Problem' ) fail ( ` the first section must be \` ## Problem \` (got ${ JSON . stringify ( h2s [ 0 ] ? ? '<none>' ) } ) ` )
for ( const required of REQUIRED [ rfc . lifecycle ] ? ? [ ] ) {
if ( ! h2s . includes ( required ) ) fail ( ` missing the required \` ${ required } \` section ` )
}
if ( rfc . lifecycle === 'implemented' ) {
for ( const h2 of h2s . filter ( h = > BANNED_IMPLEMENTED . test ( h ) ) ) {
fail ( ` \` ${ h2 } \` is a proposal-era heading; an implemented RFC states what is (fold it into Decision/Consequences/Testing) ` )
}
}
const hasSection = h2s . includes ( '## Alternatives considered' )
docs(rfc): address Codex review — fence-aware format gate, exact corpus counts
Two findings from the pre-ready review, both verified:
- verify-rfc-format scanned raw lines, so an RFC quoting a Status line,
a banned heading, or the grandfather comment inside a fenced example
would false-positive. The content scans (duplicate Status, H2 headings,
banned headings, grandfather, legacy marker) now ignore fenced blocks;
the positional header-block checks stay raw. Verified: a fenced
'Status: implemented' + '## Plan' + grandfather quote inside a walked
RFC no longer trips the gate.
- The companion RFC's pre-format corpus counts were imprecise: 27
distinct Status spellings (reasons collapsed), not 'some fifteen';
nineteen implemented files carrying thirty proposal-era heading
occurrences, not 'over twenty files'; three English files (plus one zh
counterpart) with no status, not four.
2026-07-05 23:45:34 +08:00
const hasGrandfather = prose . includes ( GRANDFATHER )
docs(rfc): define and enforce a uniform RFC format; adopt it across the corpus
Define the in-file RFC contract in docs/rfc/README.md § The file format:
the header block (`# RFC: <title>` plus a dateless Status enum
cross-checked against the lifecycle folder), the per-lifecycle body
skeleton (a Problem opener everywhere; Proposal/Alternatives considered/
Acceptance criteria/Risks in proposed/; present-tense Decision/
Consequences with proposal-era headings banned in implemented/; the
frozen proposal shape in rejected/), and a mandatory Alternatives
considered section with a date-fenced grandfather comment for pre-format
RFCs whose alternatives are not reconstructible from the record.
Enforce it with a new doc-sync gate, scripts/verify-rfc-format.ts, and
normalize all 112 RFCs to it: ~15 Status-line spellings collapse to the
enum, 29 Context openers become Problem, the 39 legacy-format XXX debt
markers are resolved and banned from reappearing, proposal-era sections
in implemented RFCs are rewritten to shipped reality (including the
web/fs/subagent seam RFCs' migration plans and test checklists, closing
the doc-tiers deferred-work item on the web seam), every RFC gains an
Alternatives considered section or the grandfather comment, and the
bilingual pair is re-mirrored and re-recorded.
Move the generated index tables out of README.md into a fully generated
docs/rfc/INDEX.md — gen-rfc-index now writes the whole file, and
verify-rfc-classification checks its freshness and rejects index-shaped
rows in the curated README — which makes room for the format contract to
live in the README front door instead of a separate FORMAT.md.
The decision record, and the first RFC written in the new format, is
docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.md.
2026-07-05 22:58:25 +08:00
if ( hasSection && hasGrandfather ) fail ( 'carries both `## Alternatives considered` and the grandfather comment — drop the comment' )
if ( ! hasSection && ! hasGrandfather ) fail ( 'missing `## Alternatives considered` (a pre-format RFC whose alternatives are not reconstructible carries the grandfather comment instead — see docs/rfc/README.md § The file format)' )
if ( hasGrandfather && rfc . date >= FORMAT_ADOPTED ) fail ( ` the grandfather comment is only valid for RFCs dated before ${ FORMAT_ADOPTED } ` )
docs(rfc): address Codex review — fence-aware format gate, exact corpus counts
Two findings from the pre-ready review, both verified:
- verify-rfc-format scanned raw lines, so an RFC quoting a Status line,
a banned heading, or the grandfather comment inside a fenced example
would false-positive. The content scans (duplicate Status, H2 headings,
banned headings, grandfather, legacy marker) now ignore fenced blocks;
the positional header-block checks stay raw. Verified: a fenced
'Status: implemented' + '## Plan' + grandfather quote inside a walked
RFC no longer trips the gate.
- The companion RFC's pre-format corpus counts were imprecise: 27
distinct Status spellings (reasons collapsed), not 'some fifteen';
nineteen implemented files carrying thirty proposal-era heading
occurrences, not 'over twenty files'; three English files (plus one zh
counterpart) with no status, not four.
2026-07-05 23:45:34 +08:00
if ( prose . some ( l = > l . includes ( LEGACY_MARKER ) ) ) fail ( 'carries the retired legacy-format debt marker' )
docs(rfc): define and enforce a uniform RFC format; adopt it across the corpus
Define the in-file RFC contract in docs/rfc/README.md § The file format:
the header block (`# RFC: <title>` plus a dateless Status enum
cross-checked against the lifecycle folder), the per-lifecycle body
skeleton (a Problem opener everywhere; Proposal/Alternatives considered/
Acceptance criteria/Risks in proposed/; present-tense Decision/
Consequences with proposal-era headings banned in implemented/; the
frozen proposal shape in rejected/), and a mandatory Alternatives
considered section with a date-fenced grandfather comment for pre-format
RFCs whose alternatives are not reconstructible from the record.
Enforce it with a new doc-sync gate, scripts/verify-rfc-format.ts, and
normalize all 112 RFCs to it: ~15 Status-line spellings collapse to the
enum, 29 Context openers become Problem, the 39 legacy-format XXX debt
markers are resolved and banned from reappearing, proposal-era sections
in implemented RFCs are rewritten to shipped reality (including the
web/fs/subagent seam RFCs' migration plans and test checklists, closing
the doc-tiers deferred-work item on the web seam), every RFC gains an
Alternatives considered section or the grandfather comment, and the
bilingual pair is re-mirrored and re-recorded.
Move the generated index tables out of README.md into a fully generated
docs/rfc/INDEX.md — gen-rfc-index now writes the whole file, and
verify-rfc-classification checks its freshness and rejects index-shaped
rows in the curated README — which makes room for the format contract to
live in the README front door instead of a separate FORMAT.md.
The decision record, and the first RFC written in the new format, is
docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.md.
2026-07-05 22:58:25 +08:00
}
if ( errors . length === 0 ) {
console . log ( ` verify-rfc-format: ${ rfcs . length } RFC(s) checked, all conform to docs/rfc/README.md § The file format. ` )
process . exit ( 0 )
}
console . error ( 'verify-rfc-format: violations found:' )
for ( const e of errors ) console . error ( ` ${ e } ` )
process . exit ( 1 )