@@ -29,6 +29,7 @@ import {
2929 reconcileFloors ,
3030 rootDevDepRowKey ,
3131 scanSkillFences ,
32+ scopedBuildNotice ,
3233 stripJsonComments ,
3334} from '../check-skill-examples.mjs' ;
3435import {
@@ -503,6 +504,139 @@ describe('the build filter carries the dependency closure', () => {
503504 } ) ;
504505} ) ;
505506
507+ /**
508+ * The scoping notice under the printed build command — objectui#7811, porting
509+ * the shape objectui#7795 landed for `check-doc-snippet-types.mjs`.
510+ *
511+ * Two precondition paths tell the reader to build and print a command that
512+ * builds a CLOSURE rather than the tree, and neither said so. Measured on
513+ * `origin/main` `65ce8c576`: `--build-filter` names 10 of the workspace's 40
514+ * packages and turbo expands those to 30 in scope, so the reader who ran the
515+ * printed command is left looking at 10 packages with no `dist/` and nothing to
516+ * tell that apart from a build that half-failed.
517+ *
518+ * Pinned here is the half that rots unwatched — the notice must keep DERIVING
519+ * its numbers and must never grow a package list of its own — plus the half that
520+ * would make it worthless: both paths have to actually print it. That it reaches
521+ * a real terminal is shown on an unbuilt tree in the pull request; this suite
522+ * cannot get there, because those paths only open when this repository's own
523+ * packages are unbuilt, and CI has built them by the time it runs.
524+ */
525+ describe ( 'the printed build command says what it does NOT build (objectui#7811)' , ( ) => {
526+ const SCRIPT = 'scripts/check-skill-examples.mjs' ;
527+ /** A workspace package name, as the notice would spell one if it grew a list. */
528+ const PACKAGE_NAME = / @ o b j e c t - u i \/ / ;
529+ const sourceOfScript = ( ) => fs . readFileSync ( path . join ( repoRoot , SCRIPT ) , 'utf8' ) ;
530+
531+ /**
532+ * Every place the gate prints the build command, paired with the slice that
533+ * runs from it to the `couldNotRun` exit that site leaves through.
534+ *
535+ * Anchored on the printed COMMAND rather than on `PRECONDITION NOT MET`: the
536+ * empty-marked-population path prints that headline too and correctly prints
537+ * no build command, so a scan anchored on the headline would demand a notice
538+ * where there is nothing to scope.
539+ */
540+ const buildCommandSites = ( source : string ) : string [ ] => {
541+ const marker = 'check-skill-examples.mjs --build-filter) --concurrency=2' ;
542+ const slices : string [ ] = [ ] ;
543+ for ( let at = source . indexOf ( marker ) ; at !== - 1 ; at = source . indexOf ( marker , at + 1 ) ) {
544+ const end = source . indexOf ( 'EXIT_CODES.couldNotRun;' , at ) ;
545+ expect ( end , 'a site that prints the build command must still leave through exit 2' ) . toBeGreaterThan ( at ) ;
546+ slices . push ( source . slice ( at , end ) ) ;
547+ }
548+ return slices ;
549+ } ;
550+
551+ it ( 'interpolates the counts it is handed — without this, a fixed sentence passes every pin below' , ( ) => {
552+ expect ( scopedBuildNotice ( 10 , 40 ) ) . toContain ( '10 package(s)' ) ;
553+ expect ( scopedBuildNotice ( 10 , 40 ) ) . toContain ( 'packages/ holds 40' ) ;
554+ // The control: different inputs, different text. A hard-coded "10 of 40" —
555+ // exactly the rotting summary this notice exists not to be — passes both
556+ // assertions above and fails these.
557+ const other = scopedBuildNotice ( 1 , 2 ) ;
558+ expect ( other ) . toContain ( '1 package(s)' ) ;
559+ expect ( other ) . toContain ( 'packages/ holds 2' ) ;
560+ expect ( other ) . not . toContain ( '10' ) ;
561+ expect ( other ) . not . toContain ( '40' ) ;
562+ } ) ;
563+
564+ it ( 'names no package of its own — the reader is sent to the filter, never to a copy of it' , ( ) => {
565+ const notice = scopedBuildNotice ( 10 , 40 ) ;
566+ // The control, and it has to be taken from THIS tree: a matcher that catches
567+ // no package name anywhere would satisfy the refusal below while proving
568+ // nothing. The names come from the gate's own reading of the workspace,
569+ // which is the very set a second copy would be copied from.
570+ const named = [ ...( analyze ( { } ) as unknown as { neededPackages : Set < string > } ) . neededPackages ] ;
571+ expect (
572+ named . filter ( ( n ) => PACKAGE_NAME . test ( n ) ) ,
573+ 'nothing in this tree matches the matcher, so the refusal below is vacuous' ,
574+ ) . not . toHaveLength ( 0 ) ;
575+ expect (
576+ notice ,
577+ 'a package name written here is a second list of what gets built, and it rots the first time the marked population moves' ,
578+ ) . not . toMatch ( PACKAGE_NAME ) ;
579+ expect ( notice ) . toContain ( '--build-filter' ) ;
580+ expect ( notice , 'without a way to ask, the notice is one more thing the reader has to trust' ) . toContain (
581+ '--dry=text' ,
582+ ) ;
583+ } ) ;
584+
585+ it ( 'says the build is scoped and that what it leaves behind is the designed end state' , ( ) => {
586+ const notice = scopedBuildNotice ( 10 , 40 ) ;
587+ expect ( notice ) . toContain ( 'not a whole-tree build' ) ;
588+ expect ( notice ) . toContain ( 'left exactly as it was' ) ;
589+ expect ( notice ) . toContain ( 'designed end state' ) ;
590+ } ) ;
591+
592+ it ( 'is printed on BOTH precondition paths that print the command, not merely defined' , ( ) => {
593+ const slices = buildCommandSites ( sourceOfScript ( ) ) ;
594+ expect (
595+ slices ,
596+ 'objectui#7811 named two sites — the regular path and the --self-test type-check leg' ,
597+ ) . toHaveLength ( 2 ) ;
598+ for ( const slice of slices ) {
599+ expect (
600+ slice ,
601+ 'a notice nothing calls is a string in a file, and objectui#7811 was filed about a reader who was never told' ,
602+ ) . toContain ( 'scopedBuildNotice(' ) ;
603+ }
604+ } ) ;
605+
606+ it ( 'is handed numbers the gate read, never a literal at the call site' , ( ) => {
607+ for ( const slice of buildCommandSites ( sourceOfScript ( ) ) ) {
608+ const call = slice . slice ( slice . indexOf ( 'scopedBuildNotice(' ) ) ;
609+ expect ( call ) . toContain ( 'state.neededPackages.size' ) ;
610+ expect ( call ) . toContain ( 'state.packageDirOf' ) ;
611+ expect (
612+ call ,
613+ 'a number written at the call site is the rotting summary this notice exists not to be' ,
614+ ) . not . toMatch ( / s c o p e d B u i l d N o t i c e \( \s * \d / ) ;
615+ }
616+ } ) ;
617+
618+ it ( 'counts the workspace from what the gate itself read, not from a number written down' , ( ) => {
619+ const names = withTree (
620+ ( write ) => {
621+ write ( 'skills/objectui/guides/sample.md' , '# Sample\n' ) ;
622+ write (
623+ 'packages/pkg-a/package.json' ,
624+ JSON . stringify ( { name : '@fixture/pkg-a' , types : './dist/index.d.ts' } ) ,
625+ ) ;
626+ write (
627+ 'packages/pkg-b/package.json' ,
628+ JSON . stringify ( { name : '@fixture/pkg-b' , types : './dist/index.d.ts' } ) ,
629+ ) ;
630+ } ,
631+ ( dir ) =>
632+ Object . keys (
633+ ( analyze ( { root : dir } ) as unknown as { packageDirOf : Record < string , string > } ) . packageDirOf ,
634+ ) . sort ( ) ,
635+ ) ;
636+ expect ( names ) . toEqual ( [ '@fixture/pkg-a' , '@fixture/pkg-b' ] ) ;
637+ } ) ;
638+ } ) ;
639+
506640describe ( 'wiring — the gate is reachable and a markdown-only PR starts it' , ( ) => {
507641 const SCRIPT = 'scripts/check-skill-examples.mjs' ;
508642 const workflowDir = path . join ( repoRoot , '.github/workflows' ) ;
0 commit comments