Skip to content

Commit 2ca1613

Browse files
committed
fix(devx): stop check-query-options-erasure-ratchet.mjs re-execing its importer
The loudest entry in the ledger. Two separate import side effects, fixed two different ways: - `ensureStackHeadroom()` at module scope. It re-execs with --stack-size and then calls process.exit(status), so importing this module REPLACED the importer's process with a fresh run of this gate. Guarded IN PLACE rather than moved into main(): its docblock's invariant is an ordering one ("re-exec once, before any linting -- including before --self-test"), and leaving the call at its original position in module order is what keeps that ordering checkable by reading rather than by re-deriving it. - The tail becomes async main(), with the self-test dispatch, behind the guard. No module-scope declaration between the two constructs an ESLint instance (all three `new ESLint` sites are inside functions), so nothing heavy moved across the re-exec point. checkHeadroomAdoption() still reads this gate as armed: it tests /ensureStackHeadroom\s*\(/ over comment-stripped source, which an indented call satisfies. CLI byte-identical before/after on both paths — stdout, stderr and exit code — captured through scripts/pm/os-verify-lock.sh. Import probe with clean argv now returns in 0.5s with the sentinel only and empty stderr, where before it never returned at all. STALE first; ledger 6 -> 5.
1 parent a0ea234 commit 2ca1613

2 files changed

Lines changed: 99 additions & 81 deletions

File tree

scripts/check-entry-guard.mjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,6 @@ export function importUnsafeStatements(source) {
460460
const KNOWN_IMPORT_UNSAFE = new Set([
461461
'scripts/check-changeset-no-major.mjs',
462462
'scripts/check-empty-changeset.mjs',
463-
'scripts/check-query-options-erasure-ratchet.mjs',
464463
'scripts/objectui-range.mjs',
465464
'scripts/qa/qa-rollup.mjs',
466465
'scripts/ts-parse.mjs',

scripts/check-query-options-erasure-ratchet.mjs

Lines changed: 99 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,24 @@ import {
120120
osThreadStackKb,
121121
stackRearmPlan,
122122
} from './eslint-stack-headroom.mjs';
123+
import { isEntrypoint } from './invoked-as.mjs';
123124

124125
// This gate lints IN-PROCESS, so it does not inherit the `--stack-size` the
125126
// root `lint` script puts on ESLint's CLI entry, and this repo's deepest file
126127
// does not parse without it (#10449). Re-exec once, before any linting --
127128
// including before `--self-test`, whose headroom assertion below is only a fact
128129
// about the gate if the self-test runs on the same stack the gate does.
129-
ensureStackHeadroom(fileURLToPath(import.meta.url));
130+
//
131+
// Guarded IN PLACE rather than moved into main(): the ordering above is the
132+
// whole point of the call, and leaving it at its original position in module
133+
// order is what makes that ordering checkable by reading. `rearmWithStackHeadroom`
134+
// re-execs and then calls `process.exit(status)`, so on an import path this line
135+
// replaced the IMPORTER's process with a fresh run of this gate -- the loudest
136+
// entry in the KNOWN_IMPORT_UNSAFE ledger, and the reason a `main()` extraction
137+
// alone would not have been enough here.
138+
if (isEntrypoint(import.meta.url)) {
139+
ensureStackHeadroom(fileURLToPath(import.meta.url));
140+
}
130141

131142
const __dirname = dirname(fileURLToPath(import.meta.url));
132143
const repoRoot = resolve(__dirname, '..');
@@ -897,92 +908,100 @@ async function selfTest() {
897908
// ---------------------------------------------------------------------------
898909
// main
899910

900-
if (process.argv.includes('--self-test')) {
901-
await selfTest();
902-
process.exit(0);
903-
}
911+
async function main() {
912+
if (!eslintConfig.some(carriesRule)) {
913+
console.error(
914+
`check-query-options-erasure-ratchet: no config block carries \`${QUERY_OPTIONS_RULE_ID}\`.\n` +
915+
'The rule was renamed or removed without updating QUERY_OPTIONS_RULE_ID — refusing\n' +
916+
'to report "clean" for a rule that is no longer being measured.',
917+
);
918+
process.exit(2);
919+
}
904920

905-
if (!eslintConfig.some(carriesRule)) {
906-
console.error(
907-
`check-query-options-erasure-ratchet: no config block carries \`${QUERY_OPTIONS_RULE_ID}\`.\n` +
908-
'The rule was renamed or removed without updating QUERY_OPTIONS_RULE_ID — refusing\n' +
909-
'to report "clean" for a rule that is no longer being measured.',
910-
);
911-
process.exit(2);
912-
}
921+
const update = process.argv.includes('--update');
922+
const baselineFile = JSON.parse(readFileSync(resolve(repoRoot, BASELINE_PATH), 'utf8'));
923+
const baseline = baselineFile.nonTest ?? {};
924+
const testCeiling = baselineFile.testSurface?.sites;
913925

914-
const update = process.argv.includes('--update');
915-
const baselineFile = JSON.parse(readFileSync(resolve(repoRoot, BASELINE_PATH), 'utf8'));
916-
const baseline = baselineFile.nonTest ?? {};
917-
const testCeiling = baselineFile.testSurface?.sites;
926+
if (typeof testCeiling !== 'number' && !update) {
927+
console.error(
928+
`check-query-options-erasure-ratchet: ${BASELINE_PATH} has no numeric ` +
929+
'`testSurface.sites`. Refusing to report clean with half the surface unmeasured.',
930+
);
931+
process.exit(2);
932+
}
918933

919-
if (typeof testCeiling !== 'number' && !update) {
920-
console.error(
921-
`check-query-options-erasure-ratchet: ${BASELINE_PATH} has no numeric ` +
922-
'`testSurface.sites`. Refusing to report clean with half the surface unmeasured.',
934+
// Two runs, one per population. The split is done by ESLint against the very
935+
// globs the rule uses, so there is no second definition of "is this a test
936+
// file" for the two halves to drift apart on.
937+
const nonTest = sortKeys(await measure(new Set(Object.keys(baseline))));
938+
const everything = sortKeys(await measure(new Set([...Object.keys(baseline), ...QUERY_OPTIONS_TEST_GLOBS])));
939+
const testOnly = sortKeys(
940+
Object.fromEntries(Object.entries(everything).filter(([file]) => !(file in nonTest))),
923941
);
924-
process.exit(2);
925-
}
942+
const testSites = sum(testOnly);
943+
944+
if (update) {
945+
const next = {
946+
...baselineFile,
947+
nonTest,
948+
testSurface: { ...(baselineFile.testSurface ?? {}), sites: testSites },
949+
};
950+
writeFileSync(resolve(repoRoot, BASELINE_PATH), JSON.stringify(next, null, 2) + '\n');
951+
console.log(
952+
`query-options-erasure baseline updated: ${sum(nonTest)} non-test site(s) in ` +
953+
`${Object.keys(nonTest).length} file(s); test surface ${testSites} site(s) in ` +
954+
`${Object.keys(testOnly).length} file(s).`,
955+
);
956+
process.exit(0);
957+
}
958+
959+
const monotonicity = baselineKeysAddedSinceMergeBase(Object.keys(baseline));
960+
const errors = diffRatchet({
961+
baseline,
962+
current: nonTest,
963+
testCeiling,
964+
testSites,
965+
addedBaselineKeys: monotonicity?.added ?? [],
966+
});
967+
968+
if (errors.length > 0) {
969+
console.error(`✗ query-options-erasure ratchet (${errors.length} problem(s)):\n`);
970+
for (const e of errors) console.error(` • ${e}`);
971+
console.error(
972+
`\nUnswept: ${sum(nonTest)} non-test site(s) in ${Object.keys(nonTest).length} file(s), ` +
973+
`plus ${testSites} in test code. Sweeping is a separate batch — part of the residual ` +
974+
`needs a boundary type WRITTEN (objectql's \`hookContext.input.options\`, the metadata ` +
975+
`loader's query bag), not the assertion deleted. See issue #4918.`,
976+
);
977+
process.exit(1);
978+
}
926979

927-
// Two runs, one per population. The split is done by ESLint against the very
928-
// globs the rule uses, so there is no second definition of "is this a test
929-
// file" for the two halves to drift apart on.
930-
const nonTest = sortKeys(await measure(new Set(Object.keys(baseline))));
931-
const everything = sortKeys(await measure(new Set([...Object.keys(baseline), ...QUERY_OPTIONS_TEST_GLOBS])));
932-
const testOnly = sortKeys(
933-
Object.fromEntries(Object.entries(everything).filter(([file]) => !(file in nonTest))),
934-
);
935-
const testSites = sum(testOnly);
936-
937-
if (update) {
938-
const next = {
939-
...baselineFile,
940-
nonTest,
941-
testSurface: { ...(baselineFile.testSurface ?? {}), sites: testSites },
942-
};
943-
writeFileSync(resolve(repoRoot, BASELINE_PATH), JSON.stringify(next, null, 2) + '\n');
944980
console.log(
945-
`query-options-erasure baseline updated: ${sum(nonTest)} non-test site(s) in ` +
946-
`${Object.keys(nonTest).length} file(s); test surface ${testSites} site(s) in ` +
947-
`${Object.keys(testOnly).length} file(s).`,
981+
`query-options-erasure ratchet holds: ${sum(nonTest)} unswept non-test site(s) in ` +
982+
`${Object.keys(nonTest).length} file(s), none new, and every file measured parsed. ` +
983+
`Every other non-test file under packages/ is covered by \`pnpm lint\`.`,
948984
);
949-
process.exit(0);
950-
}
951-
952-
const monotonicity = baselineKeysAddedSinceMergeBase(Object.keys(baseline));
953-
const errors = diffRatchet({
954-
baseline,
955-
current: nonTest,
956-
testCeiling,
957-
testSites,
958-
addedBaselineKeys: monotonicity?.added ?? [],
959-
});
960-
961-
if (errors.length > 0) {
962-
console.error(`✗ query-options-erasure ratchet (${errors.length} problem(s)):\n`);
963-
for (const e of errors) console.error(` • ${e}`);
964-
console.error(
965-
`\nUnswept: ${sum(nonTest)} non-test site(s) in ${Object.keys(nonTest).length} file(s), ` +
966-
`plus ${testSites} in test code. Sweeping is a separate batch — part of the residual ` +
967-
`needs a boundary type WRITTEN (objectql's \`hookContext.input.options\`, the metadata ` +
968-
`loader's query bag), not the assertion deleted. See issue #4918.`,
985+
console.log(
986+
` test surface: ${testSites} site(s) in ${Object.keys(testOnly).length} file(s) — at the ` +
987+
`ceiling, outside the blocking rule by the #4918 triage (a rejection test must be able ` +
988+
`to build off-contract input).`,
989+
);
990+
console.log(
991+
monotonicity
992+
? ` baseline key set verified against ${monotonicity.base}: no files added.`
993+
: ` NOT verified: could not read the baseline at the merge base with main (no git, ` +
994+
`shallow clone, or the baseline is new here), so "no files added" is unchecked this run.`,
969995
);
970-
process.exit(1);
971996
}
972997

973-
console.log(
974-
`✓ query-options-erasure ratchet holds: ${sum(nonTest)} unswept non-test site(s) in ` +
975-
`${Object.keys(nonTest).length} file(s), none new, and every file measured parsed. ` +
976-
`Every other non-test file under packages/ is covered by \`pnpm lint\`.`,
977-
);
978-
console.log(
979-
` test surface: ${testSites} site(s) in ${Object.keys(testOnly).length} file(s) — at the ` +
980-
`ceiling, outside the blocking rule by the #4918 triage (a rejection test must be able ` +
981-
`to build off-contract input).`,
982-
);
983-
console.log(
984-
monotonicity
985-
? ` baseline key set verified against ${monotonicity.base}: no files added.`
986-
: ` NOT verified: could not read the baseline at the merge base with main (no git, ` +
987-
`shallow clone, or the baseline is new here), so "no files added" is unchecked this run.`,
988-
);
998+
// The dispatch, behind the same predicate. This module exports `diffRatchet`,
999+
// `measure` and the baseline helpers; unguarded, importing one of them ran two
1000+
// full ESLint passes over packages/** inside the importer.
1001+
if (isEntrypoint(import.meta.url)) {
1002+
if (process.argv.includes('--self-test')) {
1003+
await selfTest();
1004+
process.exit(0);
1005+
}
1006+
await main();
1007+
}

0 commit comments

Comments
 (0)