@@ -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
131142const __dirname = dirname ( fileURLToPath ( import . meta. url ) ) ;
132143const 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