Skip to content

Commit af94015

Browse files
claude[bot]claude
andauthored
fix(devx): wire check:parse-guard into CI, and widen it to all three TypeScript parser entry points (#10612)
* fix(devx): wire check:parse-guard into CI, and widen it to all three TypeScript parser entry points Fixes #10574 Fixes #10575 `scripts/check-parse-guard.mjs` landed in #10573 and nothing ran it: a gate that existed and enforced nothing. Wire it (`pnpm check:parse-guard` + a step in `lint.yml`'s `Lint & Repo Gates`), and close the two scope caveats its own header carried, so its green line stops being narrower than it reads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt * fix(devx): report the program/transpile doors in the ts-parse census line The census exit line counted every parse but not which of the three entry points each came through, which is exactly the observation needed to prove a converted call site really runs against a real tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt --------- Co-authored-by: claude <noreply@anthropic.com>
1 parent 5e50e2e commit af94015

7 files changed

Lines changed: 644 additions & 51 deletions

‎.github/workflows/lint.yml‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,32 @@ jobs:
172172
- name: scripts/ entry guards go through one predicate
173173
run: pnpm check:entry-guard
174174

175+
# Every `scripts/**` TypeScript parse goes through ONE module (#10133 /
176+
# #10573), and this is the half that keeps it that way (#10574).
177+
# NONE of the three parser entry points throws on a source it cannot
178+
# read: `ts.createSourceFile` returns a tree built by error recovery with
179+
# the errors parked on `parseDiagnostics`; `ts.createProgram` parks them
180+
# behind a second call, `getSyntacticDiagnostics()`; `ts.transpileModule`
181+
# reports nothing at all without `reportDiagnostics: true` and still
182+
# hands back an `outputText`. A gate then walks the wreckage, finds none
183+
# of the shapes it is looking for, and scores the file CLEAN — so the
184+
# SYMPTOM OF A MISSING REFUSAL IS A GREEN LINE, and an unguarded gate is
185+
# indistinguishable from a guarded one by reading CI. That is not a
186+
# theory: one gate here forced `ScriptKind.TSX` on 2504 test files, read
187+
# 32 of them as wreckage, and printed `OK` while six pinned engine
188+
# doubles went uncounted.
189+
# The #10573 sweep converted 32 call sites across 15 gates; it could not
190+
# stop the sixteenth being typed, and within the hour a new gate landed
191+
# with two raw calls in it — caught by this step, which is the whole
192+
# argument for having it. Same shape as `check:entry-guard` above.
193+
# Also prints the parses OUTSIDE `scripts/**` (#10575) that it does not
194+
# govern, so its green line is read as a claim about `scripts/` and not
195+
# about the repository.
196+
# Scans ~121 scripts/ files plus a read-only census of the rest, no
197+
# spawns; ~0.6s.
198+
- name: scripts/ TypeScript parses go through one module
199+
run: pnpm check:parse-guard
200+
175201
# Stack-collection enumerations vs the schema (#6242). `stack.zod.ts`
176202
# decides which collections a stack may declare; eight other enumerations
177203
# of that same set are hand-maintained (the map-format list, the

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"check:app-nav-i18n": "pnpm --filter @objectstack/cli run check:app-nav-i18n",
3636
"check:nul-bytes": "node scripts/check-nul-bytes.mjs --self-test && node scripts/check-nul-bytes.mjs",
3737
"check:entry-guard": "node scripts/check-entry-guard.mjs --self-test && node scripts/check-entry-guard.mjs",
38+
"check:parse-guard": "node scripts/check-parse-guard.mjs --self-test && node scripts/check-parse-guard.mjs",
3839
"check:stack-collection-maps": "node scripts/check-stack-collection-maps.mjs --self-test && node scripts/check-stack-collection-maps.mjs",
3940
"check:doc-authoring": "node scripts/check-doc-authoring.mjs --self-test && node scripts/check-doc-authoring.mjs",
4041
"check:doc-anchors": "node scripts/check-doc-anchors.mjs --self-test && node scripts/check-doc-anchors.mjs",

‎scripts/check-optional-error-sink-contract.mjs‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ import { dirname, join, relative, resolve, sep } from 'node:path';
164164
import { fileURLToPath } from 'node:url';
165165
import ts from 'typescript';
166166

167+
import { parseSourceFile } from './ts-parse.mjs';
168+
167169
const HERE = dirname(fileURLToPath(import.meta.url));
168170
const ROOT = resolve(HERE, '..');
169171
const BASELINE_PATH = join(HERE, 'optional-error-sink-contract.baseline.json');
@@ -489,7 +491,14 @@ function run({ list = false } = {}) {
489491
// this regex is exactly how the first draft of this population read a
490492
// clean tree while missing both audit sinks.
491493
if (!/\berror\s*\??\s*[:(]/.test(text)) continue;
492-
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
494+
// `parseSourceFile` rather than the raw call: ts.createSourceFile never
495+
// throws, so a `.ts` file with a syntax error would be walked as a
496+
// recovered partial tree, contribute nothing to the census, and be
497+
// scored as a file with no sinks to report. scriptKind is OMITTED —
498+
// `collectSourceFiles` yields `.ts` only, so the file name infers
499+
// exactly what the forced `ScriptKind.TS` used to say, and forcing
500+
// one is its own blind spot (see scripts/ts-parse.mjs).
501+
const sf = parseSourceFile(file, text);
493502
analyzeSourceFile(sf, relative(ROOT, file).split(sep).join('/'), census);
494503
}
495504
}
@@ -656,7 +665,7 @@ function selfTest() {
656665

657666
let failures = 0;
658667
for (const c of cases) {
659-
const sf = ts.createSourceFile('t.ts', c.code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
668+
const sf = parseSourceFile('t.ts', c.code);
660669
const census = emptyCensus();
661670
analyzeSourceFile(sf, 't.ts', census);
662671
const verdicts = census.sinks.map((s) => s.verdict);

0 commit comments

Comments
 (0)