@@ -31,7 +31,13 @@ import os from 'node:os';
3131import path from 'node:path' ;
3232import { fileURLToPath } from 'node:url' ;
3333
34- import { inspectDistFreshness , packageDirLabel } from './lib/dist-freshness' ;
34+ import {
35+ EXIT_FINDINGS ,
36+ EXIT_PREREQUISITE_NOT_MET ,
37+ inspectDistFreshness ,
38+ packageDirLabel ,
39+ prerequisiteNotMetText ,
40+ } from './lib/dist-freshness' ;
3541import { declarationStamp } from '../../../scripts/check-regen-pending.mjs' ;
3642
3743const HERE = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
@@ -470,7 +476,7 @@ describe('build-api-surface.ts refuses a stale dist end to end (#7122)', () => {
470476 seed ( tree . spec , { distMtime : OLD , srcMtime : NEW } ) ;
471477
472478 const run = runGenerator ( tree . spec , [ ] ) ;
473- expect ( run . status ) . toBe ( 1 ) ;
479+ expect ( run . status ) . toBe ( EXIT_PREREQUISITE_NOT_MET ) ;
474480 expect ( run . stderr ) . toContain ( 'OLDER than packages/spec/src' ) ;
475481 expect ( run . stderr ) . toContain ( 'pnpm --filter @objectstack/spec build' ) ;
476482 expect ( baseline ( tree . spec ) ) . toBe ( SENTINEL ) ;
@@ -484,7 +490,7 @@ describe('build-api-surface.ts refuses a stale dist end to end (#7122)', () => {
484490 seed ( tree . spec , { distMtime : OLD , srcMtime : NEW } ) ;
485491
486492 const run = runGenerator ( tree . spec , [ '--check' ] ) ;
487- expect ( run . status ) . toBe ( 1 ) ;
493+ expect ( run . status ) . toBe ( EXIT_PREREQUISITE_NOT_MET ) ;
488494 expect ( run . stderr ) . toContain ( 'FALSE GREEN' ) ;
489495 expect ( run . stdout ) . not . toContain ( 'unchanged' ) ;
490496 } ) ;
@@ -495,9 +501,83 @@ describe('build-api-surface.ts refuses a stale dist end to end (#7122)', () => {
495501
496502 for ( const args of [ [ ] , [ '--check' ] ] ) {
497503 const run = runGenerator ( tree . spec , args ) ;
498- expect ( run . status ) . toBe ( 1 ) ;
504+ expect ( run . status ) . toBe ( EXIT_PREREQUISITE_NOT_MET ) ;
499505 expect ( run . stderr ) . toContain ( 'no .d.ts declarations' ) ;
500506 }
501507 expect ( baseline ( tree . spec ) ) . toBe ( SENTINEL ) ;
502508 } ) ;
503509} ) ;
510+
511+ // ── The exit code the refusal answers with (#19227) ──────────────────────────
512+ //
513+ // Until this card the five dist-reading gates refused with `process.exit(1)` —
514+ // a real finding's code — while `scripts/import-prerequisite.mjs:250` declared
515+ // `EXIT_PREREQUISITE_NOT_MET = 3` for these exact two words. The consumer that
516+ // makes the difference concrete is `scripts/pm/dispatch-gates.mjs --ran`: it
517+ // derives its NOT-MEASURED class from the recorded code and counts every other
518+ // non-kill code as a family that RAN.
519+ //
520+ // These pins are the spec-side half of keeping one vocabulary. The root-side
521+ // half is that module's own `--self-test`, which asserts the same 3 there; the
522+ // value is hand-carried between the two (the reason is in the constant's
523+ // docblock), so BOTH pins have to exist or the two declarations can fork in
524+ // silence.
525+ describe ( 'the prerequisite exit code, and the refusal text that explains it (#19227)' , ( ) => {
526+ it ( 'is 3, the number this repo already means by PREREQUISITE NOT MET' , ( ) => {
527+ expect ( EXIT_PREREQUISITE_NOT_MET ) . toBe ( 3 ) ;
528+ } ) ;
529+
530+ it ( "is distinct from a finding's — the whole point, and the half a machine reads" , ( ) => {
531+ expect ( EXIT_FINDINGS ) . toBe ( 1 ) ;
532+ expect ( EXIT_PREREQUISITE_NOT_MET ) . not . toBe ( EXIT_FINDINGS ) ;
533+ expect ( EXIT_PREREQUISITE_NOT_MET ) . not . toBe ( 0 ) ;
534+ } ) ;
535+
536+ it ( 'agrees with the root frame that declares it, read from that module' , async ( ) => {
537+ // The drift check, against the authority rather than against a copy of it.
538+ // `packages/spec` already declares `scripts/**` in CROSS_PACKAGE_TEST_INPUTS,
539+ // so this read is inside a declared radius.
540+ const frame = ( await import ( '../../../scripts/import-prerequisite.mjs' ) ) as {
541+ EXIT_PREREQUISITE_NOT_MET : number ;
542+ EXIT_FINDINGS : number ;
543+ } ;
544+ expect ( EXIT_PREREQUISITE_NOT_MET ) . toBe ( frame . EXIT_PREREQUISITE_NOT_MET ) ;
545+ expect ( EXIT_FINDINGS ) . toBe ( frame . EXIT_FINDINGS ) ;
546+ } ) ;
547+
548+ it ( 'carries the fleet phrase, so it greps beside its siblings' , ( ) => {
549+ // `check-dts-closure`, `check-dual-build-cjs-loads`, `check-i18n-bundles`,
550+ // `check-i18n-coverage` and `check-closing-target-claim` all print
551+ // `<gate>: PREREQUISITE NOT MET — …`. A reader who learned the phrase from
552+ // one of those must find this one with the same grep.
553+ write ( 'dist/contracts/index.d.ts' , 'export {};' , OLD ) ;
554+ write ( 'src/contracts/job-service.ts' , 'export interface JobRunOutcome { ok: boolean }' , NEW ) ;
555+
556+ const verdict = inspectDistFreshness ( sandbox , 'check' , 'pnpm run check:example' ) ;
557+ expect ( verdict . fresh ) . toBe ( false ) ;
558+ if ( verdict . fresh ) throw new Error ( 'unreachable: the sandbox dist is stale' ) ;
559+
560+ const text = prerequisiteNotMetText ( 'check:example' , verdict ) ;
561+ expect ( text ) . toContain ( 'check:example: PREREQUISITE NOT MET' ) ;
562+ // The verdict's own words are passed through, not paraphrased.
563+ expect ( text ) . toContain ( verdict . message ) ;
564+ // And the sentence a number alone cannot carry.
565+ expect ( text ) . toContain ( 'Nothing was measured' ) ;
566+ expect ( text ) . toContain ( `Exit code ${ EXIT_PREREQUISITE_NOT_MET } ` ) ;
567+ expect ( text ) . toContain ( `distinct from a finding's ${ EXIT_FINDINGS } ` ) ;
568+ } ) ;
569+
570+ it ( 'wraps a caller-composed refusal the same way — the partial-dist arm' , ( ) => {
571+ // `check:skill-examples` refuses a self-package whose `.d.ts` was never
572+ // emitted, which the mtime rule reads as fresh, so that refusal is not an
573+ // `inspectDistFreshness` verdict at all. Same trailer, one source.
574+ const text = prerequisiteNotMetText ( 'check:skill-examples [spec]' , {
575+ headline : 'a self-package on this surface has no declarations' ,
576+ detail : '\n - packages/client-react/dist/index.d.ts (missing)' ,
577+ } ) ;
578+ expect ( text ) . toContain ( 'check:skill-examples [spec]: PREREQUISITE NOT MET' ) ;
579+ expect ( text ) . toContain ( 'a self-package on this surface has no declarations' ) ;
580+ expect ( text ) . toContain ( 'packages/client-react/dist/index.d.ts (missing)' ) ;
581+ expect ( text ) . toContain ( 'Nothing was measured' ) ;
582+ } ) ;
583+ } ) ;
0 commit comments