Filed from the PR that closes #10574 / #10575. Nothing is red, and nothing here is a request to copy a helper.
#10575's Axis 1 (ts.createProgram, ts.transpileModule, both inside scripts/**) is done in that PR: both now route through ts-parse.mjs (createProgramChecked / transpileChecked) and check-parse-guard.mjs fails on all three raw spellings. Axis 2 — the parses outsidescripts/** — is deliberately left, because it needs a decision about shape first. This card carries the decision and a re-measurement.
What is actually out there — re-measured on main at d5e7b9f5a1
check:parse-guard now prints this population on every run, so the number moves when somebody adds one. 28 parses in 20 files — 13 in shipped/gate code, 15 in tests:
| file | sites | api | note |
|---|
packages/lint/src/validate-react-page-props.ts | 1 | createSourceFile | wrapped in try { … } catch { continue; } |
packages/lint/src/lint-startup-registry-verdict.ts | 1 | createSourceFile | wrapped in try { … } catch { return []; } |
packages/lint/src/validate-hook-body-writes.ts | 1 | createSourceFile | the only site anywhere passing setParentNodes: false, and it parses a synthesised async function __body(ctx) { … } wrapper |
packages/lint/scripts/check-doc-formula-expressions.mjs | 3 | createSourceFile | a gate |
packages/spec/scripts/check-skill-examples.ts | 1 | createSourceFile | a gate |
packages/spec/scripts/lib/strictness-ledger.ts | 1 | createSourceFile | generator/gate library |
packages/cli/src/utils/detect-free-identifiers.ts | 1 | createSourceFile | runtime, not a gate |
packages/spec/scripts/build-api-surface.ts | 1 | createProgram | generator |
packages/spec/scripts/build-export-origins.ts | 1 | createProgram | generator |
packages/spec/scripts/check-dual-source-exports.ts | 1 | createProgram | a gate |
packages/spec/scripts/check-exported-any.ts | 1 | createProgram | a gate |
| 9 test files | 15 | mixed | listed by the gate, marked [test] |
Two corrections to #10575's table, both measured:
- it said
check-skill-examples.ts has 2 sites; it has 1 (:308). The second was :692, a comment naming ts.createProgram — masked by the gate, which is the whole reason the gate masks prose. - it missed the four
ts.createProgram sites in packages/spec/scripts/** entirely. Two of them (check-dual-source-exports.ts, check-exported-any.ts) are gates, and a Program parks syntax errors behind getSyntacticDiagnostics() — a call none of the four makes. Same class as the scripts/** half, one tree over.
Why this is not "copy ts-parse.mjs into packages/"
The reason invoked-as.mjs gives for its own packages/cli sibling: scripts/ runs as plain .mjs against a possibly unbuilt tree, and making a published package depend on repo tooling to answer "did this parse?" trades one bug for a worse one.
More importantly the contract may genuinely differ per row. A scripts/** gate audits a tree its author controls, so ending the process is right. A publish-time lint validator in packages/lint is handed metadata by somebody else and may legitimately want to report an unparseable source as a finding rather than abort. Those are different answers and the rows above do not all want the same one.
The trap any Axis-2 shape has to survive
validate-react-page-props.ts and lint-startup-registry-verdict.ts wrap their parse in try { … } catch { continue; } / catch { return []; }. Both are dead code today — createSourceFile cannot throw — but they are written as a skip, so the moment a parse did throw they become a silent skip, which is this defect class pre-installed one layer up. That is exactly why ts-parse.mjs exits 3 instead of throwing: an exit cannot be downgraded by a caller that meant well.
So a package-side answer that throws is not equivalent to the scripts/** one. It has to either return a finding the caller must handle, or the two catch blocks have to go first. validate-react-page-props.ts's own comment says "the syntax gate reports unparseable sources" — a claim worth verifying rather than inheriting.
The options, so the decision is a decision
- A
packages/lint-local checked parse that RETURNS a finding. Publish-time validators report rather than abort; the two catch blocks are deleted as part of it. Costs: a second refusal shape in the repo; buys: a contract that fits how a validator is actually called. - A shared package (
@objectstack/…) both trees import. One shape everywhere; costs a published dependency for repo tooling, which is what invoked-as.mjs argued against. - Per-row, no shared shape. The four
packages/spec/scripts/** Programs are gates over a controlled tree and could simply call getSyntacticDiagnostics() and fail; the packages/lint/src/** validators get option 1; detect-free-identifiers.ts is runtime and needs its own answer. - Ratchet the census
check:parse-guard already prints, so a new out-of-tree parse cannot land unnoticed while the shape question is open. Cheap, orthogonal to 1–3, and deliberately NOT done in the closing PR: a ratchet forces an answer by making the next unrelated PR red.
Recommendation: 3 + 4. The rows do not share a contract, so a single shape would be invented rather than found; the four packages/spec/scripts/** gates are the cheapest and least arguable half and can move first. 4 keeps the population from growing while 1–3 are decided.
Refs: #10133 (the class) · #10573 (the scripts/** implementation) · #10574 / #10575 (wiring and widening; where Axis 1 landed)
Generated by Claude Code
Filed from the PR that closes #10574 / #10575. Nothing is red, and nothing here is a request to copy a helper.
#10575's Axis 1 (
ts.createProgram,ts.transpileModule, both insidescripts/**) is done in that PR: both now route throughts-parse.mjs(createProgramChecked/transpileChecked) andcheck-parse-guard.mjsfails on all three raw spellings. Axis 2 — the parses outsidescripts/**— is deliberately left, because it needs a decision about shape first. This card carries the decision and a re-measurement.What is actually out there — re-measured on
mainatd5e7b9f5a1check:parse-guardnow prints this population on every run, so the number moves when somebody adds one. 28 parses in 20 files — 13 in shipped/gate code, 15 in tests:packages/lint/src/validate-react-page-props.tscreateSourceFiletry { … } catch { continue; }packages/lint/src/lint-startup-registry-verdict.tscreateSourceFiletry { … } catch { return []; }packages/lint/src/validate-hook-body-writes.tscreateSourceFilesetParentNodes: false, and it parses a synthesisedasync function __body(ctx) { … }wrapperpackages/lint/scripts/check-doc-formula-expressions.mjscreateSourceFilepackages/spec/scripts/check-skill-examples.tscreateSourceFilepackages/spec/scripts/lib/strictness-ledger.tscreateSourceFilepackages/cli/src/utils/detect-free-identifiers.tscreateSourceFilepackages/spec/scripts/build-api-surface.tscreateProgrampackages/spec/scripts/build-export-origins.tscreateProgrampackages/spec/scripts/check-dual-source-exports.tscreateProgrampackages/spec/scripts/check-exported-any.tscreateProgram[test]Two corrections to #10575's table, both measured:
check-skill-examples.tshas 2 sites; it has 1 (:308). The second was:692, a comment namingts.createProgram— masked by the gate, which is the whole reason the gate masks prose.ts.createProgramsites inpackages/spec/scripts/**entirely. Two of them (check-dual-source-exports.ts,check-exported-any.ts) are gates, and a Program parks syntax errors behindgetSyntacticDiagnostics()— a call none of the four makes. Same class as thescripts/**half, one tree over.Why this is not "copy
ts-parse.mjsintopackages/"The reason
invoked-as.mjsgives for its ownpackages/clisibling:scripts/runs as plain.mjsagainst a possibly unbuilt tree, and making a published package depend on repo tooling to answer "did this parse?" trades one bug for a worse one.More importantly the contract may genuinely differ per row. A
scripts/**gate audits a tree its author controls, so ending the process is right. A publish-time lint validator inpackages/lintis handed metadata by somebody else and may legitimately want to report an unparseable source as a finding rather than abort. Those are different answers and the rows above do not all want the same one.The trap any Axis-2 shape has to survive
validate-react-page-props.tsandlint-startup-registry-verdict.tswrap their parse intry { … } catch { continue; }/catch { return []; }. Both are dead code today —createSourceFilecannot throw — but they are written as a skip, so the moment a parse did throw they become a silent skip, which is this defect class pre-installed one layer up. That is exactly whyts-parse.mjsexits3instead of throwing: an exit cannot be downgraded by a caller that meant well.So a package-side answer that throws is not equivalent to the
scripts/**one. It has to either return a finding the caller must handle, or the twocatchblocks have to go first.validate-react-page-props.ts's own comment says "the syntax gate reports unparseable sources" — a claim worth verifying rather than inheriting.The options, so the decision is a decision
packages/lint-local checked parse that RETURNS a finding. Publish-time validators report rather than abort; the twocatchblocks are deleted as part of it. Costs: a second refusal shape in the repo; buys: a contract that fits how a validator is actually called.@objectstack/…) both trees import. One shape everywhere; costs a published dependency for repo tooling, which is whatinvoked-as.mjsargued against.packages/spec/scripts/**Programs are gates over a controlled tree and could simply callgetSyntacticDiagnostics()and fail; thepackages/lint/src/**validators get option 1;detect-free-identifiers.tsis runtime and needs its own answer.check:parse-guardalready prints, so a new out-of-tree parse cannot land unnoticed while the shape question is open. Cheap, orthogonal to 1–3, and deliberately NOT done in the closing PR: a ratchet forces an answer by making the next unrelated PR red.Recommendation: 3 + 4. The rows do not share a contract, so a single shape would be invented rather than found; the four
packages/spec/scripts/**gates are the cheapest and least arguable half and can move first. 4 keeps the population from growing while 1–3 are decided.Refs: #10133 (the class) · #10573 (the
scripts/**implementation) · #10574 / #10575 (wiring and widening; where Axis 1 landed)Generated by Claude Code