2026-07-04 14:47:51 +08:00
/ * *
2026-07-12 03:36:43 +08:00
* Shared source of truth for the RFC index : the tree walker ( structure rules ) and the README
* table renderer . ` gen-rfc-index.ts ` writes the generated regions ;
* ` verify-rfc-classification.ts ` checks structure and asserts the committed regions are fresh .
2026-07-13 23:27:00 +08:00
* Lifecycle and class sets are closed under ` docs/rfc/README.md ` ; rows derive
* from path , H1 , and filename date and sort deterministically . Import is pure .
2026-07-04 14:47:51 +08:00
* /
2026-07-04 15:10:22 +08:00
import { readFileSync , readdirSync } from 'node:fs'
2026-07-06 02:28:44 +08:00
import { resolve , sep } from 'node:path'
2026-07-04 14:47:51 +08:00
import { globSync } from 'node:fs'
export const rfcRoot = resolve ( import . meta . dirname , '../docs/rfc' )
/** The closed set of RFC lifecycles (top-level folders under docs/rfc/). */
2026-07-04 14:52:09 +08:00
const LIFECYCLES = [ 'proposed' , 'implemented' , 'rejected' ] as const
2026-07-04 14:47:51 +08:00
/ * *
* The closed set of RFC classes ( nested folder under each lifecycle ) . Adding a
* class is a deliberate act : extend this list AND the README ' s Classification
* section . The gate rejects any folder not listed here .
* /
2026-07-04 14:52:09 +08:00
const CLASSES = [ 'feature' , 'bug-fix' , 'simplification' , 'architecture' , 'process' , 'testing' ] as const
2026-07-04 14:47:51 +08:00
/** Non-RFC Markdown allowed to sit directly at a lifecycle root. */
const ROOT_ALLOWLIST = new Set ( [ 'AGENTS.md' , 'CLAUDE.md' ] )
/** Title-case a class/lifecycle folder name for a README heading. */
2026-07-04 14:52:09 +08:00
const heading = ( s : string ) : string = > s . charAt ( 0 ) . toUpperCase ( ) + s . slice ( 1 )
2026-07-04 14:47:51 +08:00
/** One RFC file, as discovered by the walker. */
export interface Rfc {
lifecycle : string
cls : string
base : string
/** Path relative to docs/rfc — the README link target. */
rel : string
/** H1 text with any `RFC: ` prefix stripped — the README row title. */
title : string
/** `yyyy-mm-dd` from the filename — the "First proposed" column. */
date : string
}
/ * *
* Walk the RFC tree , enforcing the structure rules . Returns every valid RFC
2026-07-04 15:10:22 +08:00
* plus one error string per violation ( unknown lifecycle or class folder , bad
* depth , bad filename , missing / malformed H1 ) . Callers treat a non - empty error
* list as fatal — the index is only generated from a structurally valid tree .
2026-07-04 14:47:51 +08:00
* /
export function walkRfcTree ( ) : { rfcs : Rfc [ ] ; errors : string [ ] } {
const rfcs : Rfc [ ] = [ ]
const errors : string [ ] = [ ]
2026-07-04 15:10:22 +08:00
// The lifecycle set is closed too: any directory under docs/rfc/ that is not
// a known lifecycle would otherwise hold RFCs invisible to the walk below.
for ( const entry of readdirSync ( rfcRoot , { withFileTypes : true } ) ) {
if ( entry . isDirectory ( ) && ! ( LIFECYCLES as readonly string [ ] ) . includes ( entry . name ) ) {
errors . push ( ` structure: ${ entry . name } / — unknown lifecycle folder (allowed: ${ LIFECYCLES . join ( ', ' ) } ) ` )
}
}
2026-07-04 14:47:51 +08:00
for ( const lifecycle of LIFECYCLES ) {
2026-07-06 02:28:44 +08:00
for ( const match of globSync ( ` ${ lifecycle } /**/*.md ` , { cwd : rfcRoot } ) . map ( path = > path . split ( sep ) . join ( '/' ) ) . sort ( ) ) {
2026-07-04 14:47:51 +08:00
const segs = match . split ( '/' )
// Allowlisted file directly at the lifecycle root (e.g. implemented/AGENTS.md).
if ( segs . length === 2 && ROOT_ALLOWLIST . has ( segs [ 1 ] ? ? '' ) ) continue
// A Chinese counterpart (foo.zh.md, docs/i18n/README.md) is the SAME RFC,
// indexed via its English filename; the pairing gate owns its consistency.
if ( match . endsWith ( '.zh.md' ) ) continue
const cls = segs [ 1 ]
const base = segs [ 2 ]
if ( segs . length !== 3 || cls === undefined || base === undefined ) {
errors . push ( ` structure: ${ match } — expected {lifecycle}/{class}/file.md (got depth ${ segs . length } ) ` )
continue
}
if ( ! ( CLASSES as readonly string [ ] ) . includes ( cls ) ) {
errors . push ( ` structure: ${ match } — unknown class folder " ${ cls } " (allowed: ${ CLASSES . join ( ', ' ) } ) ` )
continue
}
if ( ! /^\d{4}-\d{2}-\d{2}-.+\.md$/ . test ( base ) ) {
errors . push ( ` structure: ${ match } — filename must be yyyy-mm-dd-topic.md ` )
continue
}
const firstLine = readFileSync ( resolve ( rfcRoot , match ) , 'utf8' ) . split ( '\n' , 1 ) [ 0 ] ? ? ''
const h1 = /^#\s+(?:RFC:\s+)?(.+?)\s*$/ . exec ( firstLine )
if ( ! h1 ? . [ 1 ] ) {
errors . push ( ` title: ${ match } — first line must be an H1 ( \` # RFC: <title> \` or \` # <title> \` ), got: ${ JSON . stringify ( firstLine ) } ` )
continue
}
rfcs . push ( { lifecycle , cls , base , rel : match , title : h1 [ 1 ] , date : base.slice ( 0 , 10 ) } )
}
}
return { rfcs , errors }
}
/ * *
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
* Render one lifecycle ' s section body : a ` ### {Class} ` heading plus a
2026-07-04 14:47:51 +08:00
* ` | Title | First proposed | ` table for every non - empty class , in CLASSES
* order , rows sorted by date then filename .
* /
2026-07-04 14:52:09 +08:00
function renderLifecycle ( rfcs : Rfc [ ] , lifecycle : string ) : string {
2026-07-04 14:47:51 +08:00
const sections : string [ ] = [ ]
for ( const cls of CLASSES ) {
const rows = rfcs
. filter ( r = > r . lifecycle === lifecycle && r . cls === cls )
. sort ( ( a , b ) = > a . date . localeCompare ( b . date ) || a . base . localeCompare ( b . base ) )
if ( rows . length === 0 ) continue
const table = rows . map ( r = > ` | [ ${ r . title } ]( ${ r . rel } ) | ${ r . date } | ` ) . join ( '\n' )
sections . push ( ` ### ${ heading ( cls ) } \ n \ n| Title | First proposed | \ n|---|---| \ n ${ table } ` )
}
return sections . join ( '\n\n' )
}
/ * *
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
* Render the complete ` docs/rfc/INDEX.md ` content : a generated - file banner
* followed by one ` ## {Lifecycle} ` section per lifecycle in canonical order .
* The whole file is generated state — there is no curated region to preserve .
2026-07-04 14:47:51 +08:00
* /
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
export function renderIndex ( rfcs : Rfc [ ] ) : string {
const parts = [
'# RFC index' ,
'' ,
'Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand; `verify-rfc-classification` fails when this file is stale. The curated front door — layout, classification, when to write one, and the in-file format — is [README.md](README.md).' ,
]
2026-07-04 14:47:51 +08:00
for ( const lifecycle of LIFECYCLES ) {
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
parts . push ( '' , ` ## ${ heading ( lifecycle ) } ` , '' , renderLifecycle ( rfcs , lifecycle ) )
2026-07-04 15:10:22 +08:00
}
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
return ` ${ parts . join ( '\n' ) } \ n `
2026-07-04 14:47:51 +08:00
}
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
/** Matches an index-shaped table row (a `| [title](lifecycle/…) |` line) — generated state that must not appear in curated prose. */
export const INDEX_ROW = /^\|\s*\[[^\]]+\]\((?:proposed|implemented|rejected)\//