Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 201 additions & 13 deletions scripts/check-type-check-coverage.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,40 @@
// # -- see BUILT CLOSURE.
// node scripts/check-type-check-coverage.mjs --self-test
//
// ## Exit codes -- and why a REFUSAL has one of its own
//
// 0 the tree was read and every invariant above holds.
// 1 a FINDING: a structural problem, or -- under `--re-measure` -- a ledger
// entry that drifted UPWARD. A claim about the tree.
// 3 PREREQUISITE NOT MET. The gate refused to measure, so nothing was
// measured and the run says NOTHING about the ledger. NOT a pass, and
// NOT a finding.
//
// The split is the sibling convention, not a local invention:
// `check-test-completeness.mjs` states it in its own failure text ("Exit code
// 3, distinct from a finding's 1") and `check-dual-build-cjs-loads.mjs`
// answers the IDENTICAL condition -- a gate that reads built output, run
// against a tree with no `dist/` -- with 3. This gate used to answer it by
// letting the refusal reach node's uncaught handler, which exits 1.
//
// ⭐ Why the class matters HERE in particular, more than it does for a gate
// whose 1 means "something is wrong somewhere". Exit 1 from this gate has one
// specific meaning: a package's recorded debt went UP. The remedy that meaning
// prescribes ends at the ledger below, and raising a DEBT/TEST_DEBT entry is a
// MAINTAINER-only act this file spells out at length. So a reader who takes an
// unmeasurable run for that red is pointed straight at the one place this
// evidence must never send them. The prose said so all along -- but the prose
// is not what an exit-code reader reads. A CI step, a wrapper, or an agent
// reconciling a derived gate family sees the number and nothing else.
//
// ⛔ The boundary, deliberately. Exit 3 is for a prerequisite the WORLD failed
// to supply and that the caller clears with a named command: an unbuilt or
// stale dependency closure, a closure that does not build, an absent `turbo`
// or `tsc` binary, a tsc that could not be spawned or could not read the
// project it was handed. A malformed `tsconfig.json` checked INTO the tree
// stays exit 1 -- that is a fact about the tree, which is what a finding is,
// and `readTsconfig` keeps throwing it.
//
// Invariants, per workspace package (the root workspace package included --
// #4311's audit counted its top-level TypeScript like any other package's):
//
Expand DownExpand Up@@ -468,6 +502,74 @@ import {
const ROOT = resolve(import.meta.dirname, '..');
const SELF = 'scripts/check-type-check-coverage.mjs';
const TRACKING_ISSUE = 'https://github.com/objectstack-ai/objectstack/issues/4311';

// The exit-code contract, NAMED rather than spelled inline at each site, so the
// self-test pins the value each path actually returns instead of a comment
// about it -- the shape `check-test-completeness.mjs` uses for the same split.
//
// ⛔ Module-local, NOT exported, and that is a decision rather than an
// oversight: this file's top level RUNS (it is a gate, invoked as a script and
// nothing else), so exporting any binding at all would make it importable for
// that binding and run the whole gate inside the importer -- the class
// `check:entry-guard` refuses, and it caught this constant block on its first
// run. The sibling that does export its codes carries `isEntrypoint` for
// exactly this reason. Nothing imports this file today; the day something
// needs to, the guard comes with the export.
const EXIT_OK = 0;
const EXIT_FINDINGS = 1;
const EXIT_PREREQUISITE_NOT_MET = 3;

/**
* The text a refusal prints, as a VALUE -- so the self-test can assert on the
* advisory without spawning a process or stubbing `process.exit`, the split
* `import-prerequisite.mjs` documents for the same reason.
*
* The gate's own refusal message is embedded VERBATIM. Every one of them was
* already argued at length at its throw site, and none of that reasoning is
* this frame's to restate, shorten or improve -- the frame adds only the two
* things a reader could not get from the message: what CLASS of result this is,
* and which exit code carries it.
*
* The pipe advisory is not decoration. `EXIT=$?` written after `cmd | tail -40`
* reads TAIL's status, and `head`/`tail` essentially never fail -- so a refusal
* and a green run are the same `0` there, which is the one reading this whole
* exit-code split exists to make impossible.
*
* @param {string} message the refusal, in the words of the site that raised it
* @returns {string}
*/
function prerequisiteNotMetText(message) {
return (
`\ncheck-type-check-coverage: PREREQUISITE NOT MET\n\n` +
`${message}\n\n` +
` ⛔ This is NOT a pass and NOT a finding: nothing was measured, so this run says\n` +
` NOTHING about whether any DEBT or TEST_DEBT number is still correct. In particular\n` +
` it is NOT evidence that a recorded number went up, and ⛔ no ledger entry below may\n` +
` be raised on it -- raising one is a maintainer's act even when the evidence is real.\n` +
` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n` +
` \`node ${SELF} --re-measure > /tmp/type-check-debt.log 2>&1; echo "EXIT=$?"\`.\n` +
` Piped, \`$?\` is the LAST command's status, and \`head\`/\`tail\` essentially never fail — that\n` +
` is the false green. \`\${PIPESTATUS[0]}\`/\`pipefail\` do recover this gate's own code.)`
);
}

/**
* Refuse to measure, and say so in the exit code as well as in the prose.
*
* ⛔ Prints and EXITS rather than throwing. A thrown refusal reaches node's
* uncaught handler, which exits 1 -- the code this gate reserves for "a
* recorded debt number went UP" -- and prints a stack trace over a message
* whose whole job is to be read. The self-test pins that the three functions
* that refuse contain no bare `throw new Error(` for exactly this reason: the
* rot this repairs is one careless `throw` away from returning.
*
* @param {string} message the refusal, in the words of the site that raised it
* @returns {never}
*/
function refusePrerequisite(message) {
console.error(prerequisiteNotMetText(message));
process.exit(EXIT_PREREQUISITE_NOT_MET);
}
// An `exclude` pattern that names tests (`**/*.test.ts`, `**/*.spec.tsx`, ...)
// and the files such a pattern hides. Kept deliberately broad: the question is
// "does this config steer tsc away from the test layer", not "which exact glob".
Expand DownExpand Up@@ -1532,12 +1634,14 @@ function gitIgnoredPaths(rels) {
maxBuffer: 16 * 1024 * 1024,
});
if (res.error) {
throw new Error(`git check-ignore could not run, so GENERATED_COVERED cannot be judged`, { cause: res.error });
refusePrerequisite(
`git check-ignore could not run, so GENERATED_COVERED cannot be judged: ${res.error.message}`,
);
}
// 0 = some path is ignored, 1 = none is. Anything else (128: not a git
// checkout, bad option) is a failed MEASUREMENT, not a clean tree.
if (res.status !== 0 && res.status !== 1) {
throw new Error(
refusePrerequisite(
`git check-ignore exited ${res.status}, so GENERATED_COVERED cannot be judged: ${String(res.stderr).trim()}`,
);
}
Expand DownExpand Up@@ -2701,14 +2805,14 @@ function workspaceBuildGraph(packages) {
function refreshBuiltClosure() {
const bin = join(ROOT, 'node_modules', '.bin', 'turbo');
if (!existsSync(bin)) {
throw new Error(`--re-measure needs the workspace's own turbo at ${bin}; run \`pnpm install\` first.`);
refusePrerequisite(`--re-measure needs the workspace's own turbo at ${bin}; run \`pnpm install\` first.`);
}
const args = ['run', 'build', '--filter=./packages/*', '--filter=./packages/*/*'];
const run = spawnSync(bin, args, { cwd: ROOT, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
if (run.error) throw new Error(`the closure build could not be run: ${run.error.message}`);
if (run.error) refusePrerequisite(`the closure build could not be run: ${run.error.message}`);
if (run.status !== 0) {
const output = `${run.stdout ?? ''}${run.stderr ?? ''}`.trim();
throw new Error(
refusePrerequisite(
`--re-measure cannot run: the ledgered packages' dependency closure does not build, so there is no `
+ `world to measure against. Fix the build first -- every number in DEBT and TEST_DEBT is measured `
+ `with tsc resolving workspace imports through each dependency's built \`dist/*.d.ts\`.\n`
Expand DownExpand Up@@ -2929,7 +3033,7 @@ const REMEASURE_HEAP = remeasureHeapCeiling({
function tscErrorCount(project, options = {}) {
const bin = join(ROOT, 'node_modules', '.bin', 'tsc');
if (!existsSync(bin)) {
throw new Error(`--re-measure needs the workspace's own tsc at ${bin}; run \`pnpm install\` first.`);
refusePrerequisite(`--re-measure needs the workspace's own tsc at ${bin}; run \`pnpm install\` first.`);
}
const run = spawnSync(bin, ['--noEmit', '--pretty', 'false', '-p', project], {
cwd: ROOT,
Expand All@@ -2941,10 +3045,10 @@ function tscErrorCount(project, options = {}) {
// dressed as a different one.
env: heapCappedEnv(process.env, REMEASURE_HEAP.mb),
});
if (run.error) throw new Error(`tsc could not be run for ${project}: ${run.error.message}`);
if (run.error) refusePrerequisite(`tsc could not be run for ${project}: ${run.error.message}`);
const output = `${run.stdout ?? ''}${run.stderr ?? ''}`;
if (TSC_SETUP_ERROR.test(output)) {
throw new Error(`tsc could not read ${project} -- the measurement is invalid, not zero:\n${output.trim()}`);
refusePrerequisite(`tsc could not read ${project} -- the measurement is invalid, not zero:\n${output.trim()}`);
}
const errors = countTscErrors(output, options);
// Exit 0 means a clean program; anything else must have produced diagnostics
Expand All@@ -2956,7 +3060,7 @@ function tscErrorCount(project, options = {}) {
// the `return 0` below -- refusing there would turn "this test layer is clean
// apart from an artefact of our own generated config" into a hard crash.
if (run.status !== 0 && countTscErrors(output) === 0) {
throw new Error(
refusePrerequisite(
`tsc exited ${run.status} for ${project} but printed no recognisable diagnostics -- ` +
`refusing to record 0:\n${output.trim().slice(0, 2000)}`,
);
Expand DownExpand Up@@ -3199,10 +3303,20 @@ function measureTestDebt(dir, hiddenTests = []) {
const holder = mkdtempSync(join(tmpdir(), 'objectstack-debt-remeasure-'));
const configPath = join(holder, REMEASURE_CONFIG);
writeFileSync(configPath, `${JSON.stringify(project, null, 2)}\n`);
// Registered on `exit` as WELL as in the `finally`, because `tscErrorCount`
// can now REFUSE, and a refusal calls `process.exit` -- which runs `exit`
// handlers and does NOT run `finally`. Belt and braces on purpose: the
// `finally` keeps the directory's lifetime visible where it is created, and
// the handler is what makes the refusal path leave nothing behind. `off`
// first in the `finally` so a run measuring 34 entries does not accumulate 34
// live handlers on a directory each has already removed.
const cleanup = () => rmSync(holder, { force: true, recursive: true });
process.once('exit', cleanup);
try {
return tscErrorCount(configPath, { dropRootDirDiagnostics: true });
} finally {
rmSync(holder, { force: true, recursive: true });
process.off('exit', cleanup);
cleanup();
}
}

Expand All@@ -3228,7 +3342,7 @@ function measureLedgers(packages, rootName, state) {
.filter((name) => dirOf.has(name));
const unbuilt = unbuiltClosure(ledgered, workspaceBuildGraph(packages));
if (unbuilt.length > 0) {
throw new Error(
refusePrerequisite(
`--re-measure cannot run: ${unbuilt.length} workspace dependenc(ies) of the ledgered packages have `
+ `no built type entry point on disk -- ${unbuilt.join(', ')}.\n`
+ `Every number in DEBT and TEST_DEBT is measured with tsc resolving workspace imports through each `
Expand DownExpand Up@@ -3262,7 +3376,7 @@ function measureLedgers(packages, rootName, state) {
// caller can act on.
const stale = staleClosure(ledgered, workspaceBuildGraph(packages));
if (stale.length > 0) {
throw new Error(
refusePrerequisite(
`--re-measure cannot run: ${stale.length} workspace dependenc(ies) of the ledgered packages still have `
+ `a type entry point OLDER than their own sources after a full closure build -- ${stale.join(', ')}.\n`
+ `The build covers \`./packages/*\` and \`./packages/*/*\`, so a package that survives it is one those `
Expand DownExpand Up@@ -5378,6 +5492,79 @@ function selfTest() {
}
}

// ── THE EXIT-CODE CLASS ───────────────────────────────────────────────────
//
// Pinned because it is exactly the kind of fact that rots back silently. The
// defect this replaces was not a wrong number typed anywhere: it was a
// refusal that reached node's UNCAUGHT handler, which exits 1 -- so the
// regression shape is one careless `throw new Error(...)` added to a refusing
// function by an author who never thought about exit codes at all, and it
// announces itself with a green CI (every consumer of this gate treats any
// non-zero as failure, so 1-instead-of-3 is invisible to all of them) and a
// human-readable message that still says the right thing. Nothing else in
// this file would notice.
//
// So the pin is over the FUNCTION BODIES, not over a constant. Reading the
// real `Function.prototype.toString()` of the four functions that refuse is
// what makes a re-added `throw` fail here rather than in six weeks, on a card
// about something else.
const REFUSING = [refreshBuiltClosure, tscErrorCount, measureLedgers, gitIgnoredPaths];
for (const fn of REFUSING) {
const body = fn.toString();
if (/throw new Error\(/.test(body)) {
failures.push(
`${fn.name} raises a bare \`throw new Error(\` — an uncaught throw exits 1, the code this gate ` +
`reserves for a ledger entry that drifted UPWARD. A refusal must go through ` +
`refusePrerequisite() so it exits ${EXIT_PREREQUISITE_NOT_MET}.`,
);
}
if (!body.includes('refusePrerequisite(')) {
failures.push(`${fn.name} no longer refuses through refusePrerequisite() — the exit-code class is unpinned`);
}
}
// The NEGATIVE control, and the reason the loop above is a measurement rather
// than a tautology over an empty set: `readTsconfig` still throws, on purpose.
// A `tsconfig.json` checked into the tree that does not parse is a fact about
// the TREE -- a finding -- not a prerequisite the caller forgot to supply, so
// it keeps exit 1 and the pin above must be able to SEE a bare throw.
if (!/throw new Error\(/.test(readTsconfig.toString())) {
failures.push(
'readTsconfig no longer throws — the bare-throw pin above can no longer fail, so it stopped measuring',
);
}

const exitCodeCases = [
{ label: 'the refusal code is 3', ok: EXIT_PREREQUISITE_NOT_MET === 3 },
{ label: 'a finding is 1', ok: EXIT_FINDINGS === 1 },
{ label: 'the refusal code is distinct from a finding and from a pass',
ok: EXIT_PREREQUISITE_NOT_MET !== EXIT_FINDINGS && EXIT_PREREQUISITE_NOT_MET !== EXIT_OK },
];
for (const c of exitCodeCases) {
if (!c.ok) failures.push(`exit-code contract — ${c.label}`);
}

// The refusal TEXT. Four load-bearing clauses, each one a thing a reader who
// sees only the exit code cannot get anywhere else.
const refusalFixture = '--re-measure cannot run: 48 workspace dependenc(ies) have no built type entry point';
const refusalText = prerequisiteNotMetText(refusalFixture);
const textCases = [
{ label: 'carries the raising site\'s own message VERBATIM', ok: refusalText.includes(refusalFixture) },
{ label: 'names the class', ok: refusalText.includes('PREREQUISITE NOT MET') },
{ label: 'says it is neither a pass nor a finding', ok: /NOT a pass and NOT a finding/.test(refusalText) },
{ label: 'names its own code and the finding code it is distinct from',
ok: refusalText.includes(`Exit code ${EXIT_PREREQUISITE_NOT_MET}`)
&& refusalText.includes(`a finding's ${EXIT_FINDINGS}`) },
// The specific misreading this whole change exists to stop: taking an
// unmeasurable run for "a recorded debt went up" routes the reader to the
// ledger, and raising an entry there is a maintainer's act.
{ label: 'turns the reader away from the ledger rather than toward it',
ok: /no ledger entry below may\n?\s*be raised on it/.test(refusalText) },
{ label: 'warns that the code must be captured before any pipe', ok: refusalText.includes('BEFORE any pipe') },
];
for (const c of textCases) {
if (!c.ok) failures.push(`prerequisiteNotMetText — ${c.label}`);
}

// The shared workspace enumerator is a plain module with no CI invocation of
// its own (#11510); every gate that consolidated onto it folds in its checks.
failures.push(...workspaceEnumeratorSelfTest({ root: ROOT }));
Expand All@@ -5396,7 +5583,8 @@ function selfTest() {
+ ceilingCases.length + heapEnvCases.length} re-measure case(s) + ` +
`${typeEntryCases.length + closureCases.length + staleCases.length + sourceFileCases.length} ` +
`built-closure case(s) + ` +
`${planCases.length + rewriteCases.length + roundTripCases.length} auto-lowering case(s) hold.`,
`${planCases.length + rewriteCases.length + roundTripCases.length} auto-lowering case(s) + ` +
`${REFUSING.length * 2 + 1 + exitCodeCases.length + textCases.length} exit-code case(s) hold.`,
);
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 201 additions & 13 deletions scripts/check-type-check-coverage.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,40 @@
// # -- see BUILT CLOSURE.
// node scripts/check-type-check-coverage.mjs --self-test
//
// ## Exit codes -- and why a REFUSAL has one of its own
//
// 0 the tree was read and every invariant above holds.
// 1 a FINDING: a structural problem, or -- under `--re-measure` -- a ledger
// entry that drifted UPWARD. A claim about the tree.
// 3 PREREQUISITE NOT MET. The gate refused to measure, so nothing was
// measured and the run says NOTHING about the ledger. NOT a pass, and
// NOT a finding.
//
// The split is the sibling convention, not a local invention:
// `check-test-completeness.mjs` states it in its own failure text ("Exit code
// 3, distinct from a finding's 1") and `check-dual-build-cjs-loads.mjs`
// answers the IDENTICAL condition -- a gate that reads built output, run
// against a tree with no `dist/` -- with 3. This gate used to answer it by
// letting the refusal reach node's uncaught handler, which exits 1.
//
// ⭐ Why the class matters HERE in particular, more than it does for a gate
// whose 1 means "something is wrong somewhere". Exit 1 from this gate has one
// specific meaning: a package's recorded debt went UP. The remedy that meaning
// prescribes ends at the ledger below, and raising a DEBT/TEST_DEBT entry is a
// MAINTAINER-only act this file spells out at length. So a reader who takes an
// unmeasurable run for that red is pointed straight at the one place this
// evidence must never send them. The prose said so all along -- but the prose
// is not what an exit-code reader reads. A CI step, a wrapper, or an agent
// reconciling a derived gate family sees the number and nothing else.
//
// ⛔ The boundary, deliberately. Exit 3 is for a prerequisite the WORLD failed
// to supply and that the caller clears with a named command: an unbuilt or
// stale dependency closure, a closure that does not build, an absent `turbo`
// or `tsc` binary, a tsc that could not be spawned or could not read the
// project it was handed. A malformed `tsconfig.json` checked INTO the tree
// stays exit 1 -- that is a fact about the tree, which is what a finding is,
// and `readTsconfig` keeps throwing it.
//
// Invariants, per workspace package (the root workspace package included --
// #4311's audit counted its top-level TypeScript like any other package's):
//
Expand DownExpand Up@@ -468,6 +502,74 @@ import {
const ROOT = resolve(import.meta.dirname, '..');
const SELF = 'scripts/check-type-check-coverage.mjs';
const TRACKING_ISSUE = 'https://github.com/objectstack-ai/objectstack/issues/4311';

// The exit-code contract, NAMED rather than spelled inline at each site, so the
// self-test pins the value each path actually returns instead of a comment
// about it -- the shape `check-test-completeness.mjs` uses for the same split.
//
// ⛔ Module-local, NOT exported, and that is a decision rather than an
// oversight: this file's top level RUNS (it is a gate, invoked as a script and
// nothing else), so exporting any binding at all would make it importable for
// that binding and run the whole gate inside the importer -- the class
// `check:entry-guard` refuses, and it caught this constant block on its first
// run. The sibling that does export its codes carries `isEntrypoint` for
// exactly this reason. Nothing imports this file today; the day something
// needs to, the guard comes with the export.
const EXIT_OK = 0;
const EXIT_FINDINGS = 1;
const EXIT_PREREQUISITE_NOT_MET = 3;

/**
* The text a refusal prints, as a VALUE -- so the self-test can assert on the
* advisory without spawning a process or stubbing `process.exit`, the split
* `import-prerequisite.mjs` documents for the same reason.
*
* The gate's own refusal message is embedded VERBATIM. Every one of them was
* already argued at length at its throw site, and none of that reasoning is
* this frame's to restate, shorten or improve -- the frame adds only the two
* things a reader could not get from the message: what CLASS of result this is,
* and which exit code carries it.
*
* The pipe advisory is not decoration. `EXIT=$?` written after `cmd | tail -40`
* reads TAIL's status, and `head`/`tail` essentially never fail -- so a refusal
* and a green run are the same `0` there, which is the one reading this whole
* exit-code split exists to make impossible.
*
* @param {string} message the refusal, in the words of the site that raised it
* @returns {string}
*/
function prerequisiteNotMetText(message) {
return (
`\ncheck-type-check-coverage: PREREQUISITE NOT MET\n\n` +
`${message}\n\n` +
` ⛔ This is NOT a pass and NOT a finding: nothing was measured, so this run says\n` +
` NOTHING about whether any DEBT or TEST_DEBT number is still correct. In particular\n` +
` it is NOT evidence that a recorded number went up, and ⛔ no ledger entry below may\n` +
` be raised on it -- raising one is a maintainer's act even when the evidence is real.\n` +
` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n` +
` \`node ${SELF} --re-measure > /tmp/type-check-debt.log 2>&1; echo "EXIT=$?"\`.\n` +
` Piped, \`$?\` is the LAST command's status, and \`head\`/\`tail\` essentially never fail — that\n` +
` is the false green. \`\${PIPESTATUS[0]}\`/\`pipefail\` do recover this gate's own code.)`
);
}

/**
* Refuse to measure, and say so in the exit code as well as in the prose.
*
* ⛔ Prints and EXITS rather than throwing. A thrown refusal reaches node's
* uncaught handler, which exits 1 -- the code this gate reserves for "a
* recorded debt number went UP" -- and prints a stack trace over a message
* whose whole job is to be read. The self-test pins that the three functions
* that refuse contain no bare `throw new Error(` for exactly this reason: the
* rot this repairs is one careless `throw` away from returning.
*
* @param {string} message the refusal, in the words of the site that raised it
* @returns {never}
*/
function refusePrerequisite(message) {
console.error(prerequisiteNotMetText(message));
process.exit(EXIT_PREREQUISITE_NOT_MET);
}
// An `exclude` pattern that names tests (`**/*.test.ts`, `**/*.spec.tsx`, ...)
// and the files such a pattern hides. Kept deliberately broad: the question is
// "does this config steer tsc away from the test layer", not "which exact glob".
Expand DownExpand Up@@ -1532,12 +1634,14 @@ function gitIgnoredPaths(rels) {
maxBuffer: 16 * 1024 * 1024,
});
if (res.error) {
throw new Error(`git check-ignore could not run, so GENERATED_COVERED cannot be judged`, { cause: res.error });
refusePrerequisite(
`git check-ignore could not run, so GENERATED_COVERED cannot be judged: ${res.error.message}`,
);
}
// 0 = some path is ignored, 1 = none is. Anything else (128: not a git
// checkout, bad option) is a failed MEASUREMENT, not a clean tree.
if (res.status !== 0 && res.status !== 1) {
throw new Error(
refusePrerequisite(
`git check-ignore exited ${res.status}, so GENERATED_COVERED cannot be judged: ${String(res.stderr).trim()}`,
);
}
Expand DownExpand Up@@ -2701,14 +2805,14 @@ function workspaceBuildGraph(packages) {
function refreshBuiltClosure() {
const bin = join(ROOT, 'node_modules', '.bin', 'turbo');
if (!existsSync(bin)) {
throw new Error(`--re-measure needs the workspace's own turbo at ${bin}; run \`pnpm install\` first.`);
refusePrerequisite(`--re-measure needs the workspace's own turbo at ${bin}; run \`pnpm install\` first.`);
}
const args = ['run', 'build', '--filter=./packages/*', '--filter=./packages/*/*'];
const run = spawnSync(bin, args, { cwd: ROOT, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
if (run.error) throw new Error(`the closure build could not be run: ${run.error.message}`);
if (run.error) refusePrerequisite(`the closure build could not be run: ${run.error.message}`);
if (run.status !== 0) {
const output = `${run.stdout ?? ''}${run.stderr ?? ''}`.trim();
throw new Error(
refusePrerequisite(
`--re-measure cannot run: the ledgered packages' dependency closure does not build, so there is no `
+ `world to measure against. Fix the build first -- every number in DEBT and TEST_DEBT is measured `
+ `with tsc resolving workspace imports through each dependency's built \`dist/*.d.ts\`.\n`
Expand DownExpand Up@@ -2929,7 +3033,7 @@ const REMEASURE_HEAP = remeasureHeapCeiling({
function tscErrorCount(project, options = {}) {
const bin = join(ROOT, 'node_modules', '.bin', 'tsc');
if (!existsSync(bin)) {
throw new Error(`--re-measure needs the workspace's own tsc at ${bin}; run \`pnpm install\` first.`);
refusePrerequisite(`--re-measure needs the workspace's own tsc at ${bin}; run \`pnpm install\` first.`);
}
const run = spawnSync(bin, ['--noEmit', '--pretty', 'false', '-p', project], {
cwd: ROOT,
Expand All@@ -2941,10 +3045,10 @@ function tscErrorCount(project, options = {}) {
// dressed as a different one.
env: heapCappedEnv(process.env, REMEASURE_HEAP.mb),
});
if (run.error) throw new Error(`tsc could not be run for ${project}: ${run.error.message}`);
if (run.error) refusePrerequisite(`tsc could not be run for ${project}: ${run.error.message}`);
const output = `${run.stdout ?? ''}${run.stderr ?? ''}`;
if (TSC_SETUP_ERROR.test(output)) {
throw new Error(`tsc could not read ${project} -- the measurement is invalid, not zero:\n${output.trim()}`);
refusePrerequisite(`tsc could not read ${project} -- the measurement is invalid, not zero:\n${output.trim()}`);
}
const errors = countTscErrors(output, options);
// Exit 0 means a clean program; anything else must have produced diagnostics
Expand All@@ -2956,7 +3060,7 @@ function tscErrorCount(project, options = {}) {
// the `return 0` below -- refusing there would turn "this test layer is clean
// apart from an artefact of our own generated config" into a hard crash.
if (run.status !== 0 && countTscErrors(output) === 0) {
throw new Error(
refusePrerequisite(
`tsc exited ${run.status} for ${project} but printed no recognisable diagnostics -- ` +
`refusing to record 0:\n${output.trim().slice(0, 2000)}`,
);
Expand DownExpand Up@@ -3199,10 +3303,20 @@ function measureTestDebt(dir, hiddenTests = []) {
const holder = mkdtempSync(join(tmpdir(), 'objectstack-debt-remeasure-'));
const configPath = join(holder, REMEASURE_CONFIG);
writeFileSync(configPath, `${JSON.stringify(project, null, 2)}\n`);
// Registered on `exit` as WELL as in the `finally`, because `tscErrorCount`
// can now REFUSE, and a refusal calls `process.exit` -- which runs `exit`
// handlers and does NOT run `finally`. Belt and braces on purpose: the
// `finally` keeps the directory's lifetime visible where it is created, and
// the handler is what makes the refusal path leave nothing behind. `off`
// first in the `finally` so a run measuring 34 entries does not accumulate 34
// live handlers on a directory each has already removed.
const cleanup = () => rmSync(holder, { force: true, recursive: true });
process.once('exit', cleanup);
try {
return tscErrorCount(configPath, { dropRootDirDiagnostics: true });
} finally {
rmSync(holder, { force: true, recursive: true });
process.off('exit', cleanup);
cleanup();
}
}

Expand All@@ -3228,7 +3342,7 @@ function measureLedgers(packages, rootName, state) {
.filter((name) => dirOf.has(name));
const unbuilt = unbuiltClosure(ledgered, workspaceBuildGraph(packages));
if (unbuilt.length > 0) {
throw new Error(
refusePrerequisite(
`--re-measure cannot run: ${unbuilt.length} workspace dependenc(ies) of the ledgered packages have `
+ `no built type entry point on disk -- ${unbuilt.join(', ')}.\n`
+ `Every number in DEBT and TEST_DEBT is measured with tsc resolving workspace imports through each `
Expand DownExpand Up@@ -3262,7 +3376,7 @@ function measureLedgers(packages, rootName, state) {
// caller can act on.
const stale = staleClosure(ledgered, workspaceBuildGraph(packages));
if (stale.length > 0) {
throw new Error(
refusePrerequisite(
`--re-measure cannot run: ${stale.length} workspace dependenc(ies) of the ledgered packages still have `
+ `a type entry point OLDER than their own sources after a full closure build -- ${stale.join(', ')}.\n`
+ `The build covers \`./packages/*\` and \`./packages/*/*\`, so a package that survives it is one those `
Expand DownExpand Up@@ -5378,6 +5492,79 @@ function selfTest() {
}
}

// ── THE EXIT-CODE CLASS ───────────────────────────────────────────────────
//
// Pinned because it is exactly the kind of fact that rots back silently. The
// defect this replaces was not a wrong number typed anywhere: it was a
// refusal that reached node's UNCAUGHT handler, which exits 1 -- so the
// regression shape is one careless `throw new Error(...)` added to a refusing
// function by an author who never thought about exit codes at all, and it
// announces itself with a green CI (every consumer of this gate treats any
// non-zero as failure, so 1-instead-of-3 is invisible to all of them) and a
// human-readable message that still says the right thing. Nothing else in
// this file would notice.
//
// So the pin is over the FUNCTION BODIES, not over a constant. Reading the
// real `Function.prototype.toString()` of the four functions that refuse is
// what makes a re-added `throw` fail here rather than in six weeks, on a card
// about something else.
const REFUSING = [refreshBuiltClosure, tscErrorCount, measureLedgers, gitIgnoredPaths];
for (const fn of REFUSING) {
const body = fn.toString();
if (/throw new Error\(/.test(body)) {
failures.push(
`${fn.name} raises a bare \`throw new Error(\` — an uncaught throw exits 1, the code this gate ` +
`reserves for a ledger entry that drifted UPWARD. A refusal must go through ` +
`refusePrerequisite() so it exits ${EXIT_PREREQUISITE_NOT_MET}.`,
);
}
if (!body.includes('refusePrerequisite(')) {
failures.push(`${fn.name} no longer refuses through refusePrerequisite() — the exit-code class is unpinned`);
}
}
// The NEGATIVE control, and the reason the loop above is a measurement rather
// than a tautology over an empty set: `readTsconfig` still throws, on purpose.
// A `tsconfig.json` checked into the tree that does not parse is a fact about
// the TREE -- a finding -- not a prerequisite the caller forgot to supply, so
// it keeps exit 1 and the pin above must be able to SEE a bare throw.
if (!/throw new Error\(/.test(readTsconfig.toString())) {
failures.push(
'readTsconfig no longer throws — the bare-throw pin above can no longer fail, so it stopped measuring',
);
}

const exitCodeCases = [
{ label: 'the refusal code is 3', ok: EXIT_PREREQUISITE_NOT_MET === 3 },
{ label: 'a finding is 1', ok: EXIT_FINDINGS === 1 },
{ label: 'the refusal code is distinct from a finding and from a pass',
ok: EXIT_PREREQUISITE_NOT_MET !== EXIT_FINDINGS && EXIT_PREREQUISITE_NOT_MET !== EXIT_OK },
];
for (const c of exitCodeCases) {
if (!c.ok) failures.push(`exit-code contract — ${c.label}`);
}

// The refusal TEXT. Four load-bearing clauses, each one a thing a reader who
// sees only the exit code cannot get anywhere else.
const refusalFixture = '--re-measure cannot run: 48 workspace dependenc(ies) have no built type entry point';
const refusalText = prerequisiteNotMetText(refusalFixture);
const textCases = [
{ label: 'carries the raising site\'s own message VERBATIM', ok: refusalText.includes(refusalFixture) },
{ label: 'names the class', ok: refusalText.includes('PREREQUISITE NOT MET') },
{ label: 'says it is neither a pass nor a finding', ok: /NOT a pass and NOT a finding/.test(refusalText) },
{ label: 'names its own code and the finding code it is distinct from',
ok: refusalText.includes(`Exit code ${EXIT_PREREQUISITE_NOT_MET}`)
&& refusalText.includes(`a finding's ${EXIT_FINDINGS}`) },
// The specific misreading this whole change exists to stop: taking an
// unmeasurable run for "a recorded debt went up" routes the reader to the
// ledger, and raising an entry there is a maintainer's act.
{ label: 'turns the reader away from the ledger rather than toward it',
ok: /no ledger entry below may\n?\s*be raised on it/.test(refusalText) },
{ label: 'warns that the code must be captured before any pipe', ok: refusalText.includes('BEFORE any pipe') },
];
for (const c of textCases) {
if (!c.ok) failures.push(`prerequisiteNotMetText — ${c.label}`);
}

// The shared workspace enumerator is a plain module with no CI invocation of
// its own (#11510); every gate that consolidated onto it folds in its checks.
failures.push(...workspaceEnumeratorSelfTest({ root: ROOT }));
Expand All@@ -5396,7 +5583,8 @@ function selfTest() {
+ ceilingCases.length + heapEnvCases.length} re-measure case(s) + ` +
`${typeEntryCases.length + closureCases.length + staleCases.length + sourceFileCases.length} ` +
`built-closure case(s) + ` +
`${planCases.length + rewriteCases.length + roundTripCases.length} auto-lowering case(s) hold.`,
`${planCases.length + rewriteCases.length + roundTripCases.length} auto-lowering case(s) + ` +
`${REFUSING.length * 2 + 1 + exitCodeCases.length + textCases.length} exit-code case(s) hold.`,
);
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 201 additions & 13 deletions scripts/check-type-check-coverage.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,40 @@
// # -- see BUILT CLOSURE.
// node scripts/check-type-check-coverage.mjs --self-test
//
// ## Exit codes -- and why a REFUSAL has one of its own
//
// 0 the tree was read and every invariant above holds.
// 1 a FINDING: a structural problem, or -- under `--re-measure` -- a ledger
// entry that drifted UPWARD. A claim about the tree.
// 3 PREREQUISITE NOT MET. The gate refused to measure, so nothing was
// measured and the run says NOTHING about the ledger. NOT a pass, and
// NOT a finding.
//
// The split is the sibling convention, not a local invention:
// `check-test-completeness.mjs` states it in its own failure text ("Exit code
// 3, distinct from a finding's 1") and `check-dual-build-cjs-loads.mjs`
// answers the IDENTICAL condition -- a gate that reads built output, run
// against a tree with no `dist/` -- with 3. This gate used to answer it by
// letting the refusal reach node's uncaught handler, which exits 1.
//
// ⭐ Why the class matters HERE in particular, more than it does for a gate
// whose 1 means "something is wrong somewhere". Exit 1 from this gate has one
// specific meaning: a package's recorded debt went UP. The remedy that meaning
// prescribes ends at the ledger below, and raising a DEBT/TEST_DEBT entry is a
// MAINTAINER-only act this file spells out at length. So a reader who takes an
// unmeasurable run for that red is pointed straight at the one place this
// evidence must never send them. The prose said so all along -- but the prose
// is not what an exit-code reader reads. A CI step, a wrapper, or an agent
// reconciling a derived gate family sees the number and nothing else.
//
// ⛔ The boundary, deliberately. Exit 3 is for a prerequisite the WORLD failed
// to supply and that the caller clears with a named command: an unbuilt or
// stale dependency closure, a closure that does not build, an absent `turbo`
// or `tsc` binary, a tsc that could not be spawned or could not read the
// project it was handed. A malformed `tsconfig.json` checked INTO the tree
// stays exit 1 -- that is a fact about the tree, which is what a finding is,
// and `readTsconfig` keeps throwing it.
//
// Invariants, per workspace package (the root workspace package included --
// #4311's audit counted its top-level TypeScript like any other package's):
//
Expand DownExpand Up@@ -468,6 +502,74 @@ import {
const ROOT = resolve(import.meta.dirname, '..');
const SELF = 'scripts/check-type-check-coverage.mjs';
const TRACKING_ISSUE = 'https://github.com/objectstack-ai/objectstack/issues/4311';

// The exit-code contract, NAMED rather than spelled inline at each site, so the
// self-test pins the value each path actually returns instead of a comment
// about it -- the shape `check-test-completeness.mjs` uses for the same split.
//
// ⛔ Module-local, NOT exported, and that is a decision rather than an
// oversight: this file's top level RUNS (it is a gate, invoked as a script and
// nothing else), so exporting any binding at all would make it importable for
// that binding and run the whole gate inside the importer -- the class
// `check:entry-guard` refuses, and it caught this constant block on its first
// run. The sibling that does export its codes carries `isEntrypoint` for
// exactly this reason. Nothing imports this file today; the day something
// needs to, the guard comes with the export.
const EXIT_OK = 0;
const EXIT_FINDINGS = 1;
const EXIT_PREREQUISITE_NOT_MET = 3;

/**
* The text a refusal prints, as a VALUE -- so the self-test can assert on the
* advisory without spawning a process or stubbing `process.exit`, the split
* `import-prerequisite.mjs` documents for the same reason.
*
* The gate's own refusal message is embedded VERBATIM. Every one of them was
* already argued at length at its throw site, and none of that reasoning is
* this frame's to restate, shorten or improve -- the frame adds only the two
* things a reader could not get from the message: what CLASS of result this is,
* and which exit code carries it.
*
* The pipe advisory is not decoration. `EXIT=$?` written after `cmd | tail -40`
* reads TAIL's status, and `head`/`tail` essentially never fail -- so a refusal
* and a green run are the same `0` there, which is the one reading this whole
* exit-code split exists to make impossible.
*
* @param {string} message the refusal, in the words of the site that raised it
* @returns {string}
*/
function prerequisiteNotMetText(message) {
return (
`\ncheck-type-check-coverage: PREREQUISITE NOT MET\n\n` +
`${message}\n\n` +
` ⛔ This is NOT a pass and NOT a finding: nothing was measured, so this run says\n` +
` NOTHING about whether any DEBT or TEST_DEBT number is still correct. In particular\n` +
` it is NOT evidence that a recorded number went up, and ⛔ no ledger entry below may\n` +
` be raised on it -- raising one is a maintainer's act even when the evidence is real.\n` +
` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n` +
` \`node ${SELF} --re-measure > /tmp/type-check-debt.log 2>&1; echo "EXIT=$?"\`.\n` +
` Piped, \`$?\` is the LAST command's status, and \`head\`/\`tail\` essentially never fail — that\n` +
` is the false green. \`\${PIPESTATUS[0]}\`/\`pipefail\` do recover this gate's own code.)`
);
}

/**
* Refuse to measure, and say so in the exit code as well as in the prose.
*
* ⛔ Prints and EXITS rather than throwing. A thrown refusal reaches node's
* uncaught handler, which exits 1 -- the code this gate reserves for "a
* recorded debt number went UP" -- and prints a stack trace over a message
* whose whole job is to be read. The self-test pins that the three functions
* that refuse contain no bare `throw new Error(` for exactly this reason: the
* rot this repairs is one careless `throw` away from returning.
*
* @param {string} message the refusal, in the words of the site that raised it
* @returns {never}
*/
function refusePrerequisite(message) {
console.error(prerequisiteNotMetText(message));
process.exit(EXIT_PREREQUISITE_NOT_MET);
}
// An `exclude` pattern that names tests (`**/*.test.ts`, `**/*.spec.tsx`, ...)
// and the files such a pattern hides. Kept deliberately broad: the question is
// "does this config steer tsc away from the test layer", not "which exact glob".
Expand DownExpand Up@@ -1532,12 +1634,14 @@ function gitIgnoredPaths(rels) {
maxBuffer: 16 * 1024 * 1024,
});
if (res.error) {
throw new Error(`git check-ignore could not run, so GENERATED_COVERED cannot be judged`, { cause: res.error });
refusePrerequisite(
`git check-ignore could not run, so GENERATED_COVERED cannot be judged: ${res.error.message}`,
);
}
// 0 = some path is ignored, 1 = none is. Anything else (128: not a git
// checkout, bad option) is a failed MEASUREMENT, not a clean tree.
if (res.status !== 0 && res.status !== 1) {
throw new Error(
refusePrerequisite(
`git check-ignore exited ${res.status}, so GENERATED_COVERED cannot be judged: ${String(res.stderr).trim()}`,
);
}
Expand DownExpand Up@@ -2701,14 +2805,14 @@ function workspaceBuildGraph(packages) {
function refreshBuiltClosure() {
const bin = join(ROOT, 'node_modules', '.bin', 'turbo');
if (!existsSync(bin)) {
throw new Error(`--re-measure needs the workspace's own turbo at ${bin}; run \`pnpm install\` first.`);
refusePrerequisite(`--re-measure needs the workspace's own turbo at ${bin}; run \`pnpm install\` first.`);
}
const args = ['run', 'build', '--filter=./packages/*', '--filter=./packages/*/*'];
const run = spawnSync(bin, args, { cwd: ROOT, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
if (run.error) throw new Error(`the closure build could not be run: ${run.error.message}`);
if (run.error) refusePrerequisite(`the closure build could not be run: ${run.error.message}`);
if (run.status !== 0) {
const output = `${run.stdout ?? ''}${run.stderr ?? ''}`.trim();
throw new Error(
refusePrerequisite(
`--re-measure cannot run: the ledgered packages' dependency closure does not build, so there is no `
+ `world to measure against. Fix the build first -- every number in DEBT and TEST_DEBT is measured `
+ `with tsc resolving workspace imports through each dependency's built \`dist/*.d.ts\`.\n`
Expand DownExpand Up@@ -2929,7 +3033,7 @@ const REMEASURE_HEAP = remeasureHeapCeiling({
function tscErrorCount(project, options = {}) {
const bin = join(ROOT, 'node_modules', '.bin', 'tsc');
if (!existsSync(bin)) {
throw new Error(`--re-measure needs the workspace's own tsc at ${bin}; run \`pnpm install\` first.`);
refusePrerequisite(`--re-measure needs the workspace's own tsc at ${bin}; run \`pnpm install\` first.`);
}
const run = spawnSync(bin, ['--noEmit', '--pretty', 'false', '-p', project], {
cwd: ROOT,
Expand All@@ -2941,10 +3045,10 @@ function tscErrorCount(project, options = {}) {
// dressed as a different one.
env: heapCappedEnv(process.env, REMEASURE_HEAP.mb),
});
if (run.error) throw new Error(`tsc could not be run for ${project}: ${run.error.message}`);
if (run.error) refusePrerequisite(`tsc could not be run for ${project}: ${run.error.message}`);
const output = `${run.stdout ?? ''}${run.stderr ?? ''}`;
if (TSC_SETUP_ERROR.test(output)) {
throw new Error(`tsc could not read ${project} -- the measurement is invalid, not zero:\n${output.trim()}`);
refusePrerequisite(`tsc could not read ${project} -- the measurement is invalid, not zero:\n${output.trim()}`);
}
const errors = countTscErrors(output, options);
// Exit 0 means a clean program; anything else must have produced diagnostics
Expand All@@ -2956,7 +3060,7 @@ function tscErrorCount(project, options = {}) {
// the `return 0` below -- refusing there would turn "this test layer is clean
// apart from an artefact of our own generated config" into a hard crash.
if (run.status !== 0 && countTscErrors(output) === 0) {
throw new Error(
refusePrerequisite(
`tsc exited ${run.status} for ${project} but printed no recognisable diagnostics -- ` +
`refusing to record 0:\n${output.trim().slice(0, 2000)}`,
);
Expand DownExpand Up@@ -3199,10 +3303,20 @@ function measureTestDebt(dir, hiddenTests = []) {
const holder = mkdtempSync(join(tmpdir(), 'objectstack-debt-remeasure-'));
const configPath = join(holder, REMEASURE_CONFIG);
writeFileSync(configPath, `${JSON.stringify(project, null, 2)}\n`);
// Registered on `exit` as WELL as in the `finally`, because `tscErrorCount`
// can now REFUSE, and a refusal calls `process.exit` -- which runs `exit`
// handlers and does NOT run `finally`. Belt and braces on purpose: the
// `finally` keeps the directory's lifetime visible where it is created, and
// the handler is what makes the refusal path leave nothing behind. `off`
// first in the `finally` so a run measuring 34 entries does not accumulate 34
// live handlers on a directory each has already removed.
const cleanup = () => rmSync(holder, { force: true, recursive: true });
process.once('exit', cleanup);
try {
return tscErrorCount(configPath, { dropRootDirDiagnostics: true });
} finally {
rmSync(holder, { force: true, recursive: true });
process.off('exit', cleanup);
cleanup();
}
}

Expand All@@ -3228,7 +3342,7 @@ function measureLedgers(packages, rootName, state) {
.filter((name) => dirOf.has(name));
const unbuilt = unbuiltClosure(ledgered, workspaceBuildGraph(packages));
if (unbuilt.length > 0) {
throw new Error(
refusePrerequisite(
`--re-measure cannot run: ${unbuilt.length} workspace dependenc(ies) of the ledgered packages have `
+ `no built type entry point on disk -- ${unbuilt.join(', ')}.\n`
+ `Every number in DEBT and TEST_DEBT is measured with tsc resolving workspace imports through each `
Expand DownExpand Up@@ -3262,7 +3376,7 @@ function measureLedgers(packages, rootName, state) {
// caller can act on.
const stale = staleClosure(ledgered, workspaceBuildGraph(packages));
if (stale.length > 0) {
throw new Error(
refusePrerequisite(
`--re-measure cannot run: ${stale.length} workspace dependenc(ies) of the ledgered packages still have `
+ `a type entry point OLDER than their own sources after a full closure build -- ${stale.join(', ')}.\n`
+ `The build covers \`./packages/*\` and \`./packages/*/*\`, so a package that survives it is one those `
Expand DownExpand Up@@ -5378,6 +5492,79 @@ function selfTest() {
}
}

// ── THE EXIT-CODE CLASS ───────────────────────────────────────────────────
//
// Pinned because it is exactly the kind of fact that rots back silently. The
// defect this replaces was not a wrong number typed anywhere: it was a
// refusal that reached node's UNCAUGHT handler, which exits 1 -- so the
// regression shape is one careless `throw new Error(...)` added to a refusing
// function by an author who never thought about exit codes at all, and it
// announces itself with a green CI (every consumer of this gate treats any
// non-zero as failure, so 1-instead-of-3 is invisible to all of them) and a
// human-readable message that still says the right thing. Nothing else in
// this file would notice.
//
// So the pin is over the FUNCTION BODIES, not over a constant. Reading the
// real `Function.prototype.toString()` of the four functions that refuse is
// what makes a re-added `throw` fail here rather than in six weeks, on a card
// about something else.
const REFUSING = [refreshBuiltClosure, tscErrorCount, measureLedgers, gitIgnoredPaths];
for (const fn of REFUSING) {
const body = fn.toString();
if (/throw new Error\(/.test(body)) {
failures.push(
`${fn.name} raises a bare \`throw new Error(\` — an uncaught throw exits 1, the code this gate ` +
`reserves for a ledger entry that drifted UPWARD. A refusal must go through ` +
`refusePrerequisite() so it exits ${EXIT_PREREQUISITE_NOT_MET}.`,
);
}
if (!body.includes('refusePrerequisite(')) {
failures.push(`${fn.name} no longer refuses through refusePrerequisite() — the exit-code class is unpinned`);
}
}
// The NEGATIVE control, and the reason the loop above is a measurement rather
// than a tautology over an empty set: `readTsconfig` still throws, on purpose.
// A `tsconfig.json` checked into the tree that does not parse is a fact about
// the TREE -- a finding -- not a prerequisite the caller forgot to supply, so
// it keeps exit 1 and the pin above must be able to SEE a bare throw.
if (!/throw new Error\(/.test(readTsconfig.toString())) {
failures.push(
'readTsconfig no longer throws — the bare-throw pin above can no longer fail, so it stopped measuring',
);
}

const exitCodeCases = [
{ label: 'the refusal code is 3', ok: EXIT_PREREQUISITE_NOT_MET === 3 },
{ label: 'a finding is 1', ok: EXIT_FINDINGS === 1 },
{ label: 'the refusal code is distinct from a finding and from a pass',
ok: EXIT_PREREQUISITE_NOT_MET !== EXIT_FINDINGS && EXIT_PREREQUISITE_NOT_MET !== EXIT_OK },
];
for (const c of exitCodeCases) {
if (!c.ok) failures.push(`exit-code contract — ${c.label}`);
}

// The refusal TEXT. Four load-bearing clauses, each one a thing a reader who
// sees only the exit code cannot get anywhere else.
const refusalFixture = '--re-measure cannot run: 48 workspace dependenc(ies) have no built type entry point';
const refusalText = prerequisiteNotMetText(refusalFixture);
const textCases = [
{ label: 'carries the raising site\'s own message VERBATIM', ok: refusalText.includes(refusalFixture) },
{ label: 'names the class', ok: refusalText.includes('PREREQUISITE NOT MET') },
{ label: 'says it is neither a pass nor a finding', ok: /NOT a pass and NOT a finding/.test(refusalText) },
{ label: 'names its own code and the finding code it is distinct from',
ok: refusalText.includes(`Exit code ${EXIT_PREREQUISITE_NOT_MET}`)
&& refusalText.includes(`a finding's ${EXIT_FINDINGS}`) },
// The specific misreading this whole change exists to stop: taking an
// unmeasurable run for "a recorded debt went up" routes the reader to the
// ledger, and raising an entry there is a maintainer's act.
{ label: 'turns the reader away from the ledger rather than toward it',
ok: /no ledger entry below may\n?\s*be raised on it/.test(refusalText) },
{ label: 'warns that the code must be captured before any pipe', ok: refusalText.includes('BEFORE any pipe') },
];
for (const c of textCases) {
if (!c.ok) failures.push(`prerequisiteNotMetText — ${c.label}`);
}

// The shared workspace enumerator is a plain module with no CI invocation of
// its own (#11510); every gate that consolidated onto it folds in its checks.
failures.push(...workspaceEnumeratorSelfTest({ root: ROOT }));
Expand All@@ -5396,7 +5583,8 @@ function selfTest() {
+ ceilingCases.length + heapEnvCases.length} re-measure case(s) + ` +
`${typeEntryCases.length + closureCases.length + staleCases.length + sourceFileCases.length} ` +
`built-closure case(s) + ` +
`${planCases.length + rewriteCases.length + roundTripCases.length} auto-lowering case(s) hold.`,
`${planCases.length + rewriteCases.length + roundTripCases.length} auto-lowering case(s) + ` +
`${REFUSING.length * 2 + 1 + exitCodeCases.length + textCases.length} exit-code case(s) hold.`,
);
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 201 additions & 13 deletions scripts/check-type-check-coverage.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,40 @@
// # -- see BUILT CLOSURE.
// node scripts/check-type-check-coverage.mjs --self-test
//
// ## Exit codes -- and why a REFUSAL has one of its own
//
// 0 the tree was read and every invariant above holds.
// 1 a FINDING: a structural problem, or -- under `--re-measure` -- a ledger
// entry that drifted UPWARD. A claim about the tree.
// 3 PREREQUISITE NOT MET. The gate refused to measure, so nothing was
// measured and the run says NOTHING about the ledger. NOT a pass, and
// NOT a finding.
//
// The split is the sibling convention, not a local invention:
// `check-test-completeness.mjs` states it in its own failure text ("Exit code
// 3, distinct from a finding's 1") and `check-dual-build-cjs-loads.mjs`
// answers the IDENTICAL condition -- a gate that reads built output, run
// against a tree with no `dist/` -- with 3. This gate used to answer it by
// letting the refusal reach node's uncaught handler, which exits 1.
//
// ⭐ Why the class matters HERE in particular, more than it does for a gate
// whose 1 means "something is wrong somewhere". Exit 1 from this gate has one
// specific meaning: a package's recorded debt went UP. The remedy that meaning
// prescribes ends at the ledger below, and raising a DEBT/TEST_DEBT entry is a
// MAINTAINER-only act this file spells out at length. So a reader who takes an
// unmeasurable run for that red is pointed straight at the one place this
// evidence must never send them. The prose said so all along -- but the prose
// is not what an exit-code reader reads. A CI step, a wrapper, or an agent
// reconciling a derived gate family sees the number and nothing else.
//
// ⛔ The boundary, deliberately. Exit 3 is for a prerequisite the WORLD failed
// to supply and that the caller clears with a named command: an unbuilt or
// stale dependency closure, a closure that does not build, an absent `turbo`
// or `tsc` binary, a tsc that could not be spawned or could not read the
// project it was handed. A malformed `tsconfig.json` checked INTO the tree
// stays exit 1 -- that is a fact about the tree, which is what a finding is,
// and `readTsconfig` keeps throwing it.
//
// Invariants, per workspace package (the root workspace package included --
// #4311's audit counted its top-level TypeScript like any other package's):
//
Expand DownExpand Up@@ -468,6 +502,74 @@ import {
const ROOT = resolve(import.meta.dirname, '..');
const SELF = 'scripts/check-type-check-coverage.mjs';
const TRACKING_ISSUE = 'https://github.com/objectstack-ai/objectstack/issues/4311';

// The exit-code contract, NAMED rather than spelled inline at each site, so the
// self-test pins the value each path actually returns instead of a comment
// about it -- the shape `check-test-completeness.mjs` uses for the same split.
//
// ⛔ Module-local, NOT exported, and that is a decision rather than an
// oversight: this file's top level RUNS (it is a gate, invoked as a script and
// nothing else), so exporting any binding at all would make it importable for
// that binding and run the whole gate inside the importer -- the class
// `check:entry-guard` refuses, and it caught this constant block on its first
// run. The sibling that does export its codes carries `isEntrypoint` for
// exactly this reason. Nothing imports this file today; the day something
// needs to, the guard comes with the export.
const EXIT_OK = 0;
const EXIT_FINDINGS = 1;
const EXIT_PREREQUISITE_NOT_MET = 3;

/**
* The text a refusal prints, as a VALUE -- so the self-test can assert on the
* advisory without spawning a process or stubbing `process.exit`, the split
* `import-prerequisite.mjs` documents for the same reason.
*
* The gate's own refusal message is embedded VERBATIM. Every one of them was
* already argued at length at its throw site, and none of that reasoning is
* this frame's to restate, shorten or improve -- the frame adds only the two
* things a reader could not get from the message: what CLASS of result this is,
* and which exit code carries it.
*
* The pipe advisory is not decoration. `EXIT=$?` written after `cmd | tail -40`
* reads TAIL's status, and `head`/`tail` essentially never fail -- so a refusal
* and a green run are the same `0` there, which is the one reading this whole
* exit-code split exists to make impossible.
*
* @param {string} message the refusal, in the words of the site that raised it
* @returns {string}
*/
function prerequisiteNotMetText(message) {
return (
`\ncheck-type-check-coverage: PREREQUISITE NOT MET\n\n` +
`${message}\n\n` +
` ⛔ This is NOT a pass and NOT a finding: nothing was measured, so this run says\n` +
` NOTHING about whether any DEBT or TEST_DEBT number is still correct. In particular\n` +
` it is NOT evidence that a recorded number went up, and ⛔ no ledger entry below may\n` +
` be raised on it -- raising one is a maintainer's act even when the evidence is real.\n` +
` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n` +
` \`node ${SELF} --re-measure > /tmp/type-check-debt.log 2>&1; echo "EXIT=$?"\`.\n` +
` Piped, \`$?\` is the LAST command's status, and \`head\`/\`tail\` essentially never fail — that\n` +
` is the false green. \`\${PIPESTATUS[0]}\`/\`pipefail\` do recover this gate's own code.)`
);
}

/**
* Refuse to measure, and say so in the exit code as well as in the prose.
*
* ⛔ Prints and EXITS rather than throwing. A thrown refusal reaches node's
* uncaught handler, which exits 1 -- the code this gate reserves for "a
* recorded debt number went UP" -- and prints a stack trace over a message
* whose whole job is to be read. The self-test pins that the three functions
* that refuse contain no bare `throw new Error(` for exactly this reason: the
* rot this repairs is one careless `throw` away from returning.
*
* @param {string} message the refusal, in the words of the site that raised it
* @returns {never}
*/
function refusePrerequisite(message) {
console.error(prerequisiteNotMetText(message));
process.exit(EXIT_PREREQUISITE_NOT_MET);
}
// An `exclude` pattern that names tests (`**/*.test.ts`, `**/*.spec.tsx`, ...)
// and the files such a pattern hides. Kept deliberately broad: the question is
// "does this config steer tsc away from the test layer", not "which exact glob".
Expand DownExpand Up@@ -1532,12 +1634,14 @@ function gitIgnoredPaths(rels) {
maxBuffer: 16 * 1024 * 1024,
});
if (res.error) {
throw new Error(`git check-ignore could not run, so GENERATED_COVERED cannot be judged`, { cause: res.error });
refusePrerequisite(
`git check-ignore could not run, so GENERATED_COVERED cannot be judged: ${res.error.message}`,
);
}
// 0 = some path is ignored, 1 = none is. Anything else (128: not a git
// checkout, bad option) is a failed MEASUREMENT, not a clean tree.
if (res.status !== 0 && res.status !== 1) {
throw new Error(
refusePrerequisite(
`git check-ignore exited ${res.status}, so GENERATED_COVERED cannot be judged: ${String(res.stderr).trim()}`,
);
}
Expand DownExpand Up@@ -2701,14 +2805,14 @@ function workspaceBuildGraph(packages) {
function refreshBuiltClosure() {
const bin = join(ROOT, 'node_modules', '.bin', 'turbo');
if (!existsSync(bin)) {
throw new Error(`--re-measure needs the workspace's own turbo at ${bin}; run \`pnpm install\` first.`);
refusePrerequisite(`--re-measure needs the workspace's own turbo at ${bin}; run \`pnpm install\` first.`);
}
const args = ['run', 'build', '--filter=./packages/*', '--filter=./packages/*/*'];
const run = spawnSync(bin, args, { cwd: ROOT, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
if (run.error) throw new Error(`the closure build could not be run: ${run.error.message}`);
if (run.error) refusePrerequisite(`the closure build could not be run: ${run.error.message}`);
if (run.status !== 0) {
const output = `${run.stdout ?? ''}${run.stderr ?? ''}`.trim();
throw new Error(
refusePrerequisite(
`--re-measure cannot run: the ledgered packages' dependency closure does not build, so there is no `
+ `world to measure against. Fix the build first -- every number in DEBT and TEST_DEBT is measured `
+ `with tsc resolving workspace imports through each dependency's built \`dist/*.d.ts\`.\n`
Expand DownExpand Up@@ -2929,7 +3033,7 @@ const REMEASURE_HEAP = remeasureHeapCeiling({
function tscErrorCount(project, options = {}) {
const bin = join(ROOT, 'node_modules', '.bin', 'tsc');
if (!existsSync(bin)) {
throw new Error(`--re-measure needs the workspace's own tsc at ${bin}; run \`pnpm install\` first.`);
refusePrerequisite(`--re-measure needs the workspace's own tsc at ${bin}; run \`pnpm install\` first.`);
}
const run = spawnSync(bin, ['--noEmit', '--pretty', 'false', '-p', project], {
cwd: ROOT,
Expand All@@ -2941,10 +3045,10 @@ function tscErrorCount(project, options = {}) {
// dressed as a different one.
env: heapCappedEnv(process.env, REMEASURE_HEAP.mb),
});
if (run.error) throw new Error(`tsc could not be run for ${project}: ${run.error.message}`);
if (run.error) refusePrerequisite(`tsc could not be run for ${project}: ${run.error.message}`);
const output = `${run.stdout ?? ''}${run.stderr ?? ''}`;
if (TSC_SETUP_ERROR.test(output)) {
throw new Error(`tsc could not read ${project} -- the measurement is invalid, not zero:\n${output.trim()}`);
refusePrerequisite(`tsc could not read ${project} -- the measurement is invalid, not zero:\n${output.trim()}`);
}
const errors = countTscErrors(output, options);
// Exit 0 means a clean program; anything else must have produced diagnostics
Expand All@@ -2956,7 +3060,7 @@ function tscErrorCount(project, options = {}) {
// the `return 0` below -- refusing there would turn "this test layer is clean
// apart from an artefact of our own generated config" into a hard crash.
if (run.status !== 0 && countTscErrors(output) === 0) {
throw new Error(
refusePrerequisite(
`tsc exited ${run.status} for ${project} but printed no recognisable diagnostics -- ` +
`refusing to record 0:\n${output.trim().slice(0, 2000)}`,
);
Expand DownExpand Up@@ -3199,10 +3303,20 @@ function measureTestDebt(dir, hiddenTests = []) {
const holder = mkdtempSync(join(tmpdir(), 'objectstack-debt-remeasure-'));
const configPath = join(holder, REMEASURE_CONFIG);
writeFileSync(configPath, `${JSON.stringify(project, null, 2)}\n`);
// Registered on `exit` as WELL as in the `finally`, because `tscErrorCount`
// can now REFUSE, and a refusal calls `process.exit` -- which runs `exit`
// handlers and does NOT run `finally`. Belt and braces on purpose: the
// `finally` keeps the directory's lifetime visible where it is created, and
// the handler is what makes the refusal path leave nothing behind. `off`
// first in the `finally` so a run measuring 34 entries does not accumulate 34
// live handlers on a directory each has already removed.
const cleanup = () => rmSync(holder, { force: true, recursive: true });
process.once('exit', cleanup);
try {
return tscErrorCount(configPath, { dropRootDirDiagnostics: true });
} finally {
rmSync(holder, { force: true, recursive: true });
process.off('exit', cleanup);
cleanup();
}
}

Expand All@@ -3228,7 +3342,7 @@ function measureLedgers(packages, rootName, state) {
.filter((name) => dirOf.has(name));
const unbuilt = unbuiltClosure(ledgered, workspaceBuildGraph(packages));
if (unbuilt.length > 0) {
throw new Error(
refusePrerequisite(
`--re-measure cannot run: ${unbuilt.length} workspace dependenc(ies) of the ledgered packages have `
+ `no built type entry point on disk -- ${unbuilt.join(', ')}.\n`
+ `Every number in DEBT and TEST_DEBT is measured with tsc resolving workspace imports through each `
Expand DownExpand Up@@ -3262,7 +3376,7 @@ function measureLedgers(packages, rootName, state) {
// caller can act on.
const stale = staleClosure(ledgered, workspaceBuildGraph(packages));
if (stale.length > 0) {
throw new Error(
refusePrerequisite(
`--re-measure cannot run: ${stale.length} workspace dependenc(ies) of the ledgered packages still have `
+ `a type entry point OLDER than their own sources after a full closure build -- ${stale.join(', ')}.\n`
+ `The build covers \`./packages/*\` and \`./packages/*/*\`, so a package that survives it is one those `
Expand DownExpand Up@@ -5378,6 +5492,79 @@ function selfTest() {
}
}

// ── THE EXIT-CODE CLASS ───────────────────────────────────────────────────
//
// Pinned because it is exactly the kind of fact that rots back silently. The
// defect this replaces was not a wrong number typed anywhere: it was a
// refusal that reached node's UNCAUGHT handler, which exits 1 -- so the
// regression shape is one careless `throw new Error(...)` added to a refusing
// function by an author who never thought about exit codes at all, and it
// announces itself with a green CI (every consumer of this gate treats any
// non-zero as failure, so 1-instead-of-3 is invisible to all of them) and a
// human-readable message that still says the right thing. Nothing else in
// this file would notice.
//
// So the pin is over the FUNCTION BODIES, not over a constant. Reading the
// real `Function.prototype.toString()` of the four functions that refuse is
// what makes a re-added `throw` fail here rather than in six weeks, on a card
// about something else.
const REFUSING = [refreshBuiltClosure, tscErrorCount, measureLedgers, gitIgnoredPaths];
for (const fn of REFUSING) {
const body = fn.toString();
if (/throw new Error\(/.test(body)) {
failures.push(
`${fn.name} raises a bare \`throw new Error(\` — an uncaught throw exits 1, the code this gate ` +
`reserves for a ledger entry that drifted UPWARD. A refusal must go through ` +
`refusePrerequisite() so it exits ${EXIT_PREREQUISITE_NOT_MET}.`,
);
}
if (!body.includes('refusePrerequisite(')) {
failures.push(`${fn.name} no longer refuses through refusePrerequisite() — the exit-code class is unpinned`);
}
}
// The NEGATIVE control, and the reason the loop above is a measurement rather
// than a tautology over an empty set: `readTsconfig` still throws, on purpose.
// A `tsconfig.json` checked into the tree that does not parse is a fact about
// the TREE -- a finding -- not a prerequisite the caller forgot to supply, so
// it keeps exit 1 and the pin above must be able to SEE a bare throw.
if (!/throw new Error\(/.test(readTsconfig.toString())) {
failures.push(
'readTsconfig no longer throws — the bare-throw pin above can no longer fail, so it stopped measuring',
);
}

const exitCodeCases = [
{ label: 'the refusal code is 3', ok: EXIT_PREREQUISITE_NOT_MET === 3 },
{ label: 'a finding is 1', ok: EXIT_FINDINGS === 1 },
{ label: 'the refusal code is distinct from a finding and from a pass',
ok: EXIT_PREREQUISITE_NOT_MET !== EXIT_FINDINGS && EXIT_PREREQUISITE_NOT_MET !== EXIT_OK },
];
for (const c of exitCodeCases) {
if (!c.ok) failures.push(`exit-code contract — ${c.label}`);
}

// The refusal TEXT. Four load-bearing clauses, each one a thing a reader who
// sees only the exit code cannot get anywhere else.
const refusalFixture = '--re-measure cannot run: 48 workspace dependenc(ies) have no built type entry point';
const refusalText = prerequisiteNotMetText(refusalFixture);
const textCases = [
{ label: 'carries the raising site\'s own message VERBATIM', ok: refusalText.includes(refusalFixture) },
{ label: 'names the class', ok: refusalText.includes('PREREQUISITE NOT MET') },
{ label: 'says it is neither a pass nor a finding', ok: /NOT a pass and NOT a finding/.test(refusalText) },
{ label: 'names its own code and the finding code it is distinct from',
ok: refusalText.includes(`Exit code ${EXIT_PREREQUISITE_NOT_MET}`)
&& refusalText.includes(`a finding's ${EXIT_FINDINGS}`) },
// The specific misreading this whole change exists to stop: taking an
// unmeasurable run for "a recorded debt went up" routes the reader to the
// ledger, and raising an entry there is a maintainer's act.
{ label: 'turns the reader away from the ledger rather than toward it',
ok: /no ledger entry below may\n?\s*be raised on it/.test(refusalText) },
{ label: 'warns that the code must be captured before any pipe', ok: refusalText.includes('BEFORE any pipe') },
];
for (const c of textCases) {
if (!c.ok) failures.push(`prerequisiteNotMetText — ${c.label}`);
}

// The shared workspace enumerator is a plain module with no CI invocation of
// its own (#11510); every gate that consolidated onto it folds in its checks.
failures.push(...workspaceEnumeratorSelfTest({ root: ROOT }));
Expand All@@ -5396,7 +5583,8 @@ function selfTest() {
+ ceilingCases.length + heapEnvCases.length} re-measure case(s) + ` +
`${typeEntryCases.length + closureCases.length + staleCases.length + sourceFileCases.length} ` +
`built-closure case(s) + ` +
`${planCases.length + rewriteCases.length + roundTripCases.length} auto-lowering case(s) hold.`,
`${planCases.length + rewriteCases.length + roundTripCases.length} auto-lowering case(s) + ` +
`${REFUSING.length * 2 + 1 + exitCodeCases.length + textCases.length} exit-code case(s) hold.`,
);
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 201 additions & 13 deletions scripts/check-type-check-coverage.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,40 @@
// # -- see BUILT CLOSURE.
// node scripts/check-type-check-coverage.mjs --self-test
//
// ## Exit codes -- and why a REFUSAL has one of its own
//
// 0 the tree was read and every invariant above holds.
// 1 a FINDING: a structural problem, or -- under `--re-measure` -- a ledger
// entry that drifted UPWARD. A claim about the tree.
// 3 PREREQUISITE NOT MET. The gate refused to measure, so nothing was
// measured and the run says NOTHING about the ledger. NOT a pass, and
// NOT a finding.
//
// The split is the sibling convention, not a local invention:
// `check-test-completeness.mjs` states it in its own failure text ("Exit code
// 3, distinct from a finding's 1") and `check-dual-build-cjs-loads.mjs`
// answers the IDENTICAL condition -- a gate that reads built output, run
// against a tree with no `dist/` -- with 3. This gate used to answer it by
// letting the refusal reach node's uncaught handler, which exits 1.
//
// ⭐ Why the class matters HERE in particular, more than it does for a gate
// whose 1 means "something is wrong somewhere". Exit 1 from this gate has one
// specific meaning: a package's recorded debt went UP. The remedy that meaning
// prescribes ends at the ledger below, and raising a DEBT/TEST_DEBT entry is a
// MAINTAINER-only act this file spells out at length. So a reader who takes an
// unmeasurable run for that red is pointed straight at the one place this
// evidence must never send them. The prose said so all along -- but the prose
// is not what an exit-code reader reads. A CI step, a wrapper, or an agent
// reconciling a derived gate family sees the number and nothing else.
//
// ⛔ The boundary, deliberately. Exit 3 is for a prerequisite the WORLD failed
// to supply and that the caller clears with a named command: an unbuilt or
// stale dependency closure, a closure that does not build, an absent `turbo`
// or `tsc` binary, a tsc that could not be spawned or could not read the
// project it was handed. A malformed `tsconfig.json` checked INTO the tree
// stays exit 1 -- that is a fact about the tree, which is what a finding is,
// and `readTsconfig` keeps throwing it.
//
// Invariants, per workspace package (the root workspace package included --
// #4311's audit counted its top-level TypeScript like any other package's):
//
Expand DownExpand Up@@ -468,6 +502,74 @@ import {
const ROOT = resolve(import.meta.dirname, '..');
const SELF = 'scripts/check-type-check-coverage.mjs';
const TRACKING_ISSUE = 'https://github.com/objectstack-ai/objectstack/issues/4311';

// The exit-code contract, NAMED rather than spelled inline at each site, so the
// self-test pins the value each path actually returns instead of a comment
// about it -- the shape `check-test-completeness.mjs` uses for the same split.
//
// ⛔ Module-local, NOT exported, and that is a decision rather than an
// oversight: this file's top level RUNS (it is a gate, invoked as a script and
// nothing else), so exporting any binding at all would make it importable for
// that binding and run the whole gate inside the importer -- the class
// `check:entry-guard` refuses, and it caught this constant block on its first
// run. The sibling that does export its codes carries `isEntrypoint` for
// exactly this reason. Nothing imports this file today; the day something
// needs to, the guard comes with the export.
const EXIT_OK = 0;
const EXIT_FINDINGS = 1;
const EXIT_PREREQUISITE_NOT_MET = 3;

/**
* The text a refusal prints, as a VALUE -- so the self-test can assert on the
* advisory without spawning a process or stubbing `process.exit`, the split
* `import-prerequisite.mjs` documents for the same reason.
*
* The gate's own refusal message is embedded VERBATIM. Every one of them was
* already argued at length at its throw site, and none of that reasoning is
* this frame's to restate, shorten or improve -- the frame adds only the two
* things a reader could not get from the message: what CLASS of result this is,
* and which exit code carries it.
*
* The pipe advisory is not decoration. `EXIT=$?` written after `cmd | tail -40`
* reads TAIL's status, and `head`/`tail` essentially never fail -- so a refusal
* and a green run are the same `0` there, which is the one reading this whole
* exit-code split exists to make impossible.
*
* @param {string} message the refusal, in the words of the site that raised it
* @returns {string}
*/
function prerequisiteNotMetText(message) {
return (
`\ncheck-type-check-coverage: PREREQUISITE NOT MET\n\n` +
`${message}\n\n` +
` ⛔ This is NOT a pass and NOT a finding: nothing was measured, so this run says\n` +
` NOTHING about whether any DEBT or TEST_DEBT number is still correct. In particular\n` +
` it is NOT evidence that a recorded number went up, and ⛔ no ledger entry below may\n` +
` be raised on it -- raising one is a maintainer's act even when the evidence is real.\n` +
` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n` +
` \`node ${SELF} --re-measure > /tmp/type-check-debt.log 2>&1; echo "EXIT=$?"\`.\n` +
` Piped, \`$?\` is the LAST command's status, and \`head\`/\`tail\` essentially never fail — that\n` +
` is the false green. \`\${PIPESTATUS[0]}\`/\`pipefail\` do recover this gate's own code.)`
);
}

/**
* Refuse to measure, and say so in the exit code as well as in the prose.
*
* ⛔ Prints and EXITS rather than throwing. A thrown refusal reaches node's
* uncaught handler, which exits 1 -- the code this gate reserves for "a
* recorded debt number went UP" -- and prints a stack trace over a message
* whose whole job is to be read. The self-test pins that the three functions
* that refuse contain no bare `throw new Error(` for exactly this reason: the
* rot this repairs is one careless `throw` away from returning.
*
* @param {string} message the refusal, in the words of the site that raised it
* @returns {never}
*/
function refusePrerequisite(message) {
console.error(prerequisiteNotMetText(message));
process.exit(EXIT_PREREQUISITE_NOT_MET);
}
// An `exclude` pattern that names tests (`**/*.test.ts`, `**/*.spec.tsx`, ...)
// and the files such a pattern hides. Kept deliberately broad: the question is
// "does this config steer tsc away from the test layer", not "which exact glob".
Expand DownExpand Up@@ -1532,12 +1634,14 @@ function gitIgnoredPaths(rels) {
maxBuffer: 16 * 1024 * 1024,
});
if (res.error) {
throw new Error(`git check-ignore could not run, so GENERATED_COVERED cannot be judged`, { cause: res.error });
refusePrerequisite(
`git check-ignore could not run, so GENERATED_COVERED cannot be judged: ${res.error.message}`,
);
}
// 0 = some path is ignored, 1 = none is. Anything else (128: not a git
// checkout, bad option) is a failed MEASUREMENT, not a clean tree.
if (res.status !== 0 && res.status !== 1) {
throw new Error(
refusePrerequisite(
`git check-ignore exited ${res.status}, so GENERATED_COVERED cannot be judged: ${String(res.stderr).trim()}`,
);
}
Expand DownExpand Up@@ -2701,14 +2805,14 @@ function workspaceBuildGraph(packages) {
function refreshBuiltClosure() {
const bin = join(ROOT, 'node_modules', '.bin', 'turbo');
if (!existsSync(bin)) {
throw new Error(`--re-measure needs the workspace's own turbo at ${bin}; run \`pnpm install\` first.`);
refusePrerequisite(`--re-measure needs the workspace's own turbo at ${bin}; run \`pnpm install\` first.`);
}
const args = ['run', 'build', '--filter=./packages/*', '--filter=./packages/*/*'];
const run = spawnSync(bin, args, { cwd: ROOT, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
if (run.error) throw new Error(`the closure build could not be run: ${run.error.message}`);
if (run.error) refusePrerequisite(`the closure build could not be run: ${run.error.message}`);
if (run.status !== 0) {
const output = `${run.stdout ?? ''}${run.stderr ?? ''}`.trim();
throw new Error(
refusePrerequisite(
`--re-measure cannot run: the ledgered packages' dependency closure does not build, so there is no `
+ `world to measure against. Fix the build first -- every number in DEBT and TEST_DEBT is measured `
+ `with tsc resolving workspace imports through each dependency's built \`dist/*.d.ts\`.\n`
Expand DownExpand Up@@ -2929,7 +3033,7 @@ const REMEASURE_HEAP = remeasureHeapCeiling({
function tscErrorCount(project, options = {}) {
const bin = join(ROOT, 'node_modules', '.bin', 'tsc');
if (!existsSync(bin)) {
throw new Error(`--re-measure needs the workspace's own tsc at ${bin}; run \`pnpm install\` first.`);
refusePrerequisite(`--re-measure needs the workspace's own tsc at ${bin}; run \`pnpm install\` first.`);
}
const run = spawnSync(bin, ['--noEmit', '--pretty', 'false', '-p', project], {
cwd: ROOT,
Expand All@@ -2941,10 +3045,10 @@ function tscErrorCount(project, options = {}) {
// dressed as a different one.
env: heapCappedEnv(process.env, REMEASURE_HEAP.mb),
});
if (run.error) throw new Error(`tsc could not be run for ${project}: ${run.error.message}`);
if (run.error) refusePrerequisite(`tsc could not be run for ${project}: ${run.error.message}`);
const output = `${run.stdout ?? ''}${run.stderr ?? ''}`;
if (TSC_SETUP_ERROR.test(output)) {
throw new Error(`tsc could not read ${project} -- the measurement is invalid, not zero:\n${output.trim()}`);
refusePrerequisite(`tsc could not read ${project} -- the measurement is invalid, not zero:\n${output.trim()}`);
}
const errors = countTscErrors(output, options);
// Exit 0 means a clean program; anything else must have produced diagnostics
Expand All@@ -2956,7 +3060,7 @@ function tscErrorCount(project, options = {}) {
// the `return 0` below -- refusing there would turn "this test layer is clean
// apart from an artefact of our own generated config" into a hard crash.
if (run.status !== 0 && countTscErrors(output) === 0) {
throw new Error(
refusePrerequisite(
`tsc exited ${run.status} for ${project} but printed no recognisable diagnostics -- ` +
`refusing to record 0:\n${output.trim().slice(0, 2000)}`,
);
Expand DownExpand Up@@ -3199,10 +3303,20 @@ function measureTestDebt(dir, hiddenTests = []) {
const holder = mkdtempSync(join(tmpdir(), 'objectstack-debt-remeasure-'));
const configPath = join(holder, REMEASURE_CONFIG);
writeFileSync(configPath, `${JSON.stringify(project, null, 2)}\n`);
// Registered on `exit` as WELL as in the `finally`, because `tscErrorCount`
// can now REFUSE, and a refusal calls `process.exit` -- which runs `exit`
// handlers and does NOT run `finally`. Belt and braces on purpose: the
// `finally` keeps the directory's lifetime visible where it is created, and
// the handler is what makes the refusal path leave nothing behind. `off`
// first in the `finally` so a run measuring 34 entries does not accumulate 34
// live handlers on a directory each has already removed.
const cleanup = () => rmSync(holder, { force: true, recursive: true });
process.once('exit', cleanup);
try {
return tscErrorCount(configPath, { dropRootDirDiagnostics: true });
} finally {
rmSync(holder, { force: true, recursive: true });
process.off('exit', cleanup);
cleanup();
}
}

Expand All@@ -3228,7 +3342,7 @@ function measureLedgers(packages, rootName, state) {
.filter((name) => dirOf.has(name));
const unbuilt = unbuiltClosure(ledgered, workspaceBuildGraph(packages));
if (unbuilt.length > 0) {
throw new Error(
refusePrerequisite(
`--re-measure cannot run: ${unbuilt.length} workspace dependenc(ies) of the ledgered packages have `
+ `no built type entry point on disk -- ${unbuilt.join(', ')}.\n`
+ `Every number in DEBT and TEST_DEBT is measured with tsc resolving workspace imports through each `
Expand DownExpand Up@@ -3262,7 +3376,7 @@ function measureLedgers(packages, rootName, state) {
// caller can act on.
const stale = staleClosure(ledgered, workspaceBuildGraph(packages));
if (stale.length > 0) {
throw new Error(
refusePrerequisite(
`--re-measure cannot run: ${stale.length} workspace dependenc(ies) of the ledgered packages still have `
+ `a type entry point OLDER than their own sources after a full closure build -- ${stale.join(', ')}.\n`
+ `The build covers \`./packages/*\` and \`./packages/*/*\`, so a package that survives it is one those `
Expand DownExpand Up@@ -5378,6 +5492,79 @@ function selfTest() {
}
}

// ── THE EXIT-CODE CLASS ───────────────────────────────────────────────────
//
// Pinned because it is exactly the kind of fact that rots back silently. The
// defect this replaces was not a wrong number typed anywhere: it was a
// refusal that reached node's UNCAUGHT handler, which exits 1 -- so the
// regression shape is one careless `throw new Error(...)` added to a refusing
// function by an author who never thought about exit codes at all, and it
// announces itself with a green CI (every consumer of this gate treats any
// non-zero as failure, so 1-instead-of-3 is invisible to all of them) and a
// human-readable message that still says the right thing. Nothing else in
// this file would notice.
//
// So the pin is over the FUNCTION BODIES, not over a constant. Reading the
// real `Function.prototype.toString()` of the four functions that refuse is
// what makes a re-added `throw` fail here rather than in six weeks, on a card
// about something else.
const REFUSING = [refreshBuiltClosure, tscErrorCount, measureLedgers, gitIgnoredPaths];
for (const fn of REFUSING) {
const body = fn.toString();
if (/throw new Error\(/.test(body)) {
failures.push(
`${fn.name} raises a bare \`throw new Error(\` — an uncaught throw exits 1, the code this gate ` +
`reserves for a ledger entry that drifted UPWARD. A refusal must go through ` +
`refusePrerequisite() so it exits ${EXIT_PREREQUISITE_NOT_MET}.`,
);
}
if (!body.includes('refusePrerequisite(')) {
failures.push(`${fn.name} no longer refuses through refusePrerequisite() — the exit-code class is unpinned`);
}
}
// The NEGATIVE control, and the reason the loop above is a measurement rather
// than a tautology over an empty set: `readTsconfig` still throws, on purpose.
// A `tsconfig.json` checked into the tree that does not parse is a fact about
// the TREE -- a finding -- not a prerequisite the caller forgot to supply, so
// it keeps exit 1 and the pin above must be able to SEE a bare throw.
if (!/throw new Error\(/.test(readTsconfig.toString())) {
failures.push(
'readTsconfig no longer throws — the bare-throw pin above can no longer fail, so it stopped measuring',
);
}

const exitCodeCases = [
{ label: 'the refusal code is 3', ok: EXIT_PREREQUISITE_NOT_MET === 3 },
{ label: 'a finding is 1', ok: EXIT_FINDINGS === 1 },
{ label: 'the refusal code is distinct from a finding and from a pass',
ok: EXIT_PREREQUISITE_NOT_MET !== EXIT_FINDINGS && EXIT_PREREQUISITE_NOT_MET !== EXIT_OK },
];
for (const c of exitCodeCases) {
if (!c.ok) failures.push(`exit-code contract — ${c.label}`);
}

// The refusal TEXT. Four load-bearing clauses, each one a thing a reader who
// sees only the exit code cannot get anywhere else.
const refusalFixture = '--re-measure cannot run: 48 workspace dependenc(ies) have no built type entry point';
const refusalText = prerequisiteNotMetText(refusalFixture);
const textCases = [
{ label: 'carries the raising site\'s own message VERBATIM', ok: refusalText.includes(refusalFixture) },
{ label: 'names the class', ok: refusalText.includes('PREREQUISITE NOT MET') },
{ label: 'says it is neither a pass nor a finding', ok: /NOT a pass and NOT a finding/.test(refusalText) },
{ label: 'names its own code and the finding code it is distinct from',
ok: refusalText.includes(`Exit code ${EXIT_PREREQUISITE_NOT_MET}`)
&& refusalText.includes(`a finding's ${EXIT_FINDINGS}`) },
// The specific misreading this whole change exists to stop: taking an
// unmeasurable run for "a recorded debt went up" routes the reader to the
// ledger, and raising an entry there is a maintainer's act.
{ label: 'turns the reader away from the ledger rather than toward it',
ok: /no ledger entry below may\n?\s*be raised on it/.test(refusalText) },
{ label: 'warns that the code must be captured before any pipe', ok: refusalText.includes('BEFORE any pipe') },
];
for (const c of textCases) {
if (!c.ok) failures.push(`prerequisiteNotMetText — ${c.label}`);
}

// The shared workspace enumerator is a plain module with no CI invocation of
// its own (#11510); every gate that consolidated onto it folds in its checks.
failures.push(...workspaceEnumeratorSelfTest({ root: ROOT }));
Expand All@@ -5396,7 +5583,8 @@ function selfTest() {
+ ceilingCases.length + heapEnvCases.length} re-measure case(s) + ` +
`${typeEntryCases.length + closureCases.length + staleCases.length + sourceFileCases.length} ` +
`built-closure case(s) + ` +
`${planCases.length + rewriteCases.length + roundTripCases.length} auto-lowering case(s) hold.`,
`${planCases.length + rewriteCases.length + roundTripCases.length} auto-lowering case(s) + ` +
`${REFUSING.length * 2 + 1 + exitCodeCases.length + textCases.length} exit-code case(s) hold.`,
);
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 201 additions & 13 deletions scripts/check-type-check-coverage.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,40 @@
// # -- see BUILT CLOSURE.
// node scripts/check-type-check-coverage.mjs --self-test
//
// ## Exit codes -- and why a REFUSAL has one of its own
//
// 0 the tree was read and every invariant above holds.
// 1 a FINDING: a structural problem, or -- under `--re-measure` -- a ledger
// entry that drifted UPWARD. A claim about the tree.
// 3 PREREQUISITE NOT MET. The gate refused to measure, so nothing was
// measured and the run says NOTHING about the ledger. NOT a pass, and
// NOT a finding.
//
// The split is the sibling convention, not a local invention:
// `check-test-completeness.mjs` states it in its own failure text ("Exit code
// 3, distinct from a finding's 1") and `check-dual-build-cjs-loads.mjs`
// answers the IDENTICAL condition -- a gate that reads built output, run
// against a tree with no `dist/` -- with 3. This gate used to answer it by
// letting the refusal reach node's uncaught handler, which exits 1.
//
// ⭐ Why the class matters HERE in particular, more than it does for a gate
// whose 1 means "something is wrong somewhere". Exit 1 from this gate has one
// specific meaning: a package's recorded debt went UP. The remedy that meaning
// prescribes ends at the ledger below, and raising a DEBT/TEST_DEBT entry is a
// MAINTAINER-only act this file spells out at length. So a reader who takes an
// unmeasurable run for that red is pointed straight at the one place this
// evidence must never send them. The prose said so all along -- but the prose
// is not what an exit-code reader reads. A CI step, a wrapper, or an agent
// reconciling a derived gate family sees the number and nothing else.
//
// ⛔ The boundary, deliberately. Exit 3 is for a prerequisite the WORLD failed
// to supply and that the caller clears with a named command: an unbuilt or
// stale dependency closure, a closure that does not build, an absent `turbo`
// or `tsc` binary, a tsc that could not be spawned or could not read the
// project it was handed. A malformed `tsconfig.json` checked INTO the tree
// stays exit 1 -- that is a fact about the tree, which is what a finding is,
// and `readTsconfig` keeps throwing it.
//
// Invariants, per workspace package (the root workspace package included --
// #4311's audit counted its top-level TypeScript like any other package's):
//
Expand DownExpand Up@@ -468,6 +502,74 @@ import {
const ROOT = resolve(import.meta.dirname, '..');
const SELF = 'scripts/check-type-check-coverage.mjs';
const TRACKING_ISSUE = 'https://github.com/objectstack-ai/objectstack/issues/4311';

// The exit-code contract, NAMED rather than spelled inline at each site, so the
// self-test pins the value each path actually returns instead of a comment
// about it -- the shape `check-test-completeness.mjs` uses for the same split.
//
// ⛔ Module-local, NOT exported, and that is a decision rather than an
// oversight: this file's top level RUNS (it is a gate, invoked as a script and
// nothing else), so exporting any binding at all would make it importable for
// that binding and run the whole gate inside the importer -- the class
// `check:entry-guard` refuses, and it caught this constant block on its first
// run. The sibling that does export its codes carries `isEntrypoint` for
// exactly this reason. Nothing imports this file today; the day something
// needs to, the guard comes with the export.
const EXIT_OK = 0;
const EXIT_FINDINGS = 1;
const EXIT_PREREQUISITE_NOT_MET = 3;

/**
* The text a refusal prints, as a VALUE -- so the self-test can assert on the
* advisory without spawning a process or stubbing `process.exit`, the split
* `import-prerequisite.mjs` documents for the same reason.
*
* The gate's own refusal message is embedded VERBATIM. Every one of them was
* already argued at length at its throw site, and none of that reasoning is
* this frame's to restate, shorten or improve -- the frame adds only the two
* things a reader could not get from the message: what CLASS of result this is,
* and which exit code carries it.
*
* The pipe advisory is not decoration. `EXIT=$?` written after `cmd | tail -40`
* reads TAIL's status, and `head`/`tail` essentially never fail -- so a refusal
* and a green run are the same `0` there, which is the one reading this whole
* exit-code split exists to make impossible.
*
* @param {string} message the refusal, in the words of the site that raised it
* @returns {string}
*/
function prerequisiteNotMetText(message) {
return (
`\ncheck-type-check-coverage: PREREQUISITE NOT MET\n\n` +
`${message}\n\n` +
` ⛔ This is NOT a pass and NOT a finding: nothing was measured, so this run says\n` +
` NOTHING about whether any DEBT or TEST_DEBT number is still correct. In particular\n` +
` it is NOT evidence that a recorded number went up, and ⛔ no ledger entry below may\n` +
` be raised on it -- raising one is a maintainer's act even when the evidence is real.\n` +
` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n` +
` \`node ${SELF} --re-measure > /tmp/type-check-debt.log 2>&1; echo "EXIT=$?"\`.\n` +
` Piped, \`$?\` is the LAST command's status, and \`head\`/\`tail\` essentially never fail — that\n` +
` is the false green. \`\${PIPESTATUS[0]}\`/\`pipefail\` do recover this gate's own code.)`
);
}

/**
* Refuse to measure, and say so in the exit code as well as in the prose.
*
* ⛔ Prints and EXITS rather than throwing. A thrown refusal reaches node's
* uncaught handler, which exits 1 -- the code this gate reserves for "a
* recorded debt number went UP" -- and prints a stack trace over a message
* whose whole job is to be read. The self-test pins that the three functions
* that refuse contain no bare `throw new Error(` for exactly this reason: the
* rot this repairs is one careless `throw` away from returning.
*
* @param {string} message the refusal, in the words of the site that raised it
* @returns {never}
*/
function refusePrerequisite(message) {
console.error(prerequisiteNotMetText(message));
process.exit(EXIT_PREREQUISITE_NOT_MET);
}
// An `exclude` pattern that names tests (`**/*.test.ts`, `**/*.spec.tsx`, ...)
// and the files such a pattern hides. Kept deliberately broad: the question is
// "does this config steer tsc away from the test layer", not "which exact glob".
Expand DownExpand Up@@ -1532,12 +1634,14 @@ function gitIgnoredPaths(rels) {
maxBuffer: 16 * 1024 * 1024,
});
if (res.error) {
throw new Error(`git check-ignore could not run, so GENERATED_COVERED cannot be judged`, { cause: res.error });
refusePrerequisite(
`git check-ignore could not run, so GENERATED_COVERED cannot be judged: ${res.error.message}`,
);
}
// 0 = some path is ignored, 1 = none is. Anything else (128: not a git
// checkout, bad option) is a failed MEASUREMENT, not a clean tree.
if (res.status !== 0 && res.status !== 1) {
throw new Error(
refusePrerequisite(
`git check-ignore exited ${res.status}, so GENERATED_COVERED cannot be judged: ${String(res.stderr).trim()}`,
);
}
Expand DownExpand Up@@ -2701,14 +2805,14 @@ function workspaceBuildGraph(packages) {
function refreshBuiltClosure() {
const bin = join(ROOT, 'node_modules', '.bin', 'turbo');
if (!existsSync(bin)) {
throw new Error(`--re-measure needs the workspace's own turbo at ${bin}; run \`pnpm install\` first.`);
refusePrerequisite(`--re-measure needs the workspace's own turbo at ${bin}; run \`pnpm install\` first.`);
}
const args = ['run', 'build', '--filter=./packages/*', '--filter=./packages/*/*'];
const run = spawnSync(bin, args, { cwd: ROOT, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
if (run.error) throw new Error(`the closure build could not be run: ${run.error.message}`);
if (run.error) refusePrerequisite(`the closure build could not be run: ${run.error.message}`);
if (run.status !== 0) {
const output = `${run.stdout ?? ''}${run.stderr ?? ''}`.trim();
throw new Error(
refusePrerequisite(
`--re-measure cannot run: the ledgered packages' dependency closure does not build, so there is no `
+ `world to measure against. Fix the build first -- every number in DEBT and TEST_DEBT is measured `
+ `with tsc resolving workspace imports through each dependency's built \`dist/*.d.ts\`.\n`
Expand DownExpand Up@@ -2929,7 +3033,7 @@ const REMEASURE_HEAP = remeasureHeapCeiling({
function tscErrorCount(project, options = {}) {
const bin = join(ROOT, 'node_modules', '.bin', 'tsc');
if (!existsSync(bin)) {
throw new Error(`--re-measure needs the workspace's own tsc at ${bin}; run \`pnpm install\` first.`);
refusePrerequisite(`--re-measure needs the workspace's own tsc at ${bin}; run \`pnpm install\` first.`);
}
const run = spawnSync(bin, ['--noEmit', '--pretty', 'false', '-p', project], {
cwd: ROOT,
Expand All@@ -2941,10 +3045,10 @@ function tscErrorCount(project, options = {}) {
// dressed as a different one.
env: heapCappedEnv(process.env, REMEASURE_HEAP.mb),
});
if (run.error) throw new Error(`tsc could not be run for ${project}: ${run.error.message}`);
if (run.error) refusePrerequisite(`tsc could not be run for ${project}: ${run.error.message}`);
const output = `${run.stdout ?? ''}${run.stderr ?? ''}`;
if (TSC_SETUP_ERROR.test(output)) {
throw new Error(`tsc could not read ${project} -- the measurement is invalid, not zero:\n${output.trim()}`);
refusePrerequisite(`tsc could not read ${project} -- the measurement is invalid, not zero:\n${output.trim()}`);
}
const errors = countTscErrors(output, options);
// Exit 0 means a clean program; anything else must have produced diagnostics
Expand All@@ -2956,7 +3060,7 @@ function tscErrorCount(project, options = {}) {
// the `return 0` below -- refusing there would turn "this test layer is clean
// apart from an artefact of our own generated config" into a hard crash.
if (run.status !== 0 && countTscErrors(output) === 0) {
throw new Error(
refusePrerequisite(
`tsc exited ${run.status} for ${project} but printed no recognisable diagnostics -- ` +
`refusing to record 0:\n${output.trim().slice(0, 2000)}`,
);
Expand DownExpand Up@@ -3199,10 +3303,20 @@ function measureTestDebt(dir, hiddenTests = []) {
const holder = mkdtempSync(join(tmpdir(), 'objectstack-debt-remeasure-'));
const configPath = join(holder, REMEASURE_CONFIG);
writeFileSync(configPath, `${JSON.stringify(project, null, 2)}\n`);
// Registered on `exit` as WELL as in the `finally`, because `tscErrorCount`
// can now REFUSE, and a refusal calls `process.exit` -- which runs `exit`
// handlers and does NOT run `finally`. Belt and braces on purpose: the
// `finally` keeps the directory's lifetime visible where it is created, and
// the handler is what makes the refusal path leave nothing behind. `off`
// first in the `finally` so a run measuring 34 entries does not accumulate 34
// live handlers on a directory each has already removed.
const cleanup = () => rmSync(holder, { force: true, recursive: true });
process.once('exit', cleanup);
try {
return tscErrorCount(configPath, { dropRootDirDiagnostics: true });
} finally {
rmSync(holder, { force: true, recursive: true });
process.off('exit', cleanup);
cleanup();
}
}

Expand All@@ -3228,7 +3342,7 @@ function measureLedgers(packages, rootName, state) {
.filter((name) => dirOf.has(name));
const unbuilt = unbuiltClosure(ledgered, workspaceBuildGraph(packages));
if (unbuilt.length > 0) {
throw new Error(
refusePrerequisite(
`--re-measure cannot run: ${unbuilt.length} workspace dependenc(ies) of the ledgered packages have `
+ `no built type entry point on disk -- ${unbuilt.join(', ')}.\n`
+ `Every number in DEBT and TEST_DEBT is measured with tsc resolving workspace imports through each `
Expand DownExpand Up@@ -3262,7 +3376,7 @@ function measureLedgers(packages, rootName, state) {
// caller can act on.
const stale = staleClosure(ledgered, workspaceBuildGraph(packages));
if (stale.length > 0) {
throw new Error(
refusePrerequisite(
`--re-measure cannot run: ${stale.length} workspace dependenc(ies) of the ledgered packages still have `
+ `a type entry point OLDER than their own sources after a full closure build -- ${stale.join(', ')}.\n`
+ `The build covers \`./packages/*\` and \`./packages/*/*\`, so a package that survives it is one those `
Expand DownExpand Up@@ -5378,6 +5492,79 @@ function selfTest() {
}
}

// ── THE EXIT-CODE CLASS ───────────────────────────────────────────────────
//
// Pinned because it is exactly the kind of fact that rots back silently. The
// defect this replaces was not a wrong number typed anywhere: it was a
// refusal that reached node's UNCAUGHT handler, which exits 1 -- so the
// regression shape is one careless `throw new Error(...)` added to a refusing
// function by an author who never thought about exit codes at all, and it
// announces itself with a green CI (every consumer of this gate treats any
// non-zero as failure, so 1-instead-of-3 is invisible to all of them) and a
// human-readable message that still says the right thing. Nothing else in
// this file would notice.
//
// So the pin is over the FUNCTION BODIES, not over a constant. Reading the
// real `Function.prototype.toString()` of the four functions that refuse is
// what makes a re-added `throw` fail here rather than in six weeks, on a card
// about something else.
const REFUSING = [refreshBuiltClosure, tscErrorCount, measureLedgers, gitIgnoredPaths];
for (const fn of REFUSING) {
const body = fn.toString();
if (/throw new Error\(/.test(body)) {
failures.push(
`${fn.name} raises a bare \`throw new Error(\` — an uncaught throw exits 1, the code this gate ` +
`reserves for a ledger entry that drifted UPWARD. A refusal must go through ` +
`refusePrerequisite() so it exits ${EXIT_PREREQUISITE_NOT_MET}.`,
);
}
if (!body.includes('refusePrerequisite(')) {
failures.push(`${fn.name} no longer refuses through refusePrerequisite() — the exit-code class is unpinned`);
}
}
// The NEGATIVE control, and the reason the loop above is a measurement rather
// than a tautology over an empty set: `readTsconfig` still throws, on purpose.
// A `tsconfig.json` checked into the tree that does not parse is a fact about
// the TREE -- a finding -- not a prerequisite the caller forgot to supply, so
// it keeps exit 1 and the pin above must be able to SEE a bare throw.
if (!/throw new Error\(/.test(readTsconfig.toString())) {
failures.push(
'readTsconfig no longer throws — the bare-throw pin above can no longer fail, so it stopped measuring',
);
}

const exitCodeCases = [
{ label: 'the refusal code is 3', ok: EXIT_PREREQUISITE_NOT_MET === 3 },
{ label: 'a finding is 1', ok: EXIT_FINDINGS === 1 },
{ label: 'the refusal code is distinct from a finding and from a pass',
ok: EXIT_PREREQUISITE_NOT_MET !== EXIT_FINDINGS && EXIT_PREREQUISITE_NOT_MET !== EXIT_OK },
];
for (const c of exitCodeCases) {
if (!c.ok) failures.push(`exit-code contract — ${c.label}`);
}

// The refusal TEXT. Four load-bearing clauses, each one a thing a reader who
// sees only the exit code cannot get anywhere else.
const refusalFixture = '--re-measure cannot run: 48 workspace dependenc(ies) have no built type entry point';
const refusalText = prerequisiteNotMetText(refusalFixture);
const textCases = [
{ label: 'carries the raising site\'s own message VERBATIM', ok: refusalText.includes(refusalFixture) },
{ label: 'names the class', ok: refusalText.includes('PREREQUISITE NOT MET') },
{ label: 'says it is neither a pass nor a finding', ok: /NOT a pass and NOT a finding/.test(refusalText) },
{ label: 'names its own code and the finding code it is distinct from',
ok: refusalText.includes(`Exit code ${EXIT_PREREQUISITE_NOT_MET}`)
&& refusalText.includes(`a finding's ${EXIT_FINDINGS}`) },
// The specific misreading this whole change exists to stop: taking an
// unmeasurable run for "a recorded debt went up" routes the reader to the
// ledger, and raising an entry there is a maintainer's act.
{ label: 'turns the reader away from the ledger rather than toward it',
ok: /no ledger entry below may\n?\s*be raised on it/.test(refusalText) },
{ label: 'warns that the code must be captured before any pipe', ok: refusalText.includes('BEFORE any pipe') },
];
for (const c of textCases) {
if (!c.ok) failures.push(`prerequisiteNotMetText — ${c.label}`);
}

// The shared workspace enumerator is a plain module with no CI invocation of
// its own (#11510); every gate that consolidated onto it folds in its checks.
failures.push(...workspaceEnumeratorSelfTest({ root: ROOT }));
Expand All@@ -5396,7 +5583,8 @@ function selfTest() {
+ ceilingCases.length + heapEnvCases.length} re-measure case(s) + ` +
`${typeEntryCases.length + closureCases.length + staleCases.length + sourceFileCases.length} ` +
`built-closure case(s) + ` +
`${planCases.length + rewriteCases.length + roundTripCases.length} auto-lowering case(s) hold.`,
`${planCases.length + rewriteCases.length + roundTripCases.length} auto-lowering case(s) + ` +
`${REFUSING.length * 2 + 1 + exitCodeCases.length + textCases.length} exit-code case(s) hold.`,
);
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 201 additions & 13 deletions scripts/check-type-check-coverage.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,40 @@
// # -- see BUILT CLOSURE.
// node scripts/check-type-check-coverage.mjs --self-test
//
// ## Exit codes -- and why a REFUSAL has one of its own
//
// 0 the tree was read and every invariant above holds.
// 1 a FINDING: a structural problem, or -- under `--re-measure` -- a ledger
// entry that drifted UPWARD. A claim about the tree.
// 3 PREREQUISITE NOT MET. The gate refused to measure, so nothing was
// measured and the run says NOTHING about the ledger. NOT a pass, and
// NOT a finding.
//
// The split is the sibling convention, not a local invention:
// `check-test-completeness.mjs` states it in its own failure text ("Exit code
// 3, distinct from a finding's 1") and `check-dual-build-cjs-loads.mjs`
// answers the IDENTICAL condition -- a gate that reads built output, run
// against a tree with no `dist/` -- with 3. This gate used to answer it by
// letting the refusal reach node's uncaught handler, which exits 1.
//
// ⭐ Why the class matters HERE in particular, more than it does for a gate
// whose 1 means "something is wrong somewhere". Exit 1 from this gate has one
// specific meaning: a package's recorded debt went UP. The remedy that meaning
// prescribes ends at the ledger below, and raising a DEBT/TEST_DEBT entry is a
// MAINTAINER-only act this file spells out at length. So a reader who takes an
// unmeasurable run for that red is pointed straight at the one place this
// evidence must never send them. The prose said so all along -- but the prose
// is not what an exit-code reader reads. A CI step, a wrapper, or an agent
// reconciling a derived gate family sees the number and nothing else.
//
// ⛔ The boundary, deliberately. Exit 3 is for a prerequisite the WORLD failed
// to supply and that the caller clears with a named command: an unbuilt or
// stale dependency closure, a closure that does not build, an absent `turbo`
// or `tsc` binary, a tsc that could not be spawned or could not read the
// project it was handed. A malformed `tsconfig.json` checked INTO the tree
// stays exit 1 -- that is a fact about the tree, which is what a finding is,
// and `readTsconfig` keeps throwing it.
//
// Invariants, per workspace package (the root workspace package included --
// #4311's audit counted its top-level TypeScript like any other package's):
//
Expand DownExpand Up@@ -468,6 +502,74 @@ import {
const ROOT = resolve(import.meta.dirname, '..');
const SELF = 'scripts/check-type-check-coverage.mjs';
const TRACKING_ISSUE = 'https://github.com/objectstack-ai/objectstack/issues/4311';

// The exit-code contract, NAMED rather than spelled inline at each site, so the
// self-test pins the value each path actually returns instead of a comment
// about it -- the shape `check-test-completeness.mjs` uses for the same split.
//
// ⛔ Module-local, NOT exported, and that is a decision rather than an
// oversight: this file's top level RUNS (it is a gate, invoked as a script and
// nothing else), so exporting any binding at all would make it importable for
// that binding and run the whole gate inside the importer -- the class
// `check:entry-guard` refuses, and it caught this constant block on its first
// run. The sibling that does export its codes carries `isEntrypoint` for
// exactly this reason. Nothing imports this file today; the day something
// needs to, the guard comes with the export.
const EXIT_OK = 0;
const EXIT_FINDINGS = 1;
const EXIT_PREREQUISITE_NOT_MET = 3;

/**
* The text a refusal prints, as a VALUE -- so the self-test can assert on the
* advisory without spawning a process or stubbing `process.exit`, the split
* `import-prerequisite.mjs` documents for the same reason.
*
* The gate's own refusal message is embedded VERBATIM. Every one of them was
* already argued at length at its throw site, and none of that reasoning is
* this frame's to restate, shorten or improve -- the frame adds only the two
* things a reader could not get from the message: what CLASS of result this is,
* and which exit code carries it.
*
* The pipe advisory is not decoration. `EXIT=$?` written after `cmd | tail -40`
* reads TAIL's status, and `head`/`tail` essentially never fail -- so a refusal
* and a green run are the same `0` there, which is the one reading this whole
* exit-code split exists to make impossible.
*
* @param {string} message the refusal, in the words of the site that raised it
* @returns {string}
*/
function prerequisiteNotMetText(message) {
return (
`\ncheck-type-check-coverage: PREREQUISITE NOT MET\n\n` +
`${message}\n\n` +
` ⛔ This is NOT a pass and NOT a finding: nothing was measured, so this run says\n` +
` NOTHING about whether any DEBT or TEST_DEBT number is still correct. In particular\n` +
` it is NOT evidence that a recorded number went up, and ⛔ no ledger entry below may\n` +
` be raised on it -- raising one is a maintainer's act even when the evidence is real.\n` +
` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n` +
` \`node ${SELF} --re-measure > /tmp/type-check-debt.log 2>&1; echo "EXIT=$?"\`.\n` +
` Piped, \`$?\` is the LAST command's status, and \`head\`/\`tail\` essentially never fail — that\n` +
` is the false green. \`\${PIPESTATUS[0]}\`/\`pipefail\` do recover this gate's own code.)`
);
}

/**
* Refuse to measure, and say so in the exit code as well as in the prose.
*
* ⛔ Prints and EXITS rather than throwing. A thrown refusal reaches node's
* uncaught handler, which exits 1 -- the code this gate reserves for "a
* recorded debt number went UP" -- and prints a stack trace over a message
* whose whole job is to be read. The self-test pins that the three functions
* that refuse contain no bare `throw new Error(` for exactly this reason: the
* rot this repairs is one careless `throw` away from returning.
*
* @param {string} message the refusal, in the words of the site that raised it
* @returns {never}
*/
function refusePrerequisite(message) {
console.error(prerequisiteNotMetText(message));
process.exit(EXIT_PREREQUISITE_NOT_MET);
}
// An `exclude` pattern that names tests (`**/*.test.ts`, `**/*.spec.tsx`, ...)
// and the files such a pattern hides. Kept deliberately broad: the question is
// "does this config steer tsc away from the test layer", not "which exact glob".
Expand DownExpand Up@@ -1532,12 +1634,14 @@ function gitIgnoredPaths(rels) {
maxBuffer: 16 * 1024 * 1024,
});
if (res.error) {
throw new Error(`git check-ignore could not run, so GENERATED_COVERED cannot be judged`, { cause: res.error });
refusePrerequisite(
`git check-ignore could not run, so GENERATED_COVERED cannot be judged: ${res.error.message}`,
);
}
// 0 = some path is ignored, 1 = none is. Anything else (128: not a git
// checkout, bad option) is a failed MEASUREMENT, not a clean tree.
if (res.status !== 0 && res.status !== 1) {
throw new Error(
refusePrerequisite(
`git check-ignore exited ${res.status}, so GENERATED_COVERED cannot be judged: ${String(res.stderr).trim()}`,
);
}
Expand DownExpand Up@@ -2701,14 +2805,14 @@ function workspaceBuildGraph(packages) {
function refreshBuiltClosure() {
const bin = join(ROOT, 'node_modules', '.bin', 'turbo');
if (!existsSync(bin)) {
throw new Error(`--re-measure needs the workspace's own turbo at ${bin}; run \`pnpm install\` first.`);
refusePrerequisite(`--re-measure needs the workspace's own turbo at ${bin}; run \`pnpm install\` first.`);
}
const args = ['run', 'build', '--filter=./packages/*', '--filter=./packages/*/*'];
const run = spawnSync(bin, args, { cwd: ROOT, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
if (run.error) throw new Error(`the closure build could not be run: ${run.error.message}`);
if (run.error) refusePrerequisite(`the closure build could not be run: ${run.error.message}`);
if (run.status !== 0) {
const output = `${run.stdout ?? ''}${run.stderr ?? ''}`.trim();
throw new Error(
refusePrerequisite(
`--re-measure cannot run: the ledgered packages' dependency closure does not build, so there is no `
+ `world to measure against. Fix the build first -- every number in DEBT and TEST_DEBT is measured `
+ `with tsc resolving workspace imports through each dependency's built \`dist/*.d.ts\`.\n`
Expand DownExpand Up@@ -2929,7 +3033,7 @@ const REMEASURE_HEAP = remeasureHeapCeiling({
function tscErrorCount(project, options = {}) {
const bin = join(ROOT, 'node_modules', '.bin', 'tsc');
if (!existsSync(bin)) {
throw new Error(`--re-measure needs the workspace's own tsc at ${bin}; run \`pnpm install\` first.`);
refusePrerequisite(`--re-measure needs the workspace's own tsc at ${bin}; run \`pnpm install\` first.`);
}
const run = spawnSync(bin, ['--noEmit', '--pretty', 'false', '-p', project], {
cwd: ROOT,
Expand All@@ -2941,10 +3045,10 @@ function tscErrorCount(project, options = {}) {
// dressed as a different one.
env: heapCappedEnv(process.env, REMEASURE_HEAP.mb),
});
if (run.error) throw new Error(`tsc could not be run for ${project}: ${run.error.message}`);
if (run.error) refusePrerequisite(`tsc could not be run for ${project}: ${run.error.message}`);
const output = `${run.stdout ?? ''}${run.stderr ?? ''}`;
if (TSC_SETUP_ERROR.test(output)) {
throw new Error(`tsc could not read ${project} -- the measurement is invalid, not zero:\n${output.trim()}`);
refusePrerequisite(`tsc could not read ${project} -- the measurement is invalid, not zero:\n${output.trim()}`);
}
const errors = countTscErrors(output, options);
// Exit 0 means a clean program; anything else must have produced diagnostics
Expand All@@ -2956,7 +3060,7 @@ function tscErrorCount(project, options = {}) {
// the `return 0` below -- refusing there would turn "this test layer is clean
// apart from an artefact of our own generated config" into a hard crash.
if (run.status !== 0 && countTscErrors(output) === 0) {
throw new Error(
refusePrerequisite(
`tsc exited ${run.status} for ${project} but printed no recognisable diagnostics -- ` +
`refusing to record 0:\n${output.trim().slice(0, 2000)}`,
);
Expand DownExpand Up@@ -3199,10 +3303,20 @@ function measureTestDebt(dir, hiddenTests = []) {
const holder = mkdtempSync(join(tmpdir(), 'objectstack-debt-remeasure-'));
const configPath = join(holder, REMEASURE_CONFIG);
writeFileSync(configPath, `${JSON.stringify(project, null, 2)}\n`);
// Registered on `exit` as WELL as in the `finally`, because `tscErrorCount`
// can now REFUSE, and a refusal calls `process.exit` -- which runs `exit`
// handlers and does NOT run `finally`. Belt and braces on purpose: the
// `finally` keeps the directory's lifetime visible where it is created, and
// the handler is what makes the refusal path leave nothing behind. `off`
// first in the `finally` so a run measuring 34 entries does not accumulate 34
// live handlers on a directory each has already removed.
const cleanup = () => rmSync(holder, { force: true, recursive: true });
process.once('exit', cleanup);
try {
return tscErrorCount(configPath, { dropRootDirDiagnostics: true });
} finally {
rmSync(holder, { force: true, recursive: true });
process.off('exit', cleanup);
cleanup();
}
}

Expand All@@ -3228,7 +3342,7 @@ function measureLedgers(packages, rootName, state) {
.filter((name) => dirOf.has(name));
const unbuilt = unbuiltClosure(ledgered, workspaceBuildGraph(packages));
if (unbuilt.length > 0) {
throw new Error(
refusePrerequisite(
`--re-measure cannot run: ${unbuilt.length} workspace dependenc(ies) of the ledgered packages have `
+ `no built type entry point on disk -- ${unbuilt.join(', ')}.\n`
+ `Every number in DEBT and TEST_DEBT is measured with tsc resolving workspace imports through each `
Expand DownExpand Up@@ -3262,7 +3376,7 @@ function measureLedgers(packages, rootName, state) {
// caller can act on.
const stale = staleClosure(ledgered, workspaceBuildGraph(packages));
if (stale.length > 0) {
throw new Error(
refusePrerequisite(
`--re-measure cannot run: ${stale.length} workspace dependenc(ies) of the ledgered packages still have `
+ `a type entry point OLDER than their own sources after a full closure build -- ${stale.join(', ')}.\n`
+ `The build covers \`./packages/*\` and \`./packages/*/*\`, so a package that survives it is one those `
Expand DownExpand Up@@ -5378,6 +5492,79 @@ function selfTest() {
}
}

// ── THE EXIT-CODE CLASS ───────────────────────────────────────────────────
//
// Pinned because it is exactly the kind of fact that rots back silently. The
// defect this replaces was not a wrong number typed anywhere: it was a
// refusal that reached node's UNCAUGHT handler, which exits 1 -- so the
// regression shape is one careless `throw new Error(...)` added to a refusing
// function by an author who never thought about exit codes at all, and it
// announces itself with a green CI (every consumer of this gate treats any
// non-zero as failure, so 1-instead-of-3 is invisible to all of them) and a
// human-readable message that still says the right thing. Nothing else in
// this file would notice.
//
// So the pin is over the FUNCTION BODIES, not over a constant. Reading the
// real `Function.prototype.toString()` of the four functions that refuse is
// what makes a re-added `throw` fail here rather than in six weeks, on a card
// about something else.
const REFUSING = [refreshBuiltClosure, tscErrorCount, measureLedgers, gitIgnoredPaths];
for (const fn of REFUSING) {
const body = fn.toString();
if (/throw new Error\(/.test(body)) {
failures.push(
`${fn.name} raises a bare \`throw new Error(\` — an uncaught throw exits 1, the code this gate ` +
`reserves for a ledger entry that drifted UPWARD. A refusal must go through ` +
`refusePrerequisite() so it exits ${EXIT_PREREQUISITE_NOT_MET}.`,
);
}
if (!body.includes('refusePrerequisite(')) {
failures.push(`${fn.name} no longer refuses through refusePrerequisite() — the exit-code class is unpinned`);
}
}
// The NEGATIVE control, and the reason the loop above is a measurement rather
// than a tautology over an empty set: `readTsconfig` still throws, on purpose.
// A `tsconfig.json` checked into the tree that does not parse is a fact about
// the TREE -- a finding -- not a prerequisite the caller forgot to supply, so
// it keeps exit 1 and the pin above must be able to SEE a bare throw.
if (!/throw new Error\(/.test(readTsconfig.toString())) {
failures.push(
'readTsconfig no longer throws — the bare-throw pin above can no longer fail, so it stopped measuring',
);
}

const exitCodeCases = [
{ label: 'the refusal code is 3', ok: EXIT_PREREQUISITE_NOT_MET === 3 },
{ label: 'a finding is 1', ok: EXIT_FINDINGS === 1 },
{ label: 'the refusal code is distinct from a finding and from a pass',
ok: EXIT_PREREQUISITE_NOT_MET !== EXIT_FINDINGS && EXIT_PREREQUISITE_NOT_MET !== EXIT_OK },
];
for (const c of exitCodeCases) {
if (!c.ok) failures.push(`exit-code contract — ${c.label}`);
}

// The refusal TEXT. Four load-bearing clauses, each one a thing a reader who
// sees only the exit code cannot get anywhere else.
const refusalFixture = '--re-measure cannot run: 48 workspace dependenc(ies) have no built type entry point';
const refusalText = prerequisiteNotMetText(refusalFixture);
const textCases = [
{ label: 'carries the raising site\'s own message VERBATIM', ok: refusalText.includes(refusalFixture) },
{ label: 'names the class', ok: refusalText.includes('PREREQUISITE NOT MET') },
{ label: 'says it is neither a pass nor a finding', ok: /NOT a pass and NOT a finding/.test(refusalText) },
{ label: 'names its own code and the finding code it is distinct from',
ok: refusalText.includes(`Exit code ${EXIT_PREREQUISITE_NOT_MET}`)
&& refusalText.includes(`a finding's ${EXIT_FINDINGS}`) },
// The specific misreading this whole change exists to stop: taking an
// unmeasurable run for "a recorded debt went up" routes the reader to the
// ledger, and raising an entry there is a maintainer's act.
{ label: 'turns the reader away from the ledger rather than toward it',
ok: /no ledger entry below may\n?\s*be raised on it/.test(refusalText) },
{ label: 'warns that the code must be captured before any pipe', ok: refusalText.includes('BEFORE any pipe') },
];
for (const c of textCases) {
if (!c.ok) failures.push(`prerequisiteNotMetText — ${c.label}`);
}

// The shared workspace enumerator is a plain module with no CI invocation of
// its own (#11510); every gate that consolidated onto it folds in its checks.
failures.push(...workspaceEnumeratorSelfTest({ root: ROOT }));
Expand All@@ -5396,7 +5583,8 @@ function selfTest() {
+ ceilingCases.length + heapEnvCases.length} re-measure case(s) + ` +
`${typeEntryCases.length + closureCases.length + staleCases.length + sourceFileCases.length} ` +
`built-closure case(s) + ` +
`${planCases.length + rewriteCases.length + roundTripCases.length} auto-lowering case(s) hold.`,
`${planCases.length + rewriteCases.length + roundTripCases.length} auto-lowering case(s) + ` +
`${REFUSING.length * 2 + 1 + exitCodeCases.length + textCases.length} exit-code case(s) hold.`,
);
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 201 additions & 13 deletions scripts/check-type-check-coverage.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,40 @@
// # -- see BUILT CLOSURE.
// node scripts/check-type-check-coverage.mjs --self-test
//
// ## Exit codes -- and why a REFUSAL has one of its own
//
// 0 the tree was read and every invariant above holds.
// 1 a FINDING: a structural problem, or -- under `--re-measure` -- a ledger
// entry that drifted UPWARD. A claim about the tree.
// 3 PREREQUISITE NOT MET. The gate refused to measure, so nothing was
// measured and the run says NOTHING about the ledger. NOT a pass, and
// NOT a finding.
//
// The split is the sibling convention, not a local invention:
// `check-test-completeness.mjs` states it in its own failure text ("Exit code
// 3, distinct from a finding's 1") and `check-dual-build-cjs-loads.mjs`
// answers the IDENTICAL condition -- a gate that reads built output, run
// against a tree with no `dist/` -- with 3. This gate used to answer it by
// letting the refusal reach node's uncaught handler, which exits 1.
//
// ⭐ Why the class matters HERE in particular, more than it does for a gate
// whose 1 means "something is wrong somewhere". Exit 1 from this gate has one
// specific meaning: a package's recorded debt went UP. The remedy that meaning
// prescribes ends at the ledger below, and raising a DEBT/TEST_DEBT entry is a
// MAINTAINER-only act this file spells out at length. So a reader who takes an
// unmeasurable run for that red is pointed straight at the one place this
// evidence must never send them. The prose said so all along -- but the prose
// is not what an exit-code reader reads. A CI step, a wrapper, or an agent
// reconciling a derived gate family sees the number and nothing else.
//
// ⛔ The boundary, deliberately. Exit 3 is for a prerequisite the WORLD failed
// to supply and that the caller clears with a named command: an unbuilt or
// stale dependency closure, a closure that does not build, an absent `turbo`
// or `tsc` binary, a tsc that could not be spawned or could not read the
// project it was handed. A malformed `tsconfig.json` checked INTO the tree
// stays exit 1 -- that is a fact about the tree, which is what a finding is,
// and `readTsconfig` keeps throwing it.
//
// Invariants, per workspace package (the root workspace package included --
// #4311's audit counted its top-level TypeScript like any other package's):
//
Expand DownExpand Up@@ -468,6 +502,74 @@ import {
const ROOT = resolve(import.meta.dirname, '..');
const SELF = 'scripts/check-type-check-coverage.mjs';
const TRACKING_ISSUE = 'https://github.com/objectstack-ai/objectstack/issues/4311';

// The exit-code contract, NAMED rather than spelled inline at each site, so the
// self-test pins the value each path actually returns instead of a comment
// about it -- the shape `check-test-completeness.mjs` uses for the same split.
//
// ⛔ Module-local, NOT exported, and that is a decision rather than an
// oversight: this file's top level RUNS (it is a gate, invoked as a script and
// nothing else), so exporting any binding at all would make it importable for
// that binding and run the whole gate inside the importer -- the class
// `check:entry-guard` refuses, and it caught this constant block on its first
// run. The sibling that does export its codes carries `isEntrypoint` for
// exactly this reason. Nothing imports this file today; the day something
// needs to, the guard comes with the export.
const EXIT_OK = 0;
const EXIT_FINDINGS = 1;
const EXIT_PREREQUISITE_NOT_MET = 3;

/**
* The text a refusal prints, as a VALUE -- so the self-test can assert on the
* advisory without spawning a process or stubbing `process.exit`, the split
* `import-prerequisite.mjs` documents for the same reason.
*
* The gate's own refusal message is embedded VERBATIM. Every one of them was
* already argued at length at its throw site, and none of that reasoning is
* this frame's to restate, shorten or improve -- the frame adds only the two
* things a reader could not get from the message: what CLASS of result this is,
* and which exit code carries it.
*
* The pipe advisory is not decoration. `EXIT=$?` written after `cmd | tail -40`
* reads TAIL's status, and `head`/`tail` essentially never fail -- so a refusal
* and a green run are the same `0` there, which is the one reading this whole
* exit-code split exists to make impossible.
*
* @param {string} message the refusal, in the words of the site that raised it
* @returns {string}
*/
function prerequisiteNotMetText(message) {
return (
`\ncheck-type-check-coverage: PREREQUISITE NOT MET\n\n` +
`${message}\n\n` +
` ⛔ This is NOT a pass and NOT a finding: nothing was measured, so this run says\n` +
` NOTHING about whether any DEBT or TEST_DEBT number is still correct. In particular\n` +
` it is NOT evidence that a recorded number went up, and ⛔ no ledger entry below may\n` +
` be raised on it -- raising one is a maintainer's act even when the evidence is real.\n` +
` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n` +
` \`node ${SELF} --re-measure > /tmp/type-check-debt.log 2>&1; echo "EXIT=$?"\`.\n` +
` Piped, \`$?\` is the LAST command's status, and \`head\`/\`tail\` essentially never fail — that\n` +
` is the false green. \`\${PIPESTATUS[0]}\`/\`pipefail\` do recover this gate's own code.)`
);
}

/**
* Refuse to measure, and say so in the exit code as well as in the prose.
*
* ⛔ Prints and EXITS rather than throwing. A thrown refusal reaches node's
* uncaught handler, which exits 1 -- the code this gate reserves for "a
* recorded debt number went UP" -- and prints a stack trace over a message
* whose whole job is to be read. The self-test pins that the three functions
* that refuse contain no bare `throw new Error(` for exactly this reason: the
* rot this repairs is one careless `throw` away from returning.
*
* @param {string} message the refusal, in the words of the site that raised it
* @returns {never}
*/
function refusePrerequisite(message) {
console.error(prerequisiteNotMetText(message));
process.exit(EXIT_PREREQUISITE_NOT_MET);
}
// An `exclude` pattern that names tests (`**/*.test.ts`, `**/*.spec.tsx`, ...)
// and the files such a pattern hides. Kept deliberately broad: the question is
// "does this config steer tsc away from the test layer", not "which exact glob".
Expand DownExpand Up@@ -1532,12 +1634,14 @@ function gitIgnoredPaths(rels) {
maxBuffer: 16 * 1024 * 1024,
});
if (res.error) {
throw new Error(`git check-ignore could not run, so GENERATED_COVERED cannot be judged`, { cause: res.error });
refusePrerequisite(
`git check-ignore could not run, so GENERATED_COVERED cannot be judged: ${res.error.message}`,
);
}
// 0 = some path is ignored, 1 = none is. Anything else (128: not a git
// checkout, bad option) is a failed MEASUREMENT, not a clean tree.
if (res.status !== 0 && res.status !== 1) {
throw new Error(
refusePrerequisite(
`git check-ignore exited ${res.status}, so GENERATED_COVERED cannot be judged: ${String(res.stderr).trim()}`,
);
}
Expand DownExpand Up@@ -2701,14 +2805,14 @@ function workspaceBuildGraph(packages) {
function refreshBuiltClosure() {
const bin = join(ROOT, 'node_modules', '.bin', 'turbo');
if (!existsSync(bin)) {
throw new Error(`--re-measure needs the workspace's own turbo at ${bin}; run \`pnpm install\` first.`);
refusePrerequisite(`--re-measure needs the workspace's own turbo at ${bin}; run \`pnpm install\` first.`);
}
const args = ['run', 'build', '--filter=./packages/*', '--filter=./packages/*/*'];
const run = spawnSync(bin, args, { cwd: ROOT, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
if (run.error) throw new Error(`the closure build could not be run: ${run.error.message}`);
if (run.error) refusePrerequisite(`the closure build could not be run: ${run.error.message}`);
if (run.status !== 0) {
const output = `${run.stdout ?? ''}${run.stderr ?? ''}`.trim();
throw new Error(
refusePrerequisite(
`--re-measure cannot run: the ledgered packages' dependency closure does not build, so there is no `
+ `world to measure against. Fix the build first -- every number in DEBT and TEST_DEBT is measured `
+ `with tsc resolving workspace imports through each dependency's built \`dist/*.d.ts\`.\n`
Expand DownExpand Up@@ -2929,7 +3033,7 @@ const REMEASURE_HEAP = remeasureHeapCeiling({
function tscErrorCount(project, options = {}) {
const bin = join(ROOT, 'node_modules', '.bin', 'tsc');
if (!existsSync(bin)) {
throw new Error(`--re-measure needs the workspace's own tsc at ${bin}; run \`pnpm install\` first.`);
refusePrerequisite(`--re-measure needs the workspace's own tsc at ${bin}; run \`pnpm install\` first.`);
}
const run = spawnSync(bin, ['--noEmit', '--pretty', 'false', '-p', project], {
cwd: ROOT,
Expand All@@ -2941,10 +3045,10 @@ function tscErrorCount(project, options = {}) {
// dressed as a different one.
env: heapCappedEnv(process.env, REMEASURE_HEAP.mb),
});
if (run.error) throw new Error(`tsc could not be run for ${project}: ${run.error.message}`);
if (run.error) refusePrerequisite(`tsc could not be run for ${project}: ${run.error.message}`);
const output = `${run.stdout ?? ''}${run.stderr ?? ''}`;
if (TSC_SETUP_ERROR.test(output)) {
throw new Error(`tsc could not read ${project} -- the measurement is invalid, not zero:\n${output.trim()}`);
refusePrerequisite(`tsc could not read ${project} -- the measurement is invalid, not zero:\n${output.trim()}`);
}
const errors = countTscErrors(output, options);
// Exit 0 means a clean program; anything else must have produced diagnostics
Expand All@@ -2956,7 +3060,7 @@ function tscErrorCount(project, options = {}) {
// the `return 0` below -- refusing there would turn "this test layer is clean
// apart from an artefact of our own generated config" into a hard crash.
if (run.status !== 0 && countTscErrors(output) === 0) {
throw new Error(
refusePrerequisite(
`tsc exited ${run.status} for ${project} but printed no recognisable diagnostics -- ` +
`refusing to record 0:\n${output.trim().slice(0, 2000)}`,
);
Expand DownExpand Up@@ -3199,10 +3303,20 @@ function measureTestDebt(dir, hiddenTests = []) {
const holder = mkdtempSync(join(tmpdir(), 'objectstack-debt-remeasure-'));
const configPath = join(holder, REMEASURE_CONFIG);
writeFileSync(configPath, `${JSON.stringify(project, null, 2)}\n`);
// Registered on `exit` as WELL as in the `finally`, because `tscErrorCount`
// can now REFUSE, and a refusal calls `process.exit` -- which runs `exit`
// handlers and does NOT run `finally`. Belt and braces on purpose: the
// `finally` keeps the directory's lifetime visible where it is created, and
// the handler is what makes the refusal path leave nothing behind. `off`
// first in the `finally` so a run measuring 34 entries does not accumulate 34
// live handlers on a directory each has already removed.
const cleanup = () => rmSync(holder, { force: true, recursive: true });
process.once('exit', cleanup);
try {
return tscErrorCount(configPath, { dropRootDirDiagnostics: true });
} finally {
rmSync(holder, { force: true, recursive: true });
process.off('exit', cleanup);
cleanup();
}
}

Expand All@@ -3228,7 +3342,7 @@ function measureLedgers(packages, rootName, state) {
.filter((name) => dirOf.has(name));
const unbuilt = unbuiltClosure(ledgered, workspaceBuildGraph(packages));
if (unbuilt.length > 0) {
throw new Error(
refusePrerequisite(
`--re-measure cannot run: ${unbuilt.length} workspace dependenc(ies) of the ledgered packages have `
+ `no built type entry point on disk -- ${unbuilt.join(', ')}.\n`
+ `Every number in DEBT and TEST_DEBT is measured with tsc resolving workspace imports through each `
Expand DownExpand Up@@ -3262,7 +3376,7 @@ function measureLedgers(packages, rootName, state) {
// caller can act on.
const stale = staleClosure(ledgered, workspaceBuildGraph(packages));
if (stale.length > 0) {
throw new Error(
refusePrerequisite(
`--re-measure cannot run: ${stale.length} workspace dependenc(ies) of the ledgered packages still have `
+ `a type entry point OLDER than their own sources after a full closure build -- ${stale.join(', ')}.\n`
+ `The build covers \`./packages/*\` and \`./packages/*/*\`, so a package that survives it is one those `
Expand DownExpand Up@@ -5378,6 +5492,79 @@ function selfTest() {
}
}

// ── THE EXIT-CODE CLASS ───────────────────────────────────────────────────
//
// Pinned because it is exactly the kind of fact that rots back silently. The
// defect this replaces was not a wrong number typed anywhere: it was a
// refusal that reached node's UNCAUGHT handler, which exits 1 -- so the
// regression shape is one careless `throw new Error(...)` added to a refusing
// function by an author who never thought about exit codes at all, and it
// announces itself with a green CI (every consumer of this gate treats any
// non-zero as failure, so 1-instead-of-3 is invisible to all of them) and a
// human-readable message that still says the right thing. Nothing else in
// this file would notice.
//
// So the pin is over the FUNCTION BODIES, not over a constant. Reading the
// real `Function.prototype.toString()` of the four functions that refuse is
// what makes a re-added `throw` fail here rather than in six weeks, on a card
// about something else.
const REFUSING = [refreshBuiltClosure, tscErrorCount, measureLedgers, gitIgnoredPaths];
for (const fn of REFUSING) {
const body = fn.toString();
if (/throw new Error\(/.test(body)) {
failures.push(
`${fn.name} raises a bare \`throw new Error(\` — an uncaught throw exits 1, the code this gate ` +
`reserves for a ledger entry that drifted UPWARD. A refusal must go through ` +
`refusePrerequisite() so it exits ${EXIT_PREREQUISITE_NOT_MET}.`,
);
}
if (!body.includes('refusePrerequisite(')) {
failures.push(`${fn.name} no longer refuses through refusePrerequisite() — the exit-code class is unpinned`);
}
}
// The NEGATIVE control, and the reason the loop above is a measurement rather
// than a tautology over an empty set: `readTsconfig` still throws, on purpose.
// A `tsconfig.json` checked into the tree that does not parse is a fact about
// the TREE -- a finding -- not a prerequisite the caller forgot to supply, so
// it keeps exit 1 and the pin above must be able to SEE a bare throw.
if (!/throw new Error\(/.test(readTsconfig.toString())) {
failures.push(
'readTsconfig no longer throws — the bare-throw pin above can no longer fail, so it stopped measuring',
);
}

const exitCodeCases = [
{ label: 'the refusal code is 3', ok: EXIT_PREREQUISITE_NOT_MET === 3 },
{ label: 'a finding is 1', ok: EXIT_FINDINGS === 1 },
{ label: 'the refusal code is distinct from a finding and from a pass',
ok: EXIT_PREREQUISITE_NOT_MET !== EXIT_FINDINGS && EXIT_PREREQUISITE_NOT_MET !== EXIT_OK },
];
for (const c of exitCodeCases) {
if (!c.ok) failures.push(`exit-code contract — ${c.label}`);
}

// The refusal TEXT. Four load-bearing clauses, each one a thing a reader who
// sees only the exit code cannot get anywhere else.
const refusalFixture = '--re-measure cannot run: 48 workspace dependenc(ies) have no built type entry point';
const refusalText = prerequisiteNotMetText(refusalFixture);
const textCases = [
{ label: 'carries the raising site\'s own message VERBATIM', ok: refusalText.includes(refusalFixture) },
{ label: 'names the class', ok: refusalText.includes('PREREQUISITE NOT MET') },
{ label: 'says it is neither a pass nor a finding', ok: /NOT a pass and NOT a finding/.test(refusalText) },
{ label: 'names its own code and the finding code it is distinct from',
ok: refusalText.includes(`Exit code ${EXIT_PREREQUISITE_NOT_MET}`)
&& refusalText.includes(`a finding's ${EXIT_FINDINGS}`) },
// The specific misreading this whole change exists to stop: taking an
// unmeasurable run for "a recorded debt went up" routes the reader to the
// ledger, and raising an entry there is a maintainer's act.
{ label: 'turns the reader away from the ledger rather than toward it',
ok: /no ledger entry below may\n?\s*be raised on it/.test(refusalText) },
{ label: 'warns that the code must be captured before any pipe', ok: refusalText.includes('BEFORE any pipe') },
];
for (const c of textCases) {
if (!c.ok) failures.push(`prerequisiteNotMetText — ${c.label}`);
}

// The shared workspace enumerator is a plain module with no CI invocation of
// its own (#11510); every gate that consolidated onto it folds in its checks.
failures.push(...workspaceEnumeratorSelfTest({ root: ROOT }));
Expand All@@ -5396,7 +5583,8 @@ function selfTest() {
+ ceilingCases.length + heapEnvCases.length} re-measure case(s) + ` +
`${typeEntryCases.length + closureCases.length + staleCases.length + sourceFileCases.length} ` +
`built-closure case(s) + ` +
`${planCases.length + rewriteCases.length + roundTripCases.length} auto-lowering case(s) hold.`,
`${planCases.length + rewriteCases.length + roundTripCases.length} auto-lowering case(s) + ` +
`${REFUSING.length * 2 + 1 + exitCodeCases.length + textCases.length} exit-code case(s) hold.`,
);
}

Expand Down
Loading