2026-07-10 01:51:50 +08:00
/**
2026-07-14 14:37:16 +08:00
* Doc-sync gate for the canonical package-README limitations section. It scans
* package manifests, rejects missing or variant sections, and requires one
* top-level bullet; audited packages in {@link NO_LIMITATIONS} must omit it.
2026-07-19 22:50:49 +08:00
* See the [limitations Agent Note](../.agents/notes/implemented/process/2026-07-10-readme-known-limitations-gate.md).
2026-07-10 01:51:50 +08:00
*/
2026-07-12 02:08:35 +08:00
import { existsSync , globSync , readFileSync } from 'node:fs'
2026-07-06 02:28:44 +08:00
import { resolve , sep } from 'node:path'
2026-07-14 14:01:35 +08:00
import { markdownHeadingLines , markdownProseLines } from './markdown.ts'
2026-07-10 01:51:50 +08:00
const root = resolve ( import . meta . dirname , '..' )
/** The one canonical section heading, required verbatim as an h2. */
const CANONICAL = '## Known Limitations and Deferred Work'
2026-07-14 14:37:16 +08:00
/** Packages audited as having no limitations section, keyed by repo-relative directory. */
2026-07-12 02:08:35 +08:00
const NO_LIMITATIONS : Readonly < Record < string , string > > = {
'packages/util/brand' : 'Type-only nominal-branding primitive with no runtime behavior or deferred work.' ,
}
2026-07-10 01:51:50 +08:00
/** A heading that reads as a limitations section — canonical or drifted. */
function isLimitationsLike ( headingText : string ) : boolean {
return (
2026-07-12 02:08:35 +08:00
/\blimitations?\b/i . test ( headingText )
2026-07-10 01:51:50 +08:00
|| /deferred work/i . test ( headingText )
|| /what is not here/i . test ( headingText )
|| /^deferred\b/i . test ( headingText )
2026-07-12 02:08:35 +08:00
|| /^non-goals?\b/i . test ( headingText )
2026-07-10 01:51:50 +08:00
)
}
2026-07-06 02:28:44 +08:00
const packageJsons = globSync ( 'packages/*/*/package.json' , { cwd : root } ) . map ( path = > path . split ( sep ) . join ( '/' ) ) . sort ( )
2026-07-12 02:08:35 +08:00
const scannedPackages = new Set ( packageJsons . map ( path = > path . slice ( 0 , - '/package.json' . length ) ) )
2026-07-10 01:51:50 +08:00
const failures : string [ ] = [ ]
2026-07-12 02:08:35 +08:00
for ( const [ entry , reason ] of Object . entries ( NO_LIMITATIONS ) ) {
2026-07-10 01:51:50 +08:00
if ( ! scannedPackages . has ( entry ) ) {
2026-07-14 00:22:52 +08:00
failures . push ( ` whitelist entry ${ entry } does not name a scanned package — renamed or removed? update NO_LIMITATIONS in scripts/verify-package-readme-limitations.ts in the same change ` )
2026-07-10 01:51:50 +08:00
}
2026-07-12 02:08:35 +08:00
if ( reason . trim ( ) . length === 0 ) {
failures . push ( ` whitelist entry ${ entry } has no justification — state why a limitations section would be empty boilerplate ` )
}
2026-07-10 01:51:50 +08:00
}
2026-07-12 02:08:35 +08:00
for ( const pkg of scannedPackages ) {
const readme = ` ${ pkg } /README.md `
if ( ! existsSync ( resolve ( root , readme ) ) ) {
failures . push ( ` ${ readme } : package manifest has no sibling README with the \` ${ CANONICAL } \` section ` )
continue
}
2026-07-14 14:01:35 +08:00
const source = readFileSync ( resolve ( root , readme ) , 'utf8' )
const lines = markdownProseLines ( source )
const headings = markdownHeadingLines ( source )
const limitations = headings . filter ( heading = > isLimitationsLike ( heading . text ) )
2026-07-10 01:51:50 +08:00
2026-07-12 02:08:35 +08:00
if ( Object . hasOwn ( NO_LIMITATIONS , pkg ) ) {
2026-07-10 01:51:50 +08:00
for ( const heading of limitations ) {
failures . push ( ` ${ readme } : ${ heading . index } : whitelisted as having no known limitations, but carries ${ JSON . stringify ( heading . raw ) } — drop the section or remove the package from NO_LIMITATIONS ` )
}
continue
}
const heading = limitations . at ( 0 )
if ( heading === undefined ) {
2026-07-14 00:22:52 +08:00
failures . push ( ` ${ readme } : missing the \` ${ CANONICAL } \` section (a package with genuinely nothing to declare joins NO_LIMITATIONS in scripts/verify-package-readme-limitations.ts instead) ` )
2026-07-10 01:51:50 +08:00
continue
}
if ( limitations . length > 1 ) {
failures . push ( ` ${ readme } : ${ limitations . length } limitations-like headings (lines ${ limitations . map ( line = > line . index ) . join ( ', ' ) } ) — keep exactly one \` ${ CANONICAL } \` section ` )
continue
}
2026-07-14 14:01:35 +08:00
if ( heading . depth !== 2 || heading . raw . trimEnd ( ) !== CANONICAL ) {
2026-07-10 01:51:50 +08:00
failures . push ( ` ${ readme } : ${ heading . index } : non-canonical heading ${ JSON . stringify ( heading . raw ) } — use \` ${ CANONICAL } \` ` )
continue
}
2026-07-14 14:01:35 +08:00
const headingAt = lines . findIndex ( line = > line . index === heading . index )
2026-07-10 01:51:50 +08:00
const body = lines . slice ( headingAt + 1 )
2026-07-14 14:01:35 +08:00
const headingLines = new Set ( headings . map ( entry = > entry . index ) )
const end = body . findIndex ( line = > headingLines . has ( line . index ) )
2026-07-10 01:51:50 +08:00
const section = end === - 1 ? body : body.slice ( 0 , end )
if ( ! section . some ( line = > /^- / . test ( line . raw ) ) ) {
failures . push ( ` ${ readme } : ${ heading . index } : the \` ${ CANONICAL } \` section has no top-level \` - \` bullet — state the limitations, or whitelist the package if there are genuinely none ` )
}
}
if ( failures . length > 0 ) {
2026-07-14 00:22:52 +08:00
console . error ( 'verify-package-readme-limitations: violations found:' )
2026-07-10 01:51:50 +08:00
for ( const failure of failures ) console . error ( ` ${ failure } ` )
process . exit ( 1 )
}
2026-07-14 00:22:52 +08:00
console . log ( ` verify-package-readme-limitations: ${ scannedPackages . size } package READMEs checked ( ${ Object . keys ( NO_LIMITATIONS ) . length } whitelisted), all conform. ` )