Skip to content

Commit 318bdaf

Browse files
fix(scripts): the SDUI manifest generator realpaths its runner directory, so a TMPDIR behind a symlink (the macOS default) no longer fails regeneration (#21077)
Fixes #20588 Clause-②: no ## What `scripts/gen-sdui-manifest-node.mjs`, the one producer of the tracked `sdui.manifest.json`, now `realpathSync`s its `mkdtemp` runner directory before it builds the runner URL. The resolve hook compares against that URL. The pin is a new row in `scripts/check-sdui-manifest.mjs --self-test`. It copies the real generator into a fixture tree, runs it under a `TMPDIR` that is a symlink, and requires the output to pass `checkTree` with check 4 required. ## Why: the mechanism, measured on Linux The hook re-anchors a bare specifier to the modules root only when `c.parentURL === RUNNER`. Node reports a module's URL by its **realpath**, but `RUNNER` was built from the unresolved `os.tmpdir()` path. Under a `TMPDIR` reached through a symlink, the two never match. macOS's default `/var/folders/...` is such a path (`/var` is a symlink to `/private/var`). So no runner import is re-anchored, and every registry import fails. The card's macOS failure reproduces on Linux by pointing `TMPDIR` at a symlink. It was measured on the **real** objectui tree built at the pin (`db11afd4967c`, the registry closure built with `turbo run build`). The base is `6073bb96b8`, and the "unfixed" legs restore the base line through `scripts/ablation-replace.mjs`: | leg | generator | `TMPDIR` | exit | output | |---|---|---|---|---| | 1 | fixed | default (`/tmp`) | 0 | 107 components, sha256 `7c7b6b6ebcb1…`, `cmp`-identical to `sdui.manifest.json` at base | | 2 | fixed | symlink to a real dir | 0 | same bytes, `cmp`-identical | | 3 | unfixed | symlink to a real dir | **1** | 16 × `Cannot find package '@object-ui/…'`, then `the registry enumeration failed (exit 1)` (the card's signature) | | 4 | unfixed | default (`/tmp`) | 0 | same bytes (control: the defect needs the symlink) | After each leg, `sdui.manifest.json` and `scripts/sdui-manifest.record.json` were restored to HEAD. The record differed only in `generatedAt`. ⛔ Neither file is in this diff, because open PR #20990 owns the artefact. ## Premise checks - **Site unchanged.** At base, `:180` is `const runnerDir = mkdtempSync(join(tmpdir(), 'sdui-manifest-runner-'));` and `:190` is the `c.parentURL === RUNNER` test. - **Only `RUNNER` is compared.** `ANCHOR` is only handed on as `parentURL`. In a minimal copy of the hook, a modules root reached through a symlink resolves with or without the fix. Repo-wide, `git grep "parentURL ==="` has exactly one hit: this hook. `ROOT` comes from the generator's own `import.meta.url`, which Node already reports as a realpath. - **The command.** The card names `pnpm sdui:manifest`, but no such script exists in `package.json`. The documented regenerate command is `node scripts/gen-sdui-manifest-node.mjs` (AGENTS.md, `docs/releases-maintenance.md`), and that is what was run. ## The pin, and why it lives where it does The generator has no test of its own: no `--self-test`, and no suite names it. `scripts/check-sdui-manifest.mjs --self-test` is the self-test of its artefact's gate, and `lint.yml` already runs it ("SDUI manifest is present, intact and fresh at the objectui pin"). Putting the row there adds no CI step and no new harness. - **The row.** `the producer, run under a TMPDIR behind a symlink, writes a tree this gate passes`. It is declared in `SELF_TEST_BATTERIES`, and `SELF_TEST_BATTERY_FLOOR` goes from 12 to 13. - **The fixture.** A throwaway objectui commit sits at a fixture pin under `.cache/objectui-SHA12/`. It holds one stub package per `@object-ui/*` name the generator spells (read off the generator's source, so a module it gains does not red the row) and a stub adapter at the parser dist path. The fixture refuses to count as a fixture if its `TMPDIR` resolves to itself. - **The assertion.** The row expects GREEN with `requireObjectui: true` against the fixture's own objectui, so the version leg cannot pass by not running. - **dispatch-gates.** A module-scope `PRODUCER = 'scripts/gen-sdui-manifest-node.mjs'` literal makes an edit to the generator alone derive this gate. Extracted hints at base were `packages/core/package.json`, `sdui.manifest.json/**`, `scripts/sdui-manifest.record.json` and `.objectui-sha/**`. Now they also include `scripts/gen-sdui-manifest-node.mjs`. `dispatch-gates scripts/gen-sdui-manifest-node.mjs` now lists both `check-sdui-manifest` invocations as matched via that hint. **Ablation (one-time proof).** The fix was committed first (`6cb74c13c4`). Then `scripts/ablation-replace.mjs` restored the base line: anchor 1 to 0, blob `2df8285f06ae` to `7c351d47dc70`. The self-test exited **1**: the producer printed 16 × `Cannot find package '@object-ui/…'` imported from `…/tmp-real/sdui-manifest-runner-…`, and the row read RED with `sdui.manifest.json is MISSING`. The file was restored with blob == HEAD (`2df8285f06ae`) and `git diff HEAD` empty. The self-test then exited 0 with 13 cases. ## Local verification (at HEAD `6cb74c13c4`) Every command `dispatch-gates --commands` derives, with its exit code captured before any pipe: ```text node scripts/check-ci-filter-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-declaration-mirrors.mjs :: exit 0 node scripts/check-declaration-mirrors.mjs --self-test :: exit 0 node scripts/check-scripts-symbol-anchors.mjs :: exit 0 node scripts/check-scripts-symbol-anchors.mjs --self-test :: exit 0 node scripts/check-sdui-manifest.mjs :: exit 0 node scripts/check-sdui-manifest.mjs --self-test :: exit 0 node scripts/check-self-test-wired.mjs :: exit 0 node scripts/check-self-test-wired.mjs --self-test :: exit 0 node scripts/check-self-test-workflow-commands.mjs :: exit 0 node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0 node scripts/check-whole-set-label-write.mjs :: exit 0 node scripts/check-whole-set-label-write.mjs --self-test :: exit 0 node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0 pnpm check:agent-test-spelling :: exit 0 pnpm check:bash32-floor :: exit 0 pnpm check:cli-command-ids :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:entry-guard :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:parse-guard :: exit 0 pnpm check:pm-dispatch-gates :: exit 0 pnpm check:pnpm-filter-targets :: exit 0 pnpm check:ratchet-remedy-authority :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:watch-hint-literal :: exit 0 ``` Verdict lines: `✓ check-sdui-manifest self-test: 13 cases behave (…; the producer, run under a symlinked TMPDIR, writes a tree that passes).` · `check-nul-bytes: OK (scanned 9639 text file(s) …; no raw ASCII control bytes).` - **Union reconciliation:** `dispatch-gates --repo objectstack-ai/objectstack --commands` (no paths) was run after the last commit. Its verdict: `✓ dispatch-gates --ran: 31 derived famil(ies) accounted for — 31 run, 0 NOT-MEASURED (a DERIVED zero — all 31 recorded an exit code and none of them is 3).` - **eslint, a proven narrowing.** ① Population: `--print-config` resolves a config for both files. ② Count: `--format json` reports 2 files, 0 errors and 0 warnings, with `--no-inline-config`. ③ Invariance: this repo's `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`), and its local plugins are per-file. So this diff cannot move the verdict for any file it does not touch. - **Not run locally:** the `Test Core` steps that `scripts/**` schedules in `ci.yml`. They are CI's own shell (`NOT MEASURED`, declared to CI). The two value-bearing families (`check-shard-attestation`, `check-test-completeness`) take `$RUNNER_TEMP` and matrix values and have no local invocation. ## Acceptance notes - **The objectui install for the measurement needed a flag.** The objectui pin's `.npmrc` sets `engine-strict=true`, and `jsdom@30.0.1` requires node `^22.22.2`. This container runs `22.22.0`, so the install used `--config.engine-strict=false`. jsdom is a test dependency and is outside the generator's import closure. This is a local environment fact only; nothing was filed. - **The changeset label.** Root `scripts/**` publishes nothing, so this PR takes the `skip-changeset` label. --- _Generated by [Claude Code](https://claude.ai/code/session_01JAhu8u8QfBvRjVZDox7CP9)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 88b484e commit 318bdaf

2 files changed

Lines changed: 131 additions & 6 deletions

File tree

‎scripts/check-sdui-manifest.mjs‎

Lines changed: 120 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,34 @@
8787
* asserted before any verdict; a missing one exits 1 naming which. No `⚠` +
8888
* exit 0 — and no line claiming a leg that did not run, which is the same
8989
* failure with better manners.
90+
*
91+
* ## The one place the producer IS run: the self-test
92+
*
93+
* No production run here executes the generator. The self-test runs it once,
94+
* over a fixture: the real `PRODUCER` file, copied byte for byte into a
95+
* throwaway repo root beside a stub registry, under a TMPDIR reached through a
96+
* symlink, with its output then held to `checkTree`. A symlinked temp base is
97+
* the shape of macOS's default TMPDIR (`/var` -> `/private/var`), and it breaks
98+
* the generator's one path comparison: its resolve hook re-anchors a runner
99+
* import only when the import's parent URL equals the runner's, and Node
100+
* reports that URL by its realpath. A Linux runner's own TMPDIR is no symlink,
101+
* so without this row no CI run reaches that comparison.
90102
*/
91103

92-
import { execFileSync } from 'node:child_process';
104+
import { execFileSync, spawnSync } from 'node:child_process';
93105
import { createHash } from 'node:crypto';
94-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
106+
import {
107+
copyFileSync,
108+
existsSync,
109+
mkdirSync,
110+
mkdtempSync,
111+
readFileSync,
112+
realpathSync,
113+
renameSync,
114+
rmSync,
115+
symlinkSync,
116+
writeFileSync,
117+
} from 'node:fs';
95118
import { tmpdir } from 'node:os';
96119
import { dirname, join, sep } from 'node:path';
97120
import { fileURLToPath } from 'node:url';
@@ -132,6 +155,14 @@ const OBJECTUI_VERSION_FILE = 'packages/core/package.json';
132155
const RECORD_FIELDS = ['objectuiSha', 'source', 'modulesRoot', 'objectuiWorkspaceVersion', 'sha256', 'components'];
133156
const RECORD_SOURCE = 'built-tree';
134157

158+
/**
159+
* The one producer, repo-relative. Read by the SELF-TEST only — copied into a
160+
* fixture tree and run there (see the header) — never by `checkTree`, so it is
161+
* not a `READ_PATHS` entry. Spelled as one literal at module scope so that
162+
* `scripts/pm/dispatch-gates.mjs` derives this gate for an edit to the producer.
163+
*/
164+
const PRODUCER = 'scripts/gen-sdui-manifest-node.mjs';
165+
135166
/**
136167
* The population this gate reads, declared for `scripts/pm/dispatch-gates.mjs`.
137168
*
@@ -393,6 +424,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
393424
'an unreachable objectui checkout is not a version verdict': 1,
394425
'--require-objectui turns an unreachable checkout RED': 1,
395426
'a pinned commit with no packages/core/package.json is RED': 1,
427+
'the producer, run under a TMPDIR behind a symlink, writes a tree this gate passes': 1,
396428
});
397429

398430
// DELETING an entry silences that battery's floor exactly as effectively as
@@ -401,7 +433,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
401433
// the literal above, so the roster falls below this number; the table
402434
// cross-check in the floor block is the other half, and names WHICH label
403435
// collided.
404-
const SELF_TEST_BATTERY_FLOOR = 12;
436+
const SELF_TEST_BATTERY_FLOOR = 13;
405437

406438
function selfTest() {
407439
const mk = (mutate, { pin = 'a'.repeat(40), version = '0.0.0-selftest' } = {}) => {
@@ -469,6 +501,81 @@ function selfTest() {
469501
const disagreeing = mkObjectui({ version: '9.9.9-not-the-recorded-one' });
470502
const noVersionFile = mkObjectui({ omitVersionFile: true });
471503

504+
/**
505+
* The PRODUCER's own output, written under a TMPDIR reached through a
506+
* symlink (see the header for why that shape).
507+
*
508+
* The fixture is a built tree as the producer expects one: a throwaway
509+
* objectui commit AT the pin under `.cache/objectui-<SHA12>/`, with one stub
510+
* per `@object-ui/*` name the producer spells, under
511+
* `apps/console/node_modules/`, and a stub adapter at the parser dist path.
512+
* Every one of those names is a BARE import issued from the runner, so each
513+
* resolves only if the hook re-anchors it. A producer whose runner URL is
514+
* built from the unresolved temp path exits 1 with `Cannot find package`,
515+
* writes nothing, and the row reads RED (missing artefact) with the
516+
* producer's stderr printed above it.
517+
*
518+
* The stub names are read off the producer's source, not restated here, so
519+
* a registry module it gains cannot red this row for a stub nobody added.
520+
*/
521+
const mkProduced = () => {
522+
const base = mkdtempSync(join(tmpdir(), 'sdui-manifest-producer-'));
523+
const realTmp = join(base, 'tmp-real');
524+
const linkTmp = join(base, 'tmp-link');
525+
mkdirSync(realTmp);
526+
symlinkSync(realTmp, linkTmp, 'dir');
527+
const root = join(base, 'repo');
528+
mkdirSync(join(root, 'scripts'), { recursive: true });
529+
// Only a fixture while the TMPDIR it hands over is NOT its own realpath:
530+
// a plain directory here would turn the row into a Linux-default run that
531+
// passes with or without the fix.
532+
if (realpathSync(linkTmp) === linkTmp) {
533+
console.error(` fixture: ${linkTmp} resolves to itself, so it exercises no symlink — the producer was not run.`);
534+
return { root, objectuiRoot: NO_OBJECTUI };
535+
}
536+
537+
const producerSource = readFileSync(join(DEFAULT_ROOT, PRODUCER), 'utf8');
538+
copyFileSync(join(DEFAULT_ROOT, PRODUCER), join(root, PRODUCER));
539+
const parserDist = join(root, 'packages', 'sdui-parser', 'dist');
540+
mkdirSync(parserDist, { recursive: true });
541+
writeFileSync(
542+
join(parserDist, 'index.mjs'),
543+
'export const manifestFromConfigs = (configs) => ({ components: Object.fromEntries(configs.map((c) => [c.type, { type: c.type, inputs: [] }])) });\n',
544+
);
545+
546+
const objectui = mkObjectui({ version: '0.0.0-selftest' });
547+
const tree = join(root, '.cache', `objectui-${objectui.sha.slice(0, 12)}`);
548+
mkdirSync(dirname(tree), { recursive: true });
549+
renameSync(objectui.dir, tree);
550+
writeFileSync(join(root, '.objectui-sha'), objectui.sha + '\n');
551+
const names = new Set(['@object-ui/core', ...[...producerSource.matchAll(/'(@object-ui\/[a-z0-9-]+)'/g)].map((m) => m[1])]);
552+
for (const name of names) {
553+
const pkg = join(tree, 'apps', 'console', 'node_modules', ...name.split('/'));
554+
mkdirSync(join(pkg, 'dist'), { recursive: true });
555+
writeFileSync(
556+
join(pkg, 'package.json'),
557+
JSON.stringify({ name, version: '0.0.0-selftest', type: 'module', exports: { '.': { import: './dist/index.js' } } }),
558+
);
559+
writeFileSync(
560+
join(pkg, 'dist', 'index.js'),
561+
name === '@object-ui/core'
562+
? "export const ComponentRegistry = { getPublicConfigs: () => [{ type: 'selftest_flex' }] };\n"
563+
: 'export {};\n',
564+
);
565+
}
566+
567+
const run = spawnSync(process.execPath, [join(root, PRODUCER)], {
568+
encoding: 'utf8',
569+
env: { ...process.env, TMPDIR: linkTmp },
570+
});
571+
if (run.status !== 0) {
572+
console.error(` the producer under TMPDIR=${linkTmp} (a symlink) exited ${run.status ?? run.signal}:`);
573+
for (const line of String(run.stderr ?? run.error ?? '').trim().split('\n')) console.error(` ${line}`);
574+
}
575+
return { root, objectuiRoot: tree };
576+
};
577+
const produced = mkProduced();
578+
472579
const cases = [
473580
['green fixture passes', mk(), 0, blind],
474581
['missing artefact is RED', mk((r) => rmSync(join(r, 'sdui.manifest.json'))), 1, blind],
@@ -505,6 +612,14 @@ function selfTest() {
505612
['an unreachable objectui checkout is not a version verdict', mk(undefined, { pin: disagreeing.sha, version: '0.0.0-selftest' }), 0, blind],
506613
['--require-objectui turns an unreachable checkout RED', mk(undefined, { pin: disagreeing.sha, version: '0.0.0-selftest' }), 1, { objectuiRoot: NO_OBJECTUI, requireObjectui: true }],
507614
['a pinned commit with no packages/core/package.json is RED', mk(undefined, { pin: noVersionFile.sha, version: '0.0.0-selftest' }), 1, { objectuiRoot: noVersionFile.dir }],
615+
616+
// ── The producer, under a symlinked TMPDIR ────────────────────────────
617+
//
618+
// GREEN is the assertion: the generator ran to completion and its artefact
619+
// and record pass all four checks, check 4 included — `requireObjectui`
620+
// with the fixture's own objectui as the oracle, so the version leg cannot
621+
// read `unreachable` and pass by not running.
622+
['the producer, run under a TMPDIR behind a symlink, writes a tree this gate passes', produced.root, 0, { objectuiRoot: produced.objectuiRoot, requireObjectui: true }],
508623
];
509624

510625
// The ledger this self-test's floor is evaluated against (#13489).
@@ -606,7 +721,8 @@ function selfTest() {
606721
}
607722
console.log(
608723
`✓ check-sdui-manifest self-test: ${cases.length} cases behave (green passes; absence, tamper, moved pin, emptiness, a retired-route record, ` +
609-
'a version the pin does not declare, and an oracle that cannot answer are RED; an absent oracle is reported, not scored).',
724+
'a version the pin does not declare, and an oracle that cannot answer are RED; an absent oracle is reported, not scored; ' +
725+
'the producer, run under a symlinked TMPDIR, writes a tree that passes).',
610726
);
611727

612728
return SELF_TEST_VERDICT;

‎scripts/gen-sdui-manifest-node.mjs‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767

6868
import { execFileSync } from 'node:child_process';
6969
import { createHash } from 'node:crypto';
70-
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
70+
import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
7171
import { tmpdir } from 'node:os';
7272
import { dirname, join } from 'node:path';
7373
import { pathToFileURL, fileURLToPath } from 'node:url';
@@ -177,7 +177,16 @@ if (existsSync(RECORD_PATH)) {
177177
// would be for a file sitting in `apps/console/`, and the build tree gets no
178178
// file written into it. The one other accommodation plain Node needs: `.css`
179179
// side-effect imports (plugin-dashboard, plugin-map) resolve to an empty module.
180-
const runnerDir = mkdtempSync(join(tmpdir(), 'sdui-manifest-runner-'));
180+
//
181+
// The hook re-anchors only when `parentURL` EQUALS `RUNNER`, and Node reports a
182+
// module's URL by its REALPATH. So the directory is resolved before any URL is
183+
// built from it: a temp base reached through a symlink (macOS's default TMPDIR,
184+
// `/var` -> `/private/var`) otherwise leaves `RUNNER` naming a path Node never
185+
// reports, nothing is re-anchored, and every registry import fails with
186+
// `Cannot find package`. `ANCHOR` is only handed on as a parent and never
187+
// compared, so it needs no resolving. Pinned with a symlinked TMPDIR by
188+
// `scripts/check-sdui-manifest.mjs --self-test`.
189+
const runnerDir = realpathSync(mkdtempSync(join(tmpdir(), 'sdui-manifest-runner-')));
181190
const runnerPath = join(runnerDir, 'sdui-manifest-runner.mjs');
182191
const runnerUrl = pathToFileURL(runnerPath).href;
183192
const anchorUrl = pathToFileURL(join(MODULES_ROOT, 'package.json')).href;

0 commit comments

Comments
 (0)