Skip to content

Commit a6ee32e

Browse files
committed
wip(sdui): the manifest self-test drives the in-tree mode and the in-tree producer
Nine rows for the no-pin mode, each a real git fixture: the digest reds a staged registry source edit and a new workspace edge, ignores tests, unreached packages and unstaged bytes; no pin and no console reds naming both; a pin-mode record reds with the in-tree command; the in-tree producer builds through a stub turbo and writes a tree the gate passes. Git children over fixtures run with gitFreeEnv(); an in-tree record writes its own note. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A6SEitNngAMugGbuJHWsDS
1 parent 55c3ebf commit a6ee32e

2 files changed

Lines changed: 255 additions & 8 deletions

File tree

‎scripts/check-sdui-manifest.mjs‎

Lines changed: 215 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,30 @@
5555
* This is the only check here that needs an input from outside this tree,
5656
* so it is the only one that can report NOT CHECKED — see below.
5757
*
58+
* ## With no pin: the in-tree registry (#25712)
59+
*
60+
* Once objectui's tree lives under `ui/` and `.objectui-sha` is gone (#25677),
61+
* there is no pin to build and no checkout to ask, so the oracle is the
62+
* registry in this tree. The mode is decided by `.objectui-sha` alone: with it
63+
* present, nothing above reads differently. Without it:
64+
*
65+
* 1–2. unchanged — the same shape and tamper checks, against a record keyed
66+
* to the in-tree producer (`IN_TREE_RECORD_FIELDS`: the pin-keyed
67+
* fields are not required, the `inTree` block is).
68+
* 3. STALENESS is a SOURCE DIGEST — `registrySourceDigest` over the
69+
* registry packages' `src/` as the index holds them must equal the
70+
* record's `inTree` reading. A registry source edit without
71+
* regeneration reds HERE, and the red prints the one command that
72+
* clears it: the generator, which builds the registry from this tree
73+
* and regenerates. Nothing is built in this gate — the digest is a
74+
* `git ls-files` over the index, with no dist and no node_modules read.
75+
* 4. none. The in-tree `@object-ui/core` version moves with every release
76+
* of the one fixed version group while the registry does not, so the
77+
* record carries it as provenance and the success line prints it.
78+
*
79+
* No pin AND no in-tree console is RED, naming both: a tree with neither has
80+
* nothing to be fresh against, which is a broken tree, not a skip.
81+
*
5882
* ## What this deliberately does NOT do, and where that risk is held
5983
*
6084
* No per-PR regeneration: that would put an objectui clone and build (network,
@@ -120,6 +144,7 @@ import { tmpdir } from 'node:os';
120144
import { dirname, join, sep } from 'node:path';
121145
import { fileURLToPath } from 'node:url';
122146

147+
import { gitFreeEnv } from './git-env.mjs';
123148
import { isEntrypoint } from './invoked-as.mjs';
124149

125150
const DEFAULT_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
@@ -213,10 +238,13 @@ export const REGISTRY_MODULES = Object.freeze([
213238
*/
214239
const PRODUCER = 'scripts/gen-sdui-manifest-node.mjs';
215240

241+
/** The repo helpers the producer (and this gate, which it imports in-tree) import: copied beside it in the self-test's fixtures. */
242+
const PRODUCER_IMPORTS = ['scripts/git-env.mjs', 'scripts/invoked-as.mjs'];
243+
216244
/**
217245
* The population this gate reads, declared for `scripts/pm/dispatch-gates.mjs`.
218246
*
219-
* `checkTree` opens exactly three files. None of them was visible to the
247+
* With a pin, `checkTree` opens exactly three files. None of them was visible to the
220248
* derivation: it reads SOURCE TEXT, and a literal with no separator is refused
221249
* by `hintCovers` as too generic (`sdui.manifest.json`), while `.objectui-sha`
222250
* is not one of the dot-prefixed names the extractor admits and the record path
@@ -233,6 +261,10 @@ const PRODUCER = 'scripts/gen-sdui-manifest-node.mjs';
233261
* to nothing else.
234262
*
235263
* ⛔ Not a whole-tree marker, and not `scripts/**`: three files are three files.
264+
* Without a pin it also reads the in-tree console's manifest and the registry
265+
* packages under `ui/packages/` (#25712) — the subtree the digest walks, so a
266+
* UI package edit derives this gate; on a tree with no `ui/` both hints match
267+
* nothing and only display.
236268
*
237269
* The self-test derives the coupling from `READ_PATHS` — the same object
238270
* `checkTree` reads — so a moved or added read reds here rather than leaving the
@@ -560,6 +592,9 @@ export function registrySourceDigest(root) {
560592
let listing;
561593
try {
562594
listing = execFileSync('git', ['-C', root, 'ls-files', '-s', '-z', '--', ...packages.map((dir) => `${dir}/src`)], {
595+
// #16644: `-C root` alone names the index read — the self-test hands this
596+
// mkdtemp fixtures, and an inherited GIT_DIR / GIT_INDEX_FILE outranks it.
597+
env: gitFreeEnv(),
563598
encoding: 'utf8',
564599
stdio: ['ignore', 'pipe', 'pipe'],
565600
maxBuffer: 64 * 1024 * 1024,
@@ -629,6 +664,15 @@ const SELF_TEST_BATTERIES = Object.freeze({
629664
'--require-objectui turns an unreachable checkout RED': 1,
630665
'a pinned commit with no packages/core/package.json is RED': 1,
631666
'the producer, run under a TMPDIR behind a symlink, writes a tree this gate passes': 1,
667+
'no pin: an in-tree record over unchanged registry sources passes': 1,
668+
'no pin and no in-tree console is RED, naming both': 1,
669+
'no pin: a pin-mode record (no inTree block) is RED with the in-tree command': 1,
670+
'no pin: a staged registry source edit is RED with the in-tree command': 1,
671+
'no pin: an edit the index does not hold is not a registry change': 1,
672+
'no pin: a test file or a package the registry does not reach is not a registry change': 1,
673+
'no pin: a new workspace edge into the registry is RED': 1,
674+
'no pin: a hand-edited artefact is RED': 1,
675+
'no pin: the in-tree producer builds, then writes a tree this gate passes': 1,
632676
});
633677

634678
// DELETING an entry silences that battery's floor exactly as effectively as
@@ -637,7 +681,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
637681
// the literal above, so the roster falls below this number; the table
638682
// cross-check in the floor block is the other half, and names WHICH label
639683
// collided.
640-
const SELF_TEST_BATTERY_FLOOR = 13;
684+
const SELF_TEST_BATTERY_FLOOR = 22;
641685

642686
function selfTest() {
643687
const mk = (mutate, { pin = 'a'.repeat(40), version = '0.0.0-selftest' } = {}) => {
@@ -739,7 +783,7 @@ function selfTest() {
739783
}
740784

741785
const producerSource = readFileSync(join(DEFAULT_ROOT, PRODUCER), 'utf8');
742-
copyFileSync(join(DEFAULT_ROOT, PRODUCER), join(root, PRODUCER));
786+
for (const file of [PRODUCER, ...PRODUCER_IMPORTS]) copyFileSync(join(DEFAULT_ROOT, file), join(root, file));
743787
const parserDist = join(root, 'packages', 'sdui-parser', 'dist');
744788
mkdirSync(parserDist, { recursive: true });
745789
writeFileSync(
@@ -780,6 +824,116 @@ function selfTest() {
780824
};
781825
const produced = mkProduced();
782826

827+
/**
828+
* The merged layout (#25712): no pin, an in-tree console, and one package
829+
* per registry name under `ui/packages/`, each with one source — plus
830+
* `@object-ui/types`, which `@object-ui/core` reaches through a dependency
831+
* (so the closure, not only the seed list, is under test), and
832+
* `@object-ui/unrelated`, which nothing reaches. A real git repository: the
833+
* digest reads the INDEX, so a stub would leave exactly that reading
834+
* unexercised. Every git child here runs with `gitFreeEnv()` (#16644).
835+
*/
836+
const UI_FIXTURE_NAMES = [REGISTRY_CORE, ...REGISTRY_MODULES, '@object-ui/types', '@object-ui/unrelated'];
837+
const pkgDir = (name) => join(UI_PACKAGES, name.slice('@object-ui/'.length));
838+
const gitIn = (root) => (...args) =>
839+
execFileSync('git', ['-C', root, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], env: gitFreeEnv() });
840+
const mkUiSources = (prefix) => {
841+
const root = mkdtempSync(join(tmpdir(), prefix));
842+
const git = gitIn(root);
843+
git('init', '-q');
844+
writeFileSync(join(root, '.gitignore'), 'node_modules/\n');
845+
mkdirSync(join(root, UI_CONSOLE), { recursive: true });
846+
writeFileSync(join(root, READ_PATHS.console), JSON.stringify({ name: '@object-ui/console', private: true }, null, 2));
847+
for (const name of UI_FIXTURE_NAMES) {
848+
const dir = join(root, pkgDir(name));
849+
mkdirSync(join(dir, 'src'), { recursive: true });
850+
const dependencies = name === REGISTRY_CORE ? { '@object-ui/types': 'workspace:*' } : {};
851+
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name, version: '0.0.0-selftest', dependencies }, null, 2));
852+
writeFileSync(join(dir, 'src', 'index.ts'), `export const id = ${JSON.stringify(name)};\n`);
853+
}
854+
git('add', '-A');
855+
return { root, git };
856+
};
857+
const mkInTree = (mutate) => {
858+
const { root, git } = mkUiSources('sdui-manifest-in-tree-');
859+
const manifest = { components: { flex: { type: 'flex', inputs: [] } } };
860+
const raw = JSON.stringify(manifest, null, 2);
861+
mkdirSync(join(root, 'scripts'), { recursive: true });
862+
writeFileSync(join(root, READ_PATHS.artefact), raw);
863+
const digest = registrySourceDigest(root);
864+
writeFileSync(
865+
join(root, READ_PATHS.record),
866+
JSON.stringify(
867+
{
868+
source: RECORD_SOURCE,
869+
sha256: createHash('sha256').update(raw).digest('hex'),
870+
components: 1,
871+
inTree: {
872+
modulesRoot: UI_CONSOLE,
873+
objectuiWorkspaceVersion: '0.0.0-selftest',
874+
registryPackages: digest.packages,
875+
registrySources: digest.files,
876+
registrySourcesSha256: digest.sha256,
877+
},
878+
},
879+
null,
880+
2,
881+
),
882+
);
883+
mutate?.(root, git);
884+
return root;
885+
};
886+
const editRecord = (root, edit) => {
887+
const p = join(root, READ_PATHS.record);
888+
writeFileSync(p, JSON.stringify(edit(JSON.parse(readFileSync(p, 'utf8'))), null, 2));
889+
};
890+
891+
/**
892+
* The PRODUCER's in-tree run (no pin): the real generator, this gate and the
893+
* two helpers they import, copied into the merged-layout fixture beside a
894+
* stub adapter and a stub `node_modules/.bin/turbo`. The stub is what writes
895+
* `@object-ui/core`'s dist into the console, so a producer that skipped its
896+
* build exits 1 on the missing dist and the row reads RED (missing artefact)
897+
* with the producer's stderr above it.
898+
*/
899+
const mkProducedInTree = () => {
900+
const { root } = mkUiSources('sdui-manifest-in-tree-producer-');
901+
mkdirSync(join(root, 'scripts'), { recursive: true });
902+
for (const file of [PRODUCER, 'scripts/check-sdui-manifest.mjs', ...PRODUCER_IMPORTS]) {
903+
copyFileSync(join(DEFAULT_ROOT, file), join(root, file));
904+
}
905+
const parserDist = join(root, 'packages', 'sdui-parser', 'dist');
906+
mkdirSync(parserDist, { recursive: true });
907+
writeFileSync(
908+
join(parserDist, 'index.mjs'),
909+
'export const manifestFromConfigs = (configs) => ({ components: Object.fromEntries(configs.map((c) => [c.type, { type: c.type, inputs: [] }])) });\n',
910+
);
911+
for (const name of [REGISTRY_CORE, ...REGISTRY_MODULES]) {
912+
const pkg = join(root, UI_CONSOLE, 'node_modules', ...name.split('/'));
913+
mkdirSync(join(pkg, 'dist'), { recursive: true });
914+
writeFileSync(
915+
join(pkg, 'package.json'),
916+
JSON.stringify({ name, version: '0.0.0-selftest', type: 'module', exports: { '.': { import: './dist/index.js' } } }),
917+
);
918+
if (name !== REGISTRY_CORE) writeFileSync(join(pkg, 'dist', 'index.js'), 'export {};\n');
919+
}
920+
const coreDist = `${UI_CONSOLE}/node_modules/${REGISTRY_CORE}/dist`;
921+
mkdirSync(join(root, 'node_modules', '.bin'), { recursive: true });
922+
writeFileSync(
923+
join(root, 'node_modules', '.bin', 'turbo'),
924+
'#!/bin/sh\n' +
925+
`mkdir -p '${coreDist}' && printf '%s\\n' "export const ComponentRegistry = { getPublicConfigs: () => [{ type: 'selftest_flex' }] };" > '${coreDist}/index.js'\n`,
926+
{ mode: 0o755 },
927+
);
928+
const run = spawnSync(process.execPath, [join(root, PRODUCER)], { encoding: 'utf8', env: gitFreeEnv() });
929+
if (run.status !== 0) {
930+
console.error(` the in-tree producer exited ${run.status ?? run.signal}:`);
931+
for (const line of String(run.stderr ?? run.error ?? '').trim().split('\n')) console.error(` ${line}`);
932+
}
933+
return root;
934+
};
935+
const producedInTree = mkProducedInTree();
936+
783937
const cases = [
784938
['green fixture passes', mk(), 0, blind],
785939
['missing artefact is RED', mk((r) => rmSync(join(r, 'sdui.manifest.json'))), 1, blind],
@@ -824,6 +978,44 @@ function selfTest() {
824978
// with the fixture's own objectui as the oracle, so the version leg cannot
825979
// read `unreachable` and pass by not running.
826980
['the producer, run under a TMPDIR behind a symlink, writes a tree this gate passes', produced.root, 0, { objectuiRoot: produced.objectuiRoot, requireObjectui: true }],
981+
982+
// ── No pin: the in-tree registry (#25712) ──────────────────────────────
983+
//
984+
// The fifth column is text every problem of a RED row must carry between
985+
// them: a red that does not name both missing oracles, or the one command
986+
// that clears it, is a red its reader cannot act on.
987+
['no pin: an in-tree record over unchanged registry sources passes', mkInTree(), 0, blind],
988+
['no pin and no in-tree console is RED, naming both', mkInTree((r) => rmSync(join(r, UI_CONSOLE), { recursive: true })), 1, blind, [READ_PATHS.pin, UI_CONSOLE]],
989+
// The tree the pin deletion lands on before it regenerates: the record is
990+
// the pin producer's, whole and intact, and still not this mode's.
991+
['no pin: a pin-mode record (no inTree block) is RED with the in-tree command', mkInTree((r) => editRecord(r, ({ inTree, ...rest }) => ({
992+
objectuiSha: 'a'.repeat(40),
993+
...rest,
994+
modulesRoot: `.cache/objectui-${'a'.repeat(12)}/apps/console`,
995+
objectuiWorkspaceVersion: '0.0.0-selftest',
996+
}))), 1, blind, ['`inTree`', IN_TREE_REGENERATE]],
997+
['no pin: a staged registry source edit is RED with the in-tree command', mkInTree((r, git) => {
998+
writeFileSync(join(r, pkgDir('@object-ui/plugin-grid'), 'src', 'index.ts'), 'export const id = "moved";\n');
999+
git('add', '-A');
1000+
}), 1, blind, [IN_TREE_REGENERATE]],
1001+
['no pin: an edit the index does not hold is not a registry change', mkInTree((r) => {
1002+
writeFileSync(join(r, pkgDir('@object-ui/plugin-grid'), 'src', 'index.ts'), 'export const id = "unstaged";\n');
1003+
writeFileSync(join(r, pkgDir('@object-ui/plugin-grid'), 'src', 'untracked.ts'), 'export {};\n');
1004+
}), 0, blind],
1005+
['no pin: a test file or a package the registry does not reach is not a registry change', mkInTree((r, git) => {
1006+
mkdirSync(join(r, pkgDir('@object-ui/plugin-grid'), 'src', '__tests__'), { recursive: true });
1007+
writeFileSync(join(r, pkgDir('@object-ui/plugin-grid'), 'src', '__tests__', 'grid.ts'), 'export {};\n');
1008+
writeFileSync(join(r, pkgDir('@object-ui/types'), 'src', 'types.spec.tsx'), 'export {};\n');
1009+
writeFileSync(join(r, pkgDir('@object-ui/unrelated'), 'src', 'more.ts'), 'export {};\n');
1010+
git('add', '-A');
1011+
}), 0, blind],
1012+
['no pin: a new workspace edge into the registry is RED', mkInTree((r) => {
1013+
const p = join(r, pkgDir('@object-ui/plugin-grid'), 'package.json');
1014+
const manifest = JSON.parse(readFileSync(p, 'utf8'));
1015+
writeFileSync(p, JSON.stringify({ ...manifest, peerDependencies: { '@object-ui/unrelated': 'workspace:*' } }, null, 2));
1016+
}), 1, blind, [IN_TREE_REGENERATE]],
1017+
['no pin: a hand-edited artefact is RED', mkInTree((r) => writeFileSync(join(r, READ_PATHS.artefact), '{"components":{"flex":{"type":"flex"}}}')), 1, blind, [IN_TREE_REGENERATE]],
1018+
['no pin: the in-tree producer builds, then writes a tree this gate passes', producedInTree, 0, blind],
8271019
];
8281020

8291021
// The ledger this self-test's floor is evaluated against (#13489).
@@ -833,13 +1025,18 @@ function selfTest() {
8331025
};
8341026

8351027
let failures = 0;
836-
for (const [name, root, want, options] of cases) {
1028+
for (const [name, root, want, options, mustName = []] of cases) {
8371029
registerCase(name);
8381030
const problems = checkTree(root, options);
8391031
const got = problems.length ? 1 : 0;
840-
if (got !== want) {
1032+
const unnamed = got ? mustName.filter((text) => !problems.join('\n').includes(text)) : [];
1033+
if (got !== want || unnamed.length > 0) {
8411034
failures++;
842-
console.error(`✗ self-test: ${name} — expected ${want ? 'RED' : 'GREEN'}, got ${got ? 'RED' : 'GREEN'}`);
1035+
console.error(
1036+
got !== want
1037+
? `✗ self-test: ${name} — expected ${want ? 'RED' : 'GREEN'}, got ${got ? 'RED' : 'GREEN'}`
1038+
: `✗ self-test: ${name} — RED, but no problem names ${unnamed.map((text) => JSON.stringify(text)).join(', ')}`,
1039+
);
8431040
for (const p of problems) console.error(` ${p}`);
8441041
}
8451042
}
@@ -862,6 +1059,15 @@ function selfTest() {
8621059
declFail('a repo-ROOT file is declared without the subtree suffix — a bare single-segment literal is '
8631060
+ 'refused by hintCovers as too generic, so it would contribute no hint at all.');
8641061
}
1062+
// The digest is only a reading of what the producer enumerates while the two
1063+
// registry lists are one list. The producer refuses a mismatch on its own
1064+
// in-tree run; this holds it on every run, with or without a pin.
1065+
const producerList = /const REGISTRY_MODULES = \[([^\]]*)\];/.exec(readFileSync(join(DEFAULT_ROOT, PRODUCER), 'utf8'));
1066+
const producerModules = producerList ? [...producerList[1].matchAll(/'(@object-ui\/[a-z0-9-]+)'/g)].map((m) => m[1]) : [];
1067+
if (JSON.stringify(producerModules) !== JSON.stringify(REGISTRY_MODULES)) {
1068+
declFail(`REGISTRY_MODULES here and in ${PRODUCER} disagree (or its list is unreadable) — the in-tree digest `
1069+
+ 'would describe a different registry than the one the producer enumerates.');
1070+
}
8651071

8661072
// ── The floor: every declared row RAN, and ran its case (#13489) ───────
8671073
//
@@ -926,7 +1132,9 @@ function selfTest() {
9261132
console.log(
9271133
`✓ check-sdui-manifest self-test: ${cases.length} cases behave (green passes; absence, tamper, moved pin, emptiness, a retired-route record, ` +
9281134
'a version the pin does not declare, and an oracle that cannot answer are RED; an absent oracle is reported, not scored; ' +
929-
'the producer, run under a symlinked TMPDIR, writes a tree that passes).',
1135+
'the producer, run under a symlinked TMPDIR, writes a tree that passes; with no pin, the in-tree registry digest reds a staged ' +
1136+
'source edit or a new workspace edge and ignores tests, unreached packages and unstaged bytes, no pin and no console is RED ' +
1137+
'naming both, and the in-tree producer builds, then writes a tree that passes).',
9301138
);
9311139

9321140
return SELF_TEST_VERDICT;

0 commit comments

Comments
 (0)