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:
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user