fix(doc-gates): review findings — scan every merge block, any quote style, .tsx too; self-check the scan

ds-review-bot round: the backstop stopped at a file's first declare-module
block while the projection it guards reads them all; the textual prefilter
matched only single quotes; .tsx sources escaped the glob. All three are the
silent-vanish class the scan exists to prevent. contextMergeFiles now yields
one entry per block (quote-agnostic prefilter, ts+tsx patterns), and a third
partition direction requires everything rendered to be scan-visible, so a
future scan regression is a hard error. Spec fixture made projection-
consistent; the partially superseded 2026-07-28 regions note is rewritten to
the current mechanism and cross-linked both ways.
This commit is contained in:
Tianyi Cui
2026-08-09 10:29:13 +08:00
parent 0fb062d85e
commit c9205901ec
9 changed files with 123 additions and 34 deletions

View File

@@ -9,41 +9,53 @@ import { globSync, readFileSync } from 'node:fs'
import { resolve, sep } from 'node:path'
import ts from 'typescript'
/** Cheap textual prefilter for a cordis module merge, quote-style agnostic
* (the AST match below reads `stmt.name.text` and never sees the quotes). */
const MERGE_HEAD = /declare module ['"](?:cordis|\.\/context\.ts)['"]/
/**
* Parse every file matching `pattern` (repo-relative, sorted, `/`-normalized)
* that textually contains a cordis module merge, yielding each file's
* module-merge body. Files without a merge are skipped.
* @param scanRoot - Repository root the pattern is resolved against.
* @param pattern - Glob selecting the TypeScript files to scan.
* @returns One entry per file with a cordis module merge, in path order.
* Parse every file matching `patterns` (repo-relative, sorted, `/`-normalized)
* that textually contains a cordis module merge, yielding one entry per merge
* BLOCK — a file may legally hold several `declare module 'cordis'` blocks
* (the Typert analyzer reads them all), so the exhaustiveness scan must too.
* Files without a merge are skipped.
* @param scanRoot - Repository root the patterns are resolved against.
* @param patterns - Glob(s) selecting the TypeScript files to scan.
* @returns One entry per cordis module block, in path then source order.
*/
export function contextMergeFiles(
scanRoot: string,
pattern: string,
patterns: string | readonly string[],
): { rel: string; sf: ts.SourceFile; text: string; body: ts.ModuleBlock }[] {
const out: { rel: string; sf: ts.SourceFile; text: string; body: ts.ModuleBlock }[] = []
for (const rel of globSync(pattern, { cwd: scanRoot }).map(s => s.split(sep).join('/')).sort()) {
const rels = [...new Set(globSync(patterns as string | string[], { cwd: scanRoot }).map(s => s.split(sep).join('/')))].sort()
for (const rel of rels) {
const abs = resolve(scanRoot, rel)
const text = readFileSync(abs, 'utf8')
if (!text.includes("declare module 'cordis'") && !text.includes("declare module './context.ts'")) continue
if (!MERGE_HEAD.test(text)) continue
const sf = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true)
const body = cordisModuleBody(sf)
if (!body) continue
out.push({ rel, sf, text, body })
for (const body of cordisModuleBodies(sf)) out.push({ rel, sf, text, body })
}
return out
}
/** The body of the cordis module merge in `sf`: `declare module 'cordis'`
* (harness packages) or `declare module './context.ts'` (vendor core), or
* null when the file has neither. */
export function cordisModuleBody(sf: ts.SourceFile): ts.ModuleBlock | null {
/** Every cordis module-merge body in `sf`: `declare module 'cordis'` (harness
* packages) or `declare module './context.ts'` (vendor core), in source order. */
export function cordisModuleBodies(sf: ts.SourceFile): ts.ModuleBlock[] {
const bodies: ts.ModuleBlock[] = []
for (const stmt of sf.statements) {
if (!ts.isModuleDeclaration(stmt) || !ts.isStringLiteral(stmt.name)) continue
if (stmt.name.text !== 'cordis' && stmt.name.text !== './context.ts') continue
if (stmt.body && ts.isModuleBlock(stmt.body)) return stmt.body
if (stmt.body && ts.isModuleBlock(stmt.body)) bodies.push(stmt.body)
}
return null
return bodies
}
/** The FIRST cordis module-merge body in `sf`, or null without one — for the
* vendor core-API renderer whose input files carry exactly one merge; the
* exhaustiveness scan uses {@link cordisModuleBodies} to read them all. */
export function cordisModuleBody(sf: ts.SourceFile): ts.ModuleBlock | null {
return cordisModuleBodies(sf)[0] ?? null
}
/**