Skip to content

Commit 8cefaa8

Browse files
committed
Merge origin/main (#8066) into claude/issue-5174-ungated-docs-batch23
#8066 touches only scripts/check-skill-examples.mjs and its test — no package source, so the built types this corpus is judged against do not move and no rebuild is owed. Its test is a reader of the gate file, so the reader suite is re-run on this head. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
2 parents 55b7fdb + 295804a commit 8cefaa8

2 files changed

Lines changed: 190 additions & 0 deletions

File tree

‎scripts/__tests__/check-skill-examples.test.ts‎

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
reconcileFloors,
3030
rootDevDepRowKey,
3131
scanSkillFences,
32+
scopedBuildNotice,
3233
stripJsonComments,
3334
} from '../check-skill-examples.mjs';
3435
import {
@@ -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 = /@object-ui\//;
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(/scopedBuildNotice\(\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+
506640
describe('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');

‎scripts/check-skill-examples.mjs‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,12 @@ export function analyze({ root = repoRoot, measure = false, baseline = KNOWN_BAR
10741074
untypedDependencies,
10751075
declaredSpecifiers,
10761076
neededPackages,
1077+
// Carried out so the precondition paths can say how much of the workspace
1078+
// their build filter leaves alone (objectui#7811). It is the map this
1079+
// function already walked to resolve `neededPackages`, not a second reading
1080+
// of the tree — the whole point of `scopedBuildNotice` is that its numbers
1081+
// are DERIVED, so the number it is handed has to come from here.
1082+
packageDirOf,
10771083
};
10781084
}
10791085

@@ -1102,6 +1108,50 @@ export function buildFilterArgs(packages) {
11021108
.join(' ');
11031109
}
11041110

1111+
/**
1112+
* What that printed build command does NOT do, printed under it — objectui#7811,
1113+
* porting the shape objectui#7795 landed as `check-doc-snippet-types.mjs`'s
1114+
* `scopedBuildNotice`.
1115+
*
1116+
* Both precondition paths tell the reader to build, and both print a command
1117+
* that builds a CLOSURE rather than the tree, having said so nowhere. Measured
1118+
* on `origin/main` `65ce8c576`: `--build-filter` names 10 of the workspace's 40
1119+
* packages, and expanding each one's dependency closure puts 30 in scope — so
1120+
* running the printed command exactly as given leaves 10 packages with no
1121+
* `dist/`, and nothing distinguishes that from a build that half-failed. The
1122+
* trimming is deliberate (`skill-examples.yml`'s header forbids the unfiltered
1123+
* build in as many words, under the 2026-08-16 ruling on objectui#4846), so what
1124+
* was missing is the sentence, never a wider filter.
1125+
*
1126+
* Both counts are ARGUMENTS and the text names no package on purpose. A list of
1127+
* "what gets built" written out here would be a second copy of a set this file
1128+
* already computes, and it would rot the first time the marked population moved;
1129+
* the reader who wants the exact set is sent to the same filter, expanded by the
1130+
* same tool that is about to run it — `--dry=text` makes turbo print a
1131+
* `Packages in scope` line (and, below it, a table of the same set) instead of
1132+
* building. `packages/` is the directory `derivePackageTypePaths` walks, which
1133+
* is where `total` is counted from; it is a location, not a package list.
1134+
*
1135+
* `check-skill-examples.test.ts` pins the half that rots unwatched — the numbers
1136+
* stay derived, no package name grows here — plus the half that would make it
1137+
* worthless: both precondition paths have to actually print it.
1138+
*
1139+
* @param {number} named packages `--build-filter` names (the marked examples' imports)
1140+
* @param {number} total packages under `packages/` this gate resolves against
1141+
* @returns {string} one paragraph for a precondition path's stderr
1142+
*/
1143+
export function scopedBuildNotice(named, total) {
1144+
return (
1145+
`That build is SCOPED, and it is not a whole-tree build: --build-filter names the ${named} package(s) the ` +
1146+
`marked examples import (packages/ holds ${total}) plus each one's dependency closure, and every package ` +
1147+
'outside that closure is left exactly as it was. An unbuilt package still sitting there when the build ' +
1148+
"finishes is this gate's designed end state, not a build that half-failed — a whole-workspace build for a " +
1149+
'guide check is what this gate\'s workflow refuses outright. For the exact set, ask that same filter rather ' +
1150+
'than a list written down somewhere else: append --dry=text to the command above and read its "Packages in ' +
1151+
'scope" line.'
1152+
);
1153+
}
1154+
11051155
// ── Reporting ────────────────────────────────────────────────────────────────
11061156

11071157
function formatDiagnostic(diagnostic, block) {
@@ -1285,6 +1335,9 @@ function main() {
12851335
' pnpm exec turbo run build $(node scripts/check-skill-examples.mjs --build-filter) --concurrency=2\n' +
12861336
' pnpm check:skill-examples',
12871337
);
1338+
console.error(
1339+
`\n${scopedBuildNotice(state.neededPackages.size, Object.keys(state.packageDirOf).length)}`,
1340+
);
12881341
return EXIT_CODES.couldNotRun;
12891342
}
12901343

@@ -1721,6 +1774,9 @@ export function selfTest() {
17211774
' pnpm exec turbo run build $(node scripts/check-skill-examples.mjs --build-filter) --concurrency=2\n' +
17221775
' node scripts/check-skill-examples.mjs --self-test',
17231776
);
1777+
console.error(
1778+
`\n${scopedBuildNotice(state.neededPackages.size, Object.keys(state.packageDirOf).length)}`,
1779+
);
17241780
for (const c of cases.filter((c2) => !c2.ok)) console.error(` ✗ ${c.name}${c.detail ? ` — ${c.detail}` : ''}`);
17251781
return EXIT_CODES.couldNotRun;
17261782
}

0 commit comments

Comments
 (0)