From 678a76c469c9f819a0ded1610417ee112a91c7ab Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 12:30:48 +0000 Subject: [PATCH 1/5] devx(sdui): one manifest record per ui registry package (check + generator) Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01SMJGPXxFBpbgAgXdnsXKVS --- scripts/check-sdui-manifest.mjs | 431 +++++++++++++++++++++++------ scripts/gen-sdui-manifest-node.mjs | 132 ++++++--- 2 files changed, 442 insertions(+), 121 deletions(-) diff --git a/scripts/check-sdui-manifest.mjs b/scripts/check-sdui-manifest.mjs index ff45b37773b..41d411b0f8b 100644 --- a/scripts/check-sdui-manifest.mjs +++ b/scripts/check-sdui-manifest.mjs @@ -65,13 +65,31 @@ * 1–2. unchanged — the same shape and tamper checks, against a record keyed * to the in-tree producer (`IN_TREE_RECORD_FIELDS`: the pin-keyed * fields are not required, the `inTree` block is). - * 3. STALENESS is a SOURCE DIGEST — `registrySourceDigest` over the - * registry packages' `src/` as the index holds them must equal the - * record's `inTree` reading. A registry source edit without - * regeneration reds HERE, and the red prints the one command that - * clears it: the generator, which builds the registry from this tree - * and regenerates. Nothing is built in this gate — the digest is a + * 3. STALENESS is a SOURCE DIGEST PER PACKAGE (#26051) — + * `registrySourceDigests` reads each registry package's `src/` as the + * index holds them, and each package's digest must equal its own + * one-line record under `scripts/sdui-manifest.sources/`, which also + * names the artefact sha256 it was verified against. A registry source + * edit without regeneration reds HERE naming the package, and so does a + * package the registry reaches with no record (a new package or a new + * dependency edge), a record the registry no longer reaches (a removed + * package or edge), and a record verified against another artefact. + * Each red prints the one command that clears it: the generator, which + * builds the registry from this tree, regenerates, and re-records only + * what moved. Nothing is built in this gate — the digest is a * `git ls-files` over the index, with no dist and no node_modules read. + * + * Why one file per package, one line per file: with one digest over + * the whole registry, any two ui PRs that touched registry packages + * conflicted on that line in the merge queue. Now two PRs on different + * packages touch different files. Two PRs on one package touch the same + * line, and so does a PR that moves the artefact (it re-records every + * package against the new sha256): those still conflict, on purpose, + * because only a regeneration on the merged tree can say what the + * manifest of the combination is. What is NOT forced is the case where + * two PRs on different packages each leave the artefact unchanged and + * their combination would not; the next regeneration of any registry + * package sees the combination. * 4. none. The in-tree `@object-ui/core` version moves with every release * of the one fixed version group while the registry does not, so the * record carries it as provenance and the success line prints it. @@ -158,10 +176,17 @@ const UI_CONSOLE = 'ui/apps/console'; /** Where the `@object-ui/*` workspace packages live in the merged layout. */ const UI_PACKAGES = 'ui/packages'; +/** + * Where the in-tree producer writes one record per registry package (#26051), + * repo-relative and POSIX-separated: this string is also what the record's + * `inTree.registryRecords` says. + */ +export const REGISTRY_RECORDS = 'scripts/sdui-manifest.sources'; + /** * What `checkTree` reads, repo-relative: three files with a pin; without one, - * the same artefact and record plus the in-tree console's manifest (presence) - * and the registry packages (the digest). + * the same artefact and record plus the in-tree console's manifest (presence), + * the registry packages (the digests) and their per-package records. */ const READ_PATHS = { artefact: 'sdui.manifest.json', @@ -169,6 +194,7 @@ const READ_PATHS = { pin: '.objectui-sha', console: join(UI_CONSOLE, 'package.json'), packages: UI_PACKAGES, + sources: join(...REGISTRY_RECORDS.split('/')), }; /** @@ -203,7 +229,14 @@ const RECORD_SOURCE = 'built-tree'; * is byte-identical — see the generator. */ const IN_TREE_RECORD_FIELDS = ['source', 'sha256', 'components', 'inTree']; -const IN_TREE_FIELDS = ['modulesRoot', 'objectuiWorkspaceVersion', 'registryPackages', 'registrySources', 'registrySourcesSha256']; +const IN_TREE_FIELDS = ['modulesRoot', 'objectuiWorkspaceVersion', 'registryRecords']; + +/** + * The single-digest layout's `inTree` fields, retired by #26051: one digest + * over every registry package, which every ui PR that touched one re-recorded. + * A record still carrying them is RED, never read as the same claim. + */ +const RETIRED_IN_TREE_FIELDS = ['registryPackages', 'registrySources', 'registrySourcesSha256']; /** The one command that clears each mode's freshness reds. */ const PIN_REGENERATE = 'pnpm objectui:build && node scripts/gen-sdui-manifest-node.mjs'; @@ -264,7 +297,8 @@ const PRODUCER_IMPORTS = ['scripts/git-env.mjs', 'scripts/invoked-as.mjs']; * Without a pin it also reads the in-tree console's manifest and the registry * packages under `ui/packages/` (#25712) — the subtree the digest walks, so a * UI package edit derives this gate; on a tree with no `ui/` both hints match - * nothing and only display. + * nothing and only display. The per-package records (#26051) are a directory, + * declared with the subtree suffix. * * The self-test derives the coupling from `READ_PATHS` — the same object * `checkTree` reads — so a moved or added read reds here rather than leaving the @@ -276,6 +310,7 @@ const ROOT_WATCH_HINTS = [ '.objectui-sha/**', 'ui/apps/console/package.json', 'ui/packages/**', + 'scripts/sdui-manifest.sources/**', ]; /** @@ -481,12 +516,12 @@ function checkArtefactAndRecord(root, problems, { fields, producerReads, regener /** * Gate a tree that has no pin (the merged layout, #25712). Checks 1–2 as - * above, keyed to the in-tree producer's record; check 3 becomes the registry - * sources' digest against `inTree.registrySourcesSha256`. There is no check 4 - * here: the in-tree `@object-ui/core` version moves with every release of the - * fixed group while the registry does not, so the record carries it as - * provenance and the success line prints both. No pin and no in-tree console - * is RED, naming both. + * above, keyed to the in-tree producer's record; check 3 becomes each registry + * package's source digest against its own record under `REGISTRY_RECORDS` + * (#26051). There is no check 4 here: the in-tree `@object-ui/core` version + * moves with every release of the fixed group while the registry does not, so + * the record carries it as provenance and the success line prints both. No pin + * and no in-tree console is RED, naming both. */ function checkTreeWithoutPin(root) { const problems = []; @@ -506,33 +541,88 @@ function checkTreeWithoutPin(root) { if (read === null) return problems; const { record } = read; - const missing = IN_TREE_FIELDS.filter((field) => record.inTree?.[field] === undefined); - if (missing.length > 0 || record.inTree?.modulesRoot !== UI_CONSOLE) { - for (const field of missing) problems.push(`record \`inTree\` is missing \`${field}\`.`); - if (!missing.includes('modulesRoot') && record.inTree?.modulesRoot !== UI_CONSOLE) { - problems.push(`record \`inTree.modulesRoot\` is ${JSON.stringify(record.inTree?.modulesRoot)}, not ${JSON.stringify(UI_CONSOLE)} — the in-tree producer reads only that.`); - } - problems.push(` Regenerate in-tree — one command builds what the generator reads and re-records:\n ${IN_TREE_REGENERATE}`); + const inTree = record.inTree ?? {}; + const missing = IN_TREE_FIELDS.filter((field) => inTree[field] === undefined); + const retired = RETIRED_IN_TREE_FIELDS.filter((field) => inTree[field] !== undefined); + const shape = []; + for (const field of missing) shape.push(`record \`inTree\` is missing \`${field}\`.`); + if (retired.length > 0) { + shape.push( + `record \`inTree\` still carries ${retired.map((field) => `\`${field}\``).join(', ')} — the retired single-digest layout. Each\n` + + ` registry package now has its own record under ${REGISTRY_RECORDS}/, so ui PRs that touch different packages\n` + + ' do not conflict on one shared line.', + ); + } + if (!missing.includes('modulesRoot') && inTree.modulesRoot !== UI_CONSOLE) { + shape.push(`record \`inTree.modulesRoot\` is ${JSON.stringify(inTree.modulesRoot)}, not ${JSON.stringify(UI_CONSOLE)} — the in-tree producer reads only that.`); + } + if (!missing.includes('registryRecords') && inTree.registryRecords !== REGISTRY_RECORDS) { + shape.push(`record \`inTree.registryRecords\` is ${JSON.stringify(inTree.registryRecords)}, not ${JSON.stringify(REGISTRY_RECORDS)} — the in-tree producer writes only there.`); + } + if (shape.length > 0) { + problems.push(...shape, ` Regenerate in-tree — one command builds what the generator reads and re-records:\n ${IN_TREE_REGENERATE}`); return problems; } - const now = registrySourceDigest(root); + const now = registrySourceDigests(root); if (now.problem) { problems.push(`cannot digest the registry sources under ${UI_TREE}/: ${now.problem}`); return problems; } - const was = record.inTree; - if ( - now.sha256 !== was.registrySourcesSha256 - || now.files !== was.registrySources - || JSON.stringify(now.packages) !== JSON.stringify(was.registryPackages) - ) { + const recorded = readRegistryRecords(root); + if (recorded.problem) { + problems.push(`${recorded.problem}\n Regenerate in-tree — one command builds what the generator reads and re-records:\n ${IN_TREE_REGENERATE}`); + return problems; + } + problems.push(...recorded.malformed); + + const stale = []; + const unrecorded = []; + const otherArtefact = []; + for (const pkg of now.packages) { + const was = recorded.records.get(pkg.dir); + if (!was) unrecorded.push(pkg); + else if (was.sha256 !== pkg.sha256 || was.sources !== pkg.files) stale.push({ pkg, was }); + else if (was.manifestSha256 !== record.sha256) otherArtefact.push({ pkg, was }); + } + const reached = new Set(now.packages.map(({ dir }) => dir)); + const unreached = [...recorded.records.keys()].filter((dir) => !reached.has(dir)); + + if (stale.length > 0) { problems.push( - `the console registry sources under ${UI_TREE}/ have changed since sdui.manifest.json was generated:\n` + - ` now ${now.packages.length} package(s), ${now.files} file(s), sha256 ${now.sha256.slice(0, 12)}…\n` + - ` recorded ${Array.isArray(was.registryPackages) ? was.registryPackages.length : '?'} package(s), ${was.registrySources} file(s), sha256 ${String(was.registrySourcesSha256).slice(0, 12)}…\n` + - ' A registry change can change which components and inputs the shipped console declares, so the manifest\n' + - ' must follow it (同步一次就烂). One command builds what the generator reads and regenerates:\n' + + `registry sources under ${UI_TREE}/ have changed since sdui.manifest.json was generated, in ${stale.length} package(s):\n` + + stale + .map(({ pkg, was }) => ` ${pkg.dir}: now ${pkg.files} file(s) sha256 ${pkg.sha256.slice(0, 12)}…, recorded ${was.sources} file(s) sha256 ${String(was.sha256).slice(0, 12)}…`) + .join('\n') + + '\n A registry change can change which components and inputs the shipped console declares, so the manifest\n' + + ' must follow it (同步一次就烂).', + ); + } + if (unrecorded.length > 0) { + problems.push( + `the registry reaches ${unrecorded.length} package(s) with no record under ${REGISTRY_RECORDS}/ — a new package, a new\n` + + ' dependency edge into the registry, or a deleted record:\n' + + unrecorded.map((pkg) => ` ${pkg.dir} (${pkg.files} source file(s))`).join('\n'), + ); + } + if (unreached.length > 0) { + problems.push( + `${unreached.length} record(s) under ${REGISTRY_RECORDS}/ name a package the registry no longer reaches — a removed\n` + + ' package or a dropped dependency edge:\n' + + unreached.map((dir) => ` ${registryRecordPath(dir)} (${dir})`).join('\n'), + ); + } + if (otherArtefact.length > 0) { + problems.push( + `${otherArtefact.length} package record(s) were verified against a different sdui.manifest.json than the committed one\n` + + ` (sha256 ${String(record.sha256).slice(0, 12)}…):\n` + + otherArtefact.map(({ pkg, was }) => ` ${pkg.dir}: verified against ${String(was.manifestSha256).slice(0, 12)}…`).join('\n') + + '\n Two branches regenerated on different bases, and no run has seen their combination.', + ); + } + if (stale.length + unrecorded.length + unreached.length + otherArtefact.length + recorded.malformed.length > 0) { + problems.push( + ' One command builds what the generator reads, regenerates, and re-records only the packages that moved:\n' + ` ${IN_TREE_REGENERATE}`, ); } @@ -543,21 +633,26 @@ function checkTreeWithoutPin(root) { const TEST_SOURCE = /(^|\/)(__tests__|__mocks__)\/|\.(test|spec)\.[cm]?[jt]sx?$/; /** - * The in-tree freshness oracle: one sha256 over the registry's SOURCES as git - * has them. Which packages: every `@object-ui/*` workspace package under - * `ui/packages/` that `@object-ui/core` and the generator's `REGISTRY_MODULES` - * reach through `dependencies`, `peerDependencies` or `optionalDependencies` - * naming another member. Which files: each one's `src/`, tests excluded - * (`TEST_SOURCE`). Which bytes: the INDEX — `git ls-files -s` gives the - * path-sorted list and each file's blob id, so the reading is the committed - * content, never a working-tree edit, an untracked file or a build output. + * The in-tree freshness oracle: one sha256 PER REGISTRY PACKAGE over its + * sources as git has them (#26051). Which packages: every `@object-ui/*` + * workspace package under `ui/packages/` that `@object-ui/core` and the + * generator's `REGISTRY_MODULES` reach through `dependencies`, + * `peerDependencies` or `optionalDependencies` naming another member. Which + * files: each one's own `src/`, tests excluded (`TEST_SOURCE`) — never its + * dependencies': an edit anywhere in the reached set still moves exactly one + * package's digest, so detection is the same as one digest over the whole set, + * and an edit to a low-level package (`core`, `types`, `react`) moves one + * record rather than every dependent's. Which bytes: the INDEX — `git ls-files + * -s` gives the path-sorted list and each file's blob id, so the reading is the + * committed content, never a working-tree edit, an untracked file or a build + * output. * - * `{ packages, files, sha256 }`, or `{ problem }` naming what could not be - * read — a registry module that is not a package here, an unreadable manifest, - * a failed listing, or an empty one (zero sources is a broken read, not a - * fresh one). + * `{ packages: [{ dir, files, sha256 }] }` in `dir` order, or `{ problem }` + * naming what could not be read — a registry module that is not a package + * here, an unreadable manifest, a failed listing, or a reached package with no + * source (zero sources is a broken read, not a fresh one). */ -export function registrySourceDigest(root) { +export function registrySourceDigests(root) { const base = join(root, UI_PACKAGES); if (!existsSync(base)) return { problem: `${UI_PACKAGES}/ does not exist` }; const byName = new Map(); @@ -587,11 +682,11 @@ export function registrySourceDigest(root) { for (const dep of Object.keys(manifest[field] ?? {})) if (byName.has(dep)) queue.push(dep); } } - const packages = [...reached].map((name) => byName.get(name).dir).sort(); + const dirs = [...reached].map((name) => byName.get(name).dir).sort(); let listing; try { - listing = execFileSync('git', ['-C', root, 'ls-files', '-s', '-z', '--', ...packages.map((dir) => `${dir}/src`)], { + listing = execFileSync('git', ['-C', root, 'ls-files', '-s', '-z', '--', ...dirs.map((dir) => `${dir}/src`)], { // #16644: `-C root` alone names the index read — the self-test hands this // mkdtemp fixtures, and an inherited GIT_DIR / GIT_INDEX_FILE outranks it. env: gitFreeEnv(), @@ -600,7 +695,7 @@ export function registrySourceDigest(root) { maxBuffer: 64 * 1024 * 1024, }); } catch (e) { - return { problem: `git ls-files over ${packages.length} package src/ trees failed: ${String(e?.stderr || e?.message || e).trim().split('\n')[0]}` }; + return { problem: `git ls-files over ${dirs.length} package src/ trees failed: ${String(e?.stderr || e?.message || e).trim().split('\n')[0]}` }; } const entries = listing .split('\0') @@ -610,12 +705,79 @@ export function registrySourceDigest(root) { return { path: line.slice(tab + 1), blob: line.slice(0, tab).split(' ')[1] }; }) .filter(({ path }) => !TEST_SOURCE.test(path)); - if (entries.length === 0) { - return { problem: `git lists no registry source under the ${packages.length} package src/ trees — zero sources is a broken read, not a fresh one` }; + const packages = []; + for (const dir of dirs) { + const own = entries.filter(({ path }) => path.startsWith(`${dir}/src/`)); + if (own.length === 0) { + return { problem: `git lists no source under ${dir}/src — zero sources is a broken read, not a fresh one` }; + } + const hash = createHash('sha256'); + for (const { path, blob } of own) hash.update(`${blob} ${path}\n`); + packages.push({ dir, files: own.length, sha256: hash.digest('hex') }); + } + return { packages }; +} + +/** A package's record file, repo-relative: its directory's basename under `REGISTRY_RECORDS`. */ +export function registryRecordPath(dir) { + return `${REGISTRY_RECORDS}/${dir.slice(dir.lastIndexOf('/') + 1)}.json`; +} + +/** + * The one form a package record takes: ONE line of JSON, keys in this order, + * plus a newline. One line is the point — two branches that re-record the same + * package differently always touch the same line, so git reports a conflict + * instead of merging a digest from one side with an artefact sha256 from the + * other. + */ +export function registryRecordLine({ dir, files, sha256 }, manifestSha256) { + return `${JSON.stringify({ package: dir, sources: files, sha256, manifestSha256 })}\n`; +} + +/** + * Read every record under `REGISTRY_RECORDS`. `{ problem }` when the directory + * is absent; otherwise `{ records, malformed }` — `records` maps a package dir + * to its parsed record, `malformed` lists each entry that is not one package + * record in the producer's one-line form (it is left out of `records`, so its + * package also reads as unrecorded). + */ +export function readRegistryRecords(root) { + const dir = join(root, ...REGISTRY_RECORDS.split('/')); + if (!existsSync(dir)) { + return { problem: `${REGISTRY_RECORDS}/ is MISSING — no registry package has a record, so no source can be judged fresh.` }; } - const hash = createHash('sha256'); - for (const { path, blob } of entries) hash.update(`${blob} ${path}\n`); - return { packages, files: entries.length, sha256: hash.digest('hex') }; + const records = new Map(); + const malformed = []; + for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1))) { + const where = `${REGISTRY_RECORDS}/${entry.name}`; + if (!entry.isFile() || !entry.name.endsWith('.json')) { + malformed.push(`${where} is not a package record (only one .json file per registry package lives there).`); + continue; + } + const raw = readFileSync(join(dir, entry.name), 'utf8'); + let parsed; + try { + parsed = JSON.parse(raw); + } catch (e) { + malformed.push(`${where} does not parse: ${e.message}`); + continue; + } + const pkgDir = `${UI_PACKAGES}/${entry.name.slice(0, -'.json'.length)}`; + if ( + parsed?.package !== pkgDir + || !Number.isInteger(parsed.sources) + || typeof parsed.sha256 !== 'string' + || typeof parsed.manifestSha256 !== 'string' + || raw !== registryRecordLine({ dir: pkgDir, files: parsed.sources, sha256: parsed.sha256 }, parsed.manifestSha256) + ) { + malformed.push( + `${where} is not ${pkgDir}'s record in the producer's one-line form ({"package","sources","sha256","manifestSha256"}).`, + ); + continue; + } + records.set(pkgDir, parsed); + } + return { records, malformed }; } // Returned by `selfTest()` only after its verdict is printed. The dispatch @@ -667,12 +829,19 @@ const SELF_TEST_BATTERIES = Object.freeze({ 'no pin: an in-tree record over unchanged registry sources passes': 1, 'no pin and no in-tree console is RED, naming both': 1, 'no pin: a pin-mode record (no inTree block) is RED with the in-tree command': 1, - 'no pin: a staged registry source edit is RED with the in-tree command': 1, + 'no pin: a staged edit to one registry package is RED naming that package alone': 1, 'no pin: an edit the index does not hold is not a registry change': 1, 'no pin: a test file or a package the registry does not reach is not a registry change': 1, 'no pin: a new workspace edge into the registry is RED': 1, + 'no pin: a new package the registry reaches is RED naming it': 1, + 'no pin: a removed registry package is RED naming its record': 1, + 'no pin: a deleted package record is RED naming the package': 1, + 'no pin: a package record verified against another artefact is RED': 1, + 'no pin: a package record out of its one-line form is RED': 1, + 'no pin: a record from the single-digest layout is RED with the in-tree command': 1, 'no pin: a hand-edited artefact is RED': 1, 'no pin: the in-tree producer builds, then writes a tree this gate passes': 1, + 'no pin: the producer re-records only the package whose sources moved': 1, }); // DELETING an entry silences that battery's floor exactly as effectively as @@ -681,7 +850,7 @@ const SELF_TEST_BATTERIES = Object.freeze({ // the literal above, so the roster falls below this number; the table // cross-check in the floor block is the other half, and names WHICH label // collided. -const SELF_TEST_BATTERY_FLOOR = 22; +const SELF_TEST_BATTERY_FLOOR = 29; function selfTest() { const mk = (mutate, { pin = 'a'.repeat(40), version = '0.0.0-selftest' } = {}) => { @@ -858,23 +1027,20 @@ function selfTest() { const { root, git } = mkUiSources('sdui-manifest-in-tree-'); const manifest = { components: { flex: { type: 'flex', inputs: [] } } }; const raw = JSON.stringify(manifest, null, 2); - mkdirSync(join(root, 'scripts'), { recursive: true }); + mkdirSync(join(root, READ_PATHS.sources), { recursive: true }); writeFileSync(join(root, READ_PATHS.artefact), raw); - const digest = registrySourceDigest(root); + const sha256 = createHash('sha256').update(raw).digest('hex'); + for (const pkg of registrySourceDigests(root).packages) { + writeFileSync(join(root, registryRecordPath(pkg.dir)), registryRecordLine(pkg, sha256)); + } writeFileSync( join(root, READ_PATHS.record), JSON.stringify( { source: RECORD_SOURCE, - sha256: createHash('sha256').update(raw).digest('hex'), + sha256, components: 1, - inTree: { - modulesRoot: UI_CONSOLE, - objectuiWorkspaceVersion: '0.0.0-selftest', - registryPackages: digest.packages, - registrySources: digest.files, - registrySourcesSha256: digest.sha256, - }, + inTree: { modulesRoot: UI_CONSOLE, objectuiWorkspaceVersion: '0.0.0-selftest', registryRecords: REGISTRY_RECORDS }, }, null, 2, @@ -887,6 +1053,7 @@ function selfTest() { const p = join(root, READ_PATHS.record); writeFileSync(p, JSON.stringify(edit(JSON.parse(readFileSync(p, 'utf8'))), null, 2)); }; + const packageRecord = (root, name) => join(root, registryRecordPath(pkgDir(name).split(sep).join('/'))); /** * The PRODUCER's in-tree run (no pin): the real generator, this gate and the @@ -895,9 +1062,13 @@ function selfTest() { * `@object-ui/core`'s dist into the console, so a producer that skipped its * build exits 1 on the missing dist and the row reads RED (missing artefact) * with the producer's stderr above it. + * + * With `rerunAfter`, the producer runs a second time after that edit, and + * the returned `moved` lists every path under `scripts/` whose bytes the + * second run changed — the re-record row's reading. */ - const mkProducedInTree = () => { - const { root } = mkUiSources('sdui-manifest-in-tree-producer-'); + const mkProducedInTree = ({ rerunAfter } = {}) => { + const { root, git } = mkUiSources('sdui-manifest-in-tree-producer-'); mkdirSync(join(root, 'scripts'), { recursive: true }); for (const file of [PRODUCER, 'scripts/check-sdui-manifest.mjs', ...PRODUCER_IMPORTS]) { copyFileSync(join(DEFAULT_ROOT, file), join(root, file)); @@ -925,15 +1096,48 @@ function selfTest() { `mkdir -p '${coreDist}' && printf '%s\\n' "export const ComponentRegistry = { getPublicConfigs: () => [{ type: 'selftest_flex' }] };" > '${coreDist}/index.js'\n`, { mode: 0o755 }, ); - const run = spawnSync(process.execPath, [join(root, PRODUCER)], { encoding: 'utf8', env: gitFreeEnv() }); - if (run.status !== 0) { - console.error(` the in-tree producer exited ${run.status ?? run.signal}:`); - for (const line of String(run.stderr ?? run.error ?? '').trim().split('\n')) console.error(` ${line}`); - } - return root; + const produce = () => { + const run = spawnSync(process.execPath, [join(root, PRODUCER)], { encoding: 'utf8', env: gitFreeEnv() }); + if (run.status !== 0) { + console.error(` the in-tree producer exited ${run.status ?? run.signal}:`); + for (const line of String(run.stderr ?? run.error ?? '').trim().split('\n')) console.error(` ${line}`); + } + }; + produce(); + if (!rerunAfter) return { root }; + const written = () => { + const files = [READ_PATHS.artefact, READ_PATHS.record]; + if (existsSync(join(root, READ_PATHS.sources))) { + for (const name of readdirSync(join(root, READ_PATHS.sources)).sort()) files.push(join(READ_PATHS.sources, name)); + } + return new Map(files.map((file) => [file.split(sep).join('/'), existsSync(join(root, file)) ? readFileSync(join(root, file), 'utf8') : null])); + }; + const first = written(); + rerunAfter(root, git); + produce(); + const second = written(); + const moved = [...new Set([...first.keys(), ...second.keys()])].filter((file) => first.get(file) !== second.get(file)).sort(); + return { root, moved }; }; const producedInTree = mkProducedInTree(); + // The re-record row (#26051): one registry package's source moves and the + // artefact does not, so the second run must rewrite that package's record + // and nothing else — no other package record, not the shared header (its + // `generatedAt` is carried while the artefact is byte-identical), not the + // artefact. A producer that rewrites every record, or stamps a fresh date, + // names the extra path here; one that skips the moved package names that. + const rerecorded = mkProducedInTree({ + rerunAfter: (r, git) => { + writeFileSync(join(r, pkgDir('@object-ui/plugin-grid'), 'src', 'index.ts'), 'export const id = "moved";\n'); + git('add', '-A'); + }, + }); + const rerecordedWant = [`${REGISTRY_RECORDS}/plugin-grid.json`]; + const rerecordedProblems = JSON.stringify(rerecorded.moved) === JSON.stringify(rerecordedWant) + ? [] + : [`the second producer run changed ${JSON.stringify(rerecorded.moved)}, not exactly ${JSON.stringify(rerecordedWant)}.`]; + const cases = [ ['green fixture passes', mk(), 0, blind], ['missing artefact is RED', mk((r) => rmSync(join(r, 'sdui.manifest.json'))), 1, blind], @@ -994,10 +1198,12 @@ function selfTest() { modulesRoot: `.cache/objectui-${'a'.repeat(12)}/apps/console`, objectuiWorkspaceVersion: '0.0.0-selftest', }))), 1, blind, ['`inTree`', IN_TREE_REGENERATE]], - ['no pin: a staged registry source edit is RED with the in-tree command', mkInTree((r, git) => { + // The sixth column is text NO problem may carry: the red names the package + // that moved and no other, which is what lets a reader re-record only it. + ['no pin: a staged edit to one registry package is RED naming that package alone', mkInTree((r, git) => { writeFileSync(join(r, pkgDir('@object-ui/plugin-grid'), 'src', 'index.ts'), 'export const id = "moved";\n'); git('add', '-A'); - }), 1, blind, [IN_TREE_REGENERATE]], + }), 1, blind, ['ui/packages/plugin-grid:', IN_TREE_REGENERATE], ['ui/packages/plugin-form', 'ui/packages/core']], ['no pin: an edit the index does not hold is not a registry change', mkInTree((r) => { writeFileSync(join(r, pkgDir('@object-ui/plugin-grid'), 'src', 'index.ts'), 'export const id = "unstaged";\n'); writeFileSync(join(r, pkgDir('@object-ui/plugin-grid'), 'src', 'untracked.ts'), 'export {};\n'); @@ -1013,9 +1219,46 @@ function selfTest() { const p = join(r, pkgDir('@object-ui/plugin-grid'), 'package.json'); const manifest = JSON.parse(readFileSync(p, 'utf8')); writeFileSync(p, JSON.stringify({ ...manifest, peerDependencies: { '@object-ui/unrelated': 'workspace:*' } }, null, 2)); - }), 1, blind, [IN_TREE_REGENERATE]], + }), 1, blind, ['ui/packages/unrelated', IN_TREE_REGENERATE]], + ['no pin: a new package the registry reaches is RED naming it', mkInTree((r, git) => { + const dir = join(r, UI_PACKAGES, 'plugin-new'); + mkdirSync(join(dir, 'src'), { recursive: true }); + writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: '@object-ui/plugin-new', version: '0.0.0-selftest' }, null, 2)); + writeFileSync(join(dir, 'src', 'index.ts'), 'export {};\n'); + const p = join(r, pkgDir('@object-ui/plugin-grid'), 'package.json'); + writeFileSync(p, JSON.stringify({ ...JSON.parse(readFileSync(p, 'utf8')), dependencies: { '@object-ui/plugin-new': 'workspace:*' } }, null, 2)); + git('add', '-A'); + }), 1, blind, ['ui/packages/plugin-new', IN_TREE_REGENERATE]], + ['no pin: a removed registry package is RED naming its record', mkInTree((r, git) => { + rmSync(join(r, pkgDir('@object-ui/types')), { recursive: true }); + const p = join(r, pkgDir(REGISTRY_CORE), 'package.json'); + writeFileSync(p, JSON.stringify({ ...JSON.parse(readFileSync(p, 'utf8')), dependencies: {} }, null, 2)); + git('add', '-A'); + }), 1, blind, [`${REGISTRY_RECORDS}/types.json`, IN_TREE_REGENERATE]], + ['no pin: a deleted package record is RED naming the package', mkInTree((r) => rmSync(packageRecord(r, '@object-ui/plugin-map'))), 1, blind, [ + 'ui/packages/plugin-map', + IN_TREE_REGENERATE, + ]], + // Two branches regenerated on different bases: this package's digest is + // current, but the artefact it was verified against is not the one committed. + ['no pin: a package record verified against another artefact is RED', mkInTree((r) => { + const p = packageRecord(r, '@object-ui/plugin-form'); + const was = JSON.parse(readFileSync(p, 'utf8')); + writeFileSync(p, registryRecordLine({ dir: was.package, files: was.sources, sha256: was.sha256 }, 'f'.repeat(64))); + }), 1, blind, ['ui/packages/plugin-form: verified against ffffffffffff', IN_TREE_REGENERATE]], + // Pretty-printed, one record spans lines, and two branches can merge a + // digest from one with an artefact sha256 from the other without a conflict. + ['no pin: a package record out of its one-line form is RED', mkInTree((r) => { + const p = packageRecord(r, '@object-ui/plugin-grid'); + writeFileSync(p, `${JSON.stringify(JSON.parse(readFileSync(p, 'utf8')), null, 2)}\n`); + }), 1, blind, [`${REGISTRY_RECORDS}/plugin-grid.json`, IN_TREE_REGENERATE]], + ['no pin: a record from the single-digest layout is RED with the in-tree command', mkInTree((r) => editRecord(r, (rec) => ({ + ...rec, + inTree: { ...rec.inTree, registryPackages: [], registrySources: 1, registrySourcesSha256: '0'.repeat(64) }, + }))), 1, blind, ['`registrySourcesSha256`', IN_TREE_REGENERATE]], ['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]], - ['no pin: the in-tree producer builds, then writes a tree this gate passes', producedInTree, 0, blind], + ['no pin: the in-tree producer builds, then writes a tree this gate passes', producedInTree.root, 0, blind], + ['no pin: the producer re-records only the package whose sources moved', rerecorded.root, 0, { ...blind, extra: rerecordedProblems }], ]; // The ledger this self-test's floor is evaluated against (#13489). @@ -1025,17 +1268,20 @@ function selfTest() { }; let failures = 0; - for (const [name, root, want, options, mustName = []] of cases) { + for (const [name, root, want, { extra = [], ...options } = {}, mustName = [], mustNotName = []] of cases) { registerCase(name); - const problems = checkTree(root, options); + const problems = [...checkTree(root, options), ...extra]; const got = problems.length ? 1 : 0; const unnamed = got ? mustName.filter((text) => !problems.join('\n').includes(text)) : []; - if (got !== want || unnamed.length > 0) { + const misnamed = got ? mustNotName.filter((text) => problems.join('\n').includes(text)) : []; + if (got !== want || unnamed.length > 0 || misnamed.length > 0) { failures++; console.error( got !== want ? `✗ self-test: ${name} — expected ${want ? 'RED' : 'GREEN'}, got ${got ? 'RED' : 'GREEN'}` - : `✗ self-test: ${name} — RED, but no problem names ${unnamed.map((text) => JSON.stringify(text)).join(', ')}`, + : unnamed.length > 0 + ? `✗ self-test: ${name} — RED, but no problem names ${unnamed.map((text) => JSON.stringify(text)).join(', ')}` + : `✗ self-test: ${name} — RED, but a problem also names ${misnamed.map((text) => JSON.stringify(text)).join(', ')}`, ); for (const p of problems) console.error(` ${p}`); } @@ -1132,9 +1378,11 @@ function selfTest() { console.log( `✓ check-sdui-manifest self-test: ${cases.length} cases behave (green passes; absence, tamper, moved pin, emptiness, a retired-route record, ` + 'a version the pin does not declare, and an oracle that cannot answer are RED; an absent oracle is reported, not scored; ' + - 'the producer, run under a symlinked TMPDIR, writes a tree that passes; with no pin, the in-tree registry digest reds a staged ' + - 'source edit or a new workspace edge and ignores tests, unreached packages and unstaged bytes, no pin and no console is RED ' + - 'naming both, and the in-tree producer builds, then writes a tree that passes).', + 'the producer, run under a symlinked TMPDIR, writes a tree that passes; with no pin, the per-package registry digests red a ' + + 'staged edit naming only its package, a new package, a new workspace edge, a removed package, a deleted, malformed or ' + + 'other-artefact record and the retired single-digest record, and ignore tests, unreached packages and unstaged bytes, no pin ' + + 'and no console is RED naming both, and the in-tree producer builds, writes a tree that passes, and re-records only the ' + + 'package that moved).', ); return SELF_TEST_VERDICT; @@ -1171,11 +1419,14 @@ if (isEntrypoint(import.meta.url)) { } catch { // display only — the verdict above did not read it } + // display only — checkTree has already held every record to its package + const { records } = readRegistryRecords(DEFAULT_ROOT); + const sources = [...records.values()].reduce((sum, rec) => sum + rec.sources, 0); console.log( `✓ check-sdui-manifest: sdui.manifest.json is present, intact (sha256 ${String(record.sha256).slice(0, 12)}…, ` + - `${record.components} components) and generated from the in-tree registry as it stands: ${inTree.registrySources} ` + - `source(s) in ${inTree.registryPackages.length} package(s) under ${UI_PACKAGES}/, sha256 ${String(inTree.registrySourcesSha256).slice(0, 12)}… ` + - '(no .objectui-sha in this tree).', + `${record.components} components) and generated from the in-tree registry as it stands: ${sources} ` + + `source(s) in ${records.size} package(s) under ${UI_PACKAGES}/, each package's digest current in its record under ` + + `${REGISTRY_RECORDS}/ (no .objectui-sha in this tree).`, ); console.log( ` Provenance, not a verdict: the generator read @object-ui/core ${JSON.stringify(inTree.objectuiWorkspaceVersion)}; ` + diff --git a/scripts/gen-sdui-manifest-node.mjs b/scripts/gen-sdui-manifest-node.mjs index 74396d04089..56f8266b433 100644 --- a/scripts/gen-sdui-manifest-node.mjs +++ b/scripts/gen-sdui-manifest-node.mjs @@ -74,15 +74,37 @@ * closure with turbo (`buildInTree`, a cache hit when nothing moved), refusing * registry sources the index does not hold, then enumerates exactly as above. * The record gains an `inTree` block — the modules root, the version read off - * the build (provenance only), and the registry source digest - * `scripts/check-sdui-manifest.mjs` recomputes — and keeps the pin-keyed - * fields only while the artefact is byte-identical to the one they describe. - * With the pin present, nothing here runs differently. + * the build (provenance only), and where the per-package records live — and + * keeps the pin-keyed fields only while the artefact is byte-identical to the + * one they describe. With the pin present, nothing here runs differently. + * + * ## One record per registry package, re-recorded only when it moved (#26051) + * + * Each registry package's source digest (`registrySourceDigests` in + * scripts/check-sdui-manifest.mjs, which the gate recomputes) goes into its own + * one-line record under `scripts/sdui-manifest.sources/`, beside the sha256 of + * the artefact it was verified against. A run rewrites a package's record only + * when that line changes: when one package's sources moved and the artefact did + * not, exactly that package's record changes, so ui PRs that touch different + * packages touch different files. When the artefact moved, every record names + * the new sha256, so every in-flight branch that regenerated against the old + * artefact conflicts once and regenerates on the merged tree. The shared record + * moves with the artefact and nothing else: `generatedAt` and the provenance + * version are carried while the artefact is byte-identical. + * + * It still BUILDS on every run, including one where only a package's sources + * moved: the build and the enumeration are the only oracle that says whether + * the artefact moved, and a record written without them would stamp a current + * digest on a manifest no run has checked. What makes that cheap is turbo's + * input hashing — measured on objectstack 0a6948210c, one container, no remote + * cache: cold 8m43s (0 of 63 tasks cached); one edit under + * `ui/packages/plugin-grid/src` 33s (61 of 63 cached); one under + * `ui/packages/core/src` 2m50s (41 of 63 cached); nothing moved 6s. */ import { execFileSync, spawnSync } from 'node:child_process'; import { createHash } from 'node:crypto'; -import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'; +import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { pathToFileURL, fileURLToPath } from 'node:url'; @@ -137,10 +159,12 @@ const IN_TREE_NOTE = [ 'scripts/gen-sdui-manifest-node.mjs, the one producer (ruling 丙 on #17735): it builds the registry packages under', 'ui/packages/, then enumerates them from `inTree.modulesRoot` exactly as ui/apps/console/dev/manifest-dump.tsx', 'does: eager-import the 16 registry modules, ComponentRegistry.getPublicConfigs(), manifestFromConfigs from', - "@objectstack/sdui-parser. `inTree.registrySourcesSha256` digests those packages' src/ as the index holds them;", - '`scripts/check-sdui-manifest.mjs` recomputes it, so a registry source edit without regeneration goes RED there.', - '`inTree.objectuiWorkspaceVersion` is provenance, not a verdict (#25712). Pin-keyed fields, where present, say the', - 'artefact is byte-identical to the one generated at that pin.', + '@objectstack/sdui-parser. Each registry package has a one-line record under `inTree.registryRecords` (#26051): a', + "digest of that package's own src/ as the index holds them, and the sha256 of the artefact it was verified against.", + '`scripts/check-sdui-manifest.mjs` recomputes every digest, so a registry source edit without regeneration goes RED', + 'there naming the package. This record moves only with the artefact; a run where only sources moved re-records only', + 'their packages. `inTree.objectuiWorkspaceVersion` is provenance, not a verdict (#25712). Pin-keyed fields, where', + 'present, say the artefact is byte-identical to the one generated at that pin.', 'Regenerate + re-record: node scripts/gen-sdui-manifest-node.mjs', ]; @@ -159,11 +183,11 @@ function unindexed(dirs) { /** * No pin — the merged layout (#25712): build the registry's packages from this - * tree's own sources, then read that build. Returns the `inTree` block's - * reading of the sources the build was made from. + * tree's own sources, then read that build. Returns the gate module and the + * per-package reading of the sources the build was made from. * - * The build is THIS script's step here, not the caller's: the record names the - * sources by digest (`registrySourceDigest` in scripts/check-sdui-manifest.mjs, + * The build is THIS script's step here, not the caller's: the records name the + * sources by digest (`registrySourceDigests` in scripts/check-sdui-manifest.mjs, * which the gate recomputes), and only a build of exactly those sources makes * the artefact describe them — a run over a stale dist would stamp a fresh * digest on a stale manifest. Turbo's input hashing makes an up-to-date build a @@ -186,9 +210,9 @@ async function buildInTree() { ' would describe a different registry than the one enumerated here. Make the two lists equal.', ); } - const before = gate.registrySourceDigest(ROOT); + const before = gate.registrySourceDigests(ROOT); if (before.problem) fail(`cannot digest the registry sources: ${before.problem}`); - const sources = before.packages.map((dir) => `${dir}/src`); + const sources = before.packages.map(({ dir }) => `${dir}/src`); const notIndexed = unindexed(sources); if (notIndexed.length > 0) { fail( @@ -203,8 +227,8 @@ async function buildInTree() { if (!existsSync(turbo)) fail('no turbo at node_modules/.bin/turbo — run `pnpm install` first.'); const filters = ['@object-ui/core', ...REGISTRY_MODULES].map((name) => `--filter=${name}...`); console.error( - `→ building the registry from this tree: ${before.packages.length} package(s), ${before.files} source(s), ` + - `sha256 ${before.sha256.slice(0, 12)}… (turbo — a cache hit when nothing moved)`, + `→ building the registry from this tree: ${before.packages.length} package(s), ` + + `${before.packages.reduce((sum, { files }) => sum + files, 0)} source(s) (turbo — a cache hit when nothing moved)`, ); const build = spawnSync(turbo, ['run', 'build', ...filters, '--filter=@objectstack/sdui-parser', '--output-logs=errors-only'], { cwd: ROOT, @@ -212,14 +236,14 @@ async function buildInTree() { }); if (build.status !== 0) fail(`the in-tree build exited ${build.status ?? build.signal}; its output is above.`); - const after = gate.registrySourceDigest(ROOT); - if (after.problem || after.sha256 !== before.sha256 || unindexed(sources).length > 0) { + const after = gate.registrySourceDigests(ROOT); + if (after.problem || JSON.stringify(after.packages) !== JSON.stringify(before.packages) || unindexed(sources).length > 0) { fail('the build moved the registry sources it was meant to read (their digest or their untracked files changed). Nothing was written.'); } if (!existsSync(join(ROOT, ...UI_CONSOLE.split('/'), 'node_modules', '@object-ui', 'core', 'dist', 'index.js'))) { fail(`${UI_CONSOLE}/node_modules/@object-ui/core/dist/index.js is missing after the build, so ${UI_CONSOLE} does not resolve the registry the build made.`); } - return { registryPackages: before.packages, registrySources: before.files, registrySourcesSha256: before.sha256 }; + return { gate, packages: before.packages }; } if (process.argv.length > 2) { @@ -369,12 +393,16 @@ const manifest = JSON.parse(json); const count = Object.keys(manifest.components).length; writeFileSync(OUT_PATH, json); const sha256 = createHash('sha256').update(json).digest('hex'); -// Pinned, the record is exactly what it has always been. In-tree, the pin-keyed -// fields describe an ARTEFACT, so they are carried only while the regenerated -// one is byte-identical to the one they were written for (the same sha256) — -// then they still say something true about these bytes — and dropped the -// moment it is not. `undefined` keys are left out of the JSON. +// Pinned, the record is exactly what it has always been. In-tree, the record +// describes an ARTEFACT, so everything in it that is not read off this run — +// the pin-keyed fields, `generatedAt`, the provenance version — is carried only +// while the regenerated artefact is byte-identical to the one it was written +// for (the same sha256), when it still says something true about these bytes, +// and replaced the moment it is not. That is what keeps this file out of the +// diff of a run where only registry sources moved (#26051). `undefined` keys +// are left out of the JSON. const carried = !pinned && record.sha256 === sha256 ? record : {}; +const today = new Date().toISOString().slice(0, 10); const nextRecord = { '//': pinned ? record['//'] ?? [] : IN_TREE_NOTE, objectuiSha: pinned ? pin : carried.objectuiSha, @@ -382,12 +410,45 @@ const nextRecord = { modulesRoot: pinned ? MODULES_ROOT_REL : carried.modulesRoot, objectuiWorkspaceVersion: pinned ? workspaceVersion : carried.objectuiWorkspaceVersion, generator: 'scripts/gen-sdui-manifest-node.mjs', - generatedAt: new Date().toISOString().slice(0, 10), + generatedAt: pinned ? today : carried.generatedAt ?? today, sha256, components: count, - inTree: pinned ? undefined : { modulesRoot: MODULES_ROOT_REL, objectuiWorkspaceVersion: workspaceVersion, ...inTree }, + inTree: pinned + ? undefined + : { + modulesRoot: MODULES_ROOT_REL, + objectuiWorkspaceVersion: carried.inTree?.objectuiWorkspaceVersion ?? workspaceVersion, + registryRecords: inTree.gate.REGISTRY_RECORDS, + }, }; -writeFileSync(RECORD_PATH, JSON.stringify(nextRecord, null, 2) + '\n'); + +// One record per registry package, written only where its line changed; a +// record for a package the registry no longer reaches is removed (#26051). +const rerecorded = []; +const removed = []; +if (!pinned) { + const { gate } = inTree; + const recordsDir = join(ROOT, ...gate.REGISTRY_RECORDS.split('/')); + mkdirSync(recordsDir, { recursive: true }); + const keep = new Set(); + for (const pkg of inTree.packages) { + const rel = gate.registryRecordPath(pkg.dir); + keep.add(rel); + const path = join(ROOT, ...rel.split('/')); + const line = gate.registryRecordLine(pkg, sha256); + if (existsSync(path) && readFileSync(path, 'utf8') === line) continue; + writeFileSync(path, line); + rerecorded.push(pkg.dir); + } + for (const name of readdirSync(recordsDir).sort()) { + if (keep.has(`${gate.REGISTRY_RECORDS}/${name}`)) continue; + rmSync(join(recordsDir, name), { recursive: true, force: true }); + removed.push(`${gate.REGISTRY_RECORDS}/${name}`); + } +} +const nextRecordText = JSON.stringify(nextRecord, null, 2) + '\n'; +const recordMoved = !existsSync(RECORD_PATH) || readFileSync(RECORD_PATH, 'utf8') !== nextRecordText; +writeFileSync(RECORD_PATH, nextRecordText); console.error(`✓ wrote sdui.manifest.json (${count} components, ${Buffer.byteLength(json)} bytes, sha256 ${sha256.slice(0, 12)}…)`); if (pinned) { console.error( @@ -395,10 +456,19 @@ if (pinned) { `@object-ui/core workspace version ${workspaceVersion}`, ); } else { + const { packages } = inTree; console.error( - `✓ re-recorded scripts/sdui-manifest.record.json in-tree — ${SOURCE} ${MODULES_ROOT_REL}, registry sources ` + - `${inTree.registrySources} file(s) in ${inTree.registryPackages.length} package(s), sha256 ` + - `${inTree.registrySourcesSha256.slice(0, 12)}…, @object-ui/core workspace version ${workspaceVersion}; ` + + `✓ ${recordMoved ? 're-recorded' : 'left unchanged'} scripts/sdui-manifest.record.json in-tree — ${SOURCE} ${MODULES_ROOT_REL}, ` + + `@object-ui/core workspace version ${nextRecord.inTree.objectuiWorkspaceVersion}` + + (carried.sha256 ? ` (artefact unchanged since ${nextRecord.generatedAt}, so the record is carried)` : '') + + '; ' + (carried.objectuiSha ? `pin fields kept (artefact unchanged since pin ${carried.objectuiSha.slice(0, 12)})` : 'no pin fields'), ); + console.error( + `✓ ${rerecorded.length} of ${packages.length} registry package record(s) re-recorded under ${inTree.gate.REGISTRY_RECORDS}/` + + (rerecorded.length > 0 ? `: ${rerecorded.map((dir) => dir.slice(dir.lastIndexOf('/') + 1)).join(', ')}` : '') + + (carried.sha256 ? '' : ' (the artefact moved, so every record names its new sha256)') + + (removed.length > 0 ? `; removed ${removed.join(', ')} (no longer reached)` : '') + + ` — ${packages.reduce((sum, { files }) => sum + files, 0)} source(s) in all.`, + ); } From e4f05e0ea16026ce0ee345bc424728b302c606e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 12:31:34 +0000 Subject: [PATCH 2/5] devx(sdui): the re-record self-test row back-dates the header so a fresh date reds it Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01SMJGPXxFBpbgAgXdnsXKVS --- scripts/check-sdui-manifest.mjs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/scripts/check-sdui-manifest.mjs b/scripts/check-sdui-manifest.mjs index 41d411b0f8b..b66bf52e59f 100644 --- a/scripts/check-sdui-manifest.mjs +++ b/scripts/check-sdui-manifest.mjs @@ -1064,7 +1064,7 @@ function selfTest() { * with the producer's stderr above it. * * With `rerunAfter`, the producer runs a second time after that edit, and - * the returned `moved` lists every path under `scripts/` whose bytes the + * the returned `moved` lists every path the producer writes whose bytes the * second run changed — the re-record row's reading. */ const mkProducedInTree = ({ rerunAfter } = {}) => { @@ -1112,8 +1112,8 @@ function selfTest() { } return new Map(files.map((file) => [file.split(sep).join('/'), existsSync(join(root, file)) ? readFileSync(join(root, file), 'utf8') : null])); }; - const first = written(); rerunAfter(root, git); + const first = written(); produce(); const second = written(); const moved = [...new Set([...first.keys(), ...second.keys()])].filter((file) => first.get(file) !== second.get(file)).sort(); @@ -1123,14 +1123,17 @@ function selfTest() { // The re-record row (#26051): one registry package's source moves and the // artefact does not, so the second run must rewrite that package's record - // and nothing else — no other package record, not the shared header (its - // `generatedAt` is carried while the artefact is byte-identical), not the - // artefact. A producer that rewrites every record, or stamps a fresh date, - // names the extra path here; one that skips the moved package names that. + // and nothing else — no other package record, not the shared header, not the + // artefact. The header is back-dated first, as a run on an earlier day left + // it: `generatedAt` is carried while the artefact is byte-identical, so a + // producer that stamps today's date names the header here, one that moves + // another record names it, and one that skips the moved package names that. const rerecorded = mkProducedInTree({ rerunAfter: (r, git) => { writeFileSync(join(r, pkgDir('@object-ui/plugin-grid'), 'src', 'index.ts'), 'export const id = "moved";\n'); git('add', '-A'); + const p = join(r, READ_PATHS.record); + writeFileSync(p, JSON.stringify({ ...JSON.parse(readFileSync(p, 'utf8')), generatedAt: '2000-01-01' }, null, 2) + '\n'); }, }); const rerecordedWant = [`${REGISTRY_RECORDS}/plugin-grid.json`]; From a8114011ec1a0a21ef293595cd07a9d46fda0576 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 12:33:10 +0000 Subject: [PATCH 3/5] devx(sdui): re-record the manifest per registry package; move the record's readers Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01SMJGPXxFBpbgAgXdnsXKVS --- docs/releases-maintenance.md | 25 +++++++---- scripts/check-changeset-no-major.mjs | 24 +++++++---- scripts/gen-sdui-manifest-node.mjs | 2 +- scripts/sdui-manifest.record.json | 42 ++++--------------- scripts/sdui-manifest.sources/components.json | 1 + scripts/sdui-manifest.sources/core.json | 1 + .../data-objectstack.json | 1 + scripts/sdui-manifest.sources/fields.json | 1 + scripts/sdui-manifest.sources/i18n.json | 1 + scripts/sdui-manifest.sources/mobile.json | 1 + .../sdui-manifest.sources/permissions.json | 1 + .../plugin-calendar.json | 1 + .../sdui-manifest.sources/plugin-charts.json | 1 + .../plugin-dashboard.json | 1 + .../sdui-manifest.sources/plugin-detail.json | 1 + .../sdui-manifest.sources/plugin-form.json | 1 + .../sdui-manifest.sources/plugin-gantt.json | 1 + .../sdui-manifest.sources/plugin-grid.json | 1 + .../sdui-manifest.sources/plugin-kanban.json | 1 + .../sdui-manifest.sources/plugin-list.json | 1 + scripts/sdui-manifest.sources/plugin-map.json | 1 + .../plugin-markdown.json | 1 + .../sdui-manifest.sources/plugin-report.json | 1 + .../plugin-timeline.json | 1 + .../sdui-manifest.sources/plugin-tree.json | 1 + .../sdui-manifest.sources/plugin-view.json | 1 + scripts/sdui-manifest.sources/providers.json | 1 + .../sdui-manifest.sources/react-runtime.json | 1 + scripts/sdui-manifest.sources/react.json | 1 + .../sdui-manifest.sources/sdui-parser.json | 1 + scripts/sdui-manifest.sources/types.json | 1 + 31 files changed, 67 insertions(+), 53 deletions(-) create mode 100644 scripts/sdui-manifest.sources/components.json create mode 100644 scripts/sdui-manifest.sources/core.json create mode 100644 scripts/sdui-manifest.sources/data-objectstack.json create mode 100644 scripts/sdui-manifest.sources/fields.json create mode 100644 scripts/sdui-manifest.sources/i18n.json create mode 100644 scripts/sdui-manifest.sources/mobile.json create mode 100644 scripts/sdui-manifest.sources/permissions.json create mode 100644 scripts/sdui-manifest.sources/plugin-calendar.json create mode 100644 scripts/sdui-manifest.sources/plugin-charts.json create mode 100644 scripts/sdui-manifest.sources/plugin-dashboard.json create mode 100644 scripts/sdui-manifest.sources/plugin-detail.json create mode 100644 scripts/sdui-manifest.sources/plugin-form.json create mode 100644 scripts/sdui-manifest.sources/plugin-gantt.json create mode 100644 scripts/sdui-manifest.sources/plugin-grid.json create mode 100644 scripts/sdui-manifest.sources/plugin-kanban.json create mode 100644 scripts/sdui-manifest.sources/plugin-list.json create mode 100644 scripts/sdui-manifest.sources/plugin-map.json create mode 100644 scripts/sdui-manifest.sources/plugin-markdown.json create mode 100644 scripts/sdui-manifest.sources/plugin-report.json create mode 100644 scripts/sdui-manifest.sources/plugin-timeline.json create mode 100644 scripts/sdui-manifest.sources/plugin-tree.json create mode 100644 scripts/sdui-manifest.sources/plugin-view.json create mode 100644 scripts/sdui-manifest.sources/providers.json create mode 100644 scripts/sdui-manifest.sources/react-runtime.json create mode 100644 scripts/sdui-manifest.sources/react.json create mode 100644 scripts/sdui-manifest.sources/sdui-parser.json create mode 100644 scripts/sdui-manifest.sources/types.json diff --git a/docs/releases-maintenance.md b/docs/releases-maintenance.md index 288eb6fbb54..4b749328fdc 100644 --- a/docs/releases-maintenance.md +++ b/docs/releases-maintenance.md @@ -55,11 +55,12 @@ ships the Console from the same commit as the spec it builds against. > root `.changeset/` before the first joint Version PR is #25831. New frontend > changesets go in the root `.changeset/`. -#### After a registry change: regenerate the committed manifest (#12924, #17735, #25712) +#### After a registry change: regenerate the committed manifest (#12924, #17735, #25712, #26051) The repo-root `sdui.manifest.json` (ADR-0080, the Console's public-tier component -manifest) and its provenance record `scripts/sdui-manifest.record.json` are -generated, and one command writes them: +manifest), its provenance record `scripts/sdui-manifest.record.json` and one record +per registry package under `scripts/sdui-manifest.sources/` are generated, and one +command writes them: ```bash node scripts/gen-sdui-manifest-node.mjs # no arguments @@ -68,14 +69,20 @@ node scripts/gen-sdui-manifest-node.mjs # no arguments It builds the registry packages under `ui/packages/`, enumerates them from `ui/apps/console` under plain Node, and serializes them through `@objectstack/sdui-parser` (it needs `packages/sdui-parser/dist`: -`pnpm --filter @objectstack/sdui-parser build`). The record carries an `inTree` -block with a digest of the registry packages' `src/`. +`pnpm --filter @objectstack/sdui-parser build`). Each package record is one line: +a digest of that package's own `src/` and the `sha256` of the artefact it was +verified against. A run rewrites only the records whose line changed, so a PR that +edits one registry package re-records that package alone; the shared record moves +only when the artefact does, and then every package record names the new `sha256`. -A required lint step reads the tracked pair on **every PR**: +A required lint step reads the tracked files on **every PR**: `node scripts/check-sdui-manifest.mjs` checks the shape, the `sha256` against the -record, and recomputes the source digest from the index. A PR that edits a registry -package's source without regenerating goes red there, and the failure text prints -the regeneration call. ⛔ There is no npm install route to the manifest; that was +record, and recomputes each package's source digest from the index. A PR that edits +a registry package's source without regenerating goes red there, naming the package, +and the failure text prints the regeneration call. Two PRs that edit different +registry packages touch different record files and do not conflict; two that edit +the same package, or a PR that changes the artefact, still conflict once, and the +later one merges `main` and re-runs the command. ⛔ There is no npm install route to the manifest; that was deleted by ruling 丙 on #17735. **One producer, one artefact.** `packages/console/build-dist.sh` copies the tracked diff --git a/scripts/check-changeset-no-major.mjs b/scripts/check-changeset-no-major.mjs index a0a874da057..e33bd3a7f15 100644 --- a/scripts/check-changeset-no-major.mjs +++ b/scripts/check-changeset-no-major.mjs @@ -1365,9 +1365,10 @@ export function publishedSourceOwners(path, manifestOf) { // packed output — one path — and never which files it copies: the day the // console build ships a second tracked file, this leg sees it with no edit // here, and the day it stops copying the manifest, the leg stops claiming it. -// `scripts/sdui-manifest.record.json` is measured OUT for that reason rather -// than by choice: the build reads it for a warning line and does not copy it, -// so it is not part of the shipped artefact. +// `scripts/sdui-manifest.record.json` and the per-package records under +// `scripts/sdui-manifest.sources/` are measured OUT for that reason rather than +// by choice: the build does not copy them, so they are not part of the shipped +// artefact. // // ⚠️ Residual, stated rather than implied: the reader understands the plain // shell the script is written in — `NAME="value"` assignments, `$NAME` / @@ -4875,8 +4876,8 @@ function selfTest() { `this repository's ${PACKED_COPY_BUILD_SCRIPTS[0]} copies the repo-root sdui.manifest.json into packages/console/dist/ as read by this leg — got ${JSON.stringify(realPairs)}`, ); assert( - !realPairs.some((p) => p.source === 'scripts/sdui-manifest.record.json'), - `measured, not chosen: the real build does not ship the manifest record — got ${JSON.stringify(realPairs)}`, + !realPairs.some((p) => p.source === 'scripts/sdui-manifest.record.json' || p.source.startsWith('scripts/sdui-manifest.sources/')), + `measured, not chosen: the real build ships neither the manifest record nor a package record — got ${JSON.stringify(realPairs)}`, ); // ── The owner of a destination ──────────────────────────────────────── @@ -4956,14 +4957,21 @@ function selfTest() { const no = levelOf(copyRepo({ head: { ...PR22812, '.changeset/console-bbbb.md': changeset([[CONSOLE, 'patch']]) } }), declaredNo); assert(no.result.verdict === 'none-clean' && no.exitCode === 0, `the manifest moved under \`Clause-②: no\` + \`none\` is unaffected — got ${no.result.verdict} / ${no.exitCode}`); - // The record moves with every regeneration but ships nothing. + // The records move with regeneration but ship nothing: the shared one, and + // the one-line record of the registry package a ui PR re-recorded. const recordOnly = levelOf( - copyRepo({ head: { 'scripts/sdui-manifest.record.json': '{"objectuiSha":"bbbb"}\n', '.changeset/console-bbbb.md': changeset([[CONSOLE, 'patch']]) } }), + copyRepo({ + head: { + 'scripts/sdui-manifest.record.json': '{"objectuiSha":"bbbb"}\n', + 'scripts/sdui-manifest.sources/plugin-grid.json': '{"package":"ui/packages/plugin-grid"}\n', + '.changeset/console-bbbb.md': changeset([[CONSOLE, 'patch']]), + }, + }), yesWidening, ); assert( recordOnly.touched.packages.length === 0 && recordOnly.touched.unreadable.length === 0 && recordOnly.result.verdict === 'clean', - `control: the record alone moves no published file — got ${JSON.stringify(recordOnly.touched)} / ${recordOnly.result.verdict}`, + `control: the records alone move no published file — got ${JSON.stringify(recordOnly.touched)} / ${recordOnly.result.verdict}`, ); // #4690 on this leg: a build script that is THERE but yields no copy is diff --git a/scripts/gen-sdui-manifest-node.mjs b/scripts/gen-sdui-manifest-node.mjs index 56f8266b433..49330951cc4 100644 --- a/scripts/gen-sdui-manifest-node.mjs +++ b/scripts/gen-sdui-manifest-node.mjs @@ -460,7 +460,7 @@ if (pinned) { console.error( `✓ ${recordMoved ? 're-recorded' : 'left unchanged'} scripts/sdui-manifest.record.json in-tree — ${SOURCE} ${MODULES_ROOT_REL}, ` + `@object-ui/core workspace version ${nextRecord.inTree.objectuiWorkspaceVersion}` + - (carried.sha256 ? ` (artefact unchanged since ${nextRecord.generatedAt}, so the record is carried)` : '') + + (carried.sha256 ? ` (artefact unchanged since ${nextRecord.generatedAt}: its date and version are carried)` : '') + '; ' + (carried.objectuiSha ? `pin fields kept (artefact unchanged since pin ${carried.objectuiSha.slice(0, 12)})` : 'no pin fields'), ); diff --git a/scripts/sdui-manifest.record.json b/scripts/sdui-manifest.record.json index 684a23dffc1..b4080228dd7 100644 --- a/scripts/sdui-manifest.record.json +++ b/scripts/sdui-manifest.record.json @@ -5,10 +5,12 @@ "scripts/gen-sdui-manifest-node.mjs, the one producer (ruling 丙 on #17735): it builds the registry packages under", "ui/packages/, then enumerates them from `inTree.modulesRoot` exactly as ui/apps/console/dev/manifest-dump.tsx", "does: eager-import the 16 registry modules, ComponentRegistry.getPublicConfigs(), manifestFromConfigs from", - "@objectstack/sdui-parser. `inTree.registrySourcesSha256` digests those packages' src/ as the index holds them;", - "`scripts/check-sdui-manifest.mjs` recomputes it, so a registry source edit without regeneration goes RED there.", - "`inTree.objectuiWorkspaceVersion` is provenance, not a verdict (#25712). Pin-keyed fields, where present, say the", - "artefact is byte-identical to the one generated at that pin.", + "@objectstack/sdui-parser. Each registry package has a one-line record under `inTree.registryRecords` (#26051): a", + "digest of that package's own src/ as the index holds them, and the sha256 of the artefact it was verified against.", + "`scripts/check-sdui-manifest.mjs` recomputes every digest, so a registry source edit without regeneration goes RED", + "there naming the package. This record moves only with the artefact; a run where only sources moved re-records only", + "their packages. `inTree.objectuiWorkspaceVersion` is provenance, not a verdict (#25712). Pin-keyed fields, where", + "present, say the artefact is byte-identical to the one generated at that pin.", "Regenerate + re-record: node scripts/gen-sdui-manifest-node.mjs" ], "source": "built-tree", @@ -19,36 +21,6 @@ "inTree": { "modulesRoot": "ui/apps/console", "objectuiWorkspaceVersion": "18.1.0", - "registryPackages": [ - "ui/packages/components", - "ui/packages/core", - "ui/packages/data-objectstack", - "ui/packages/fields", - "ui/packages/i18n", - "ui/packages/mobile", - "ui/packages/permissions", - "ui/packages/plugin-calendar", - "ui/packages/plugin-charts", - "ui/packages/plugin-dashboard", - "ui/packages/plugin-detail", - "ui/packages/plugin-form", - "ui/packages/plugin-gantt", - "ui/packages/plugin-grid", - "ui/packages/plugin-kanban", - "ui/packages/plugin-list", - "ui/packages/plugin-map", - "ui/packages/plugin-markdown", - "ui/packages/plugin-report", - "ui/packages/plugin-timeline", - "ui/packages/plugin-tree", - "ui/packages/plugin-view", - "ui/packages/providers", - "ui/packages/react", - "ui/packages/react-runtime", - "ui/packages/sdui-parser", - "ui/packages/types" - ], - "registrySources": 922, - "registrySourcesSha256": "0e127be18a1104d857822dc5c71a6380e4f00b0db729cd8b8a230a11c120fe78" + "registryRecords": "scripts/sdui-manifest.sources" } } diff --git a/scripts/sdui-manifest.sources/components.json b/scripts/sdui-manifest.sources/components.json new file mode 100644 index 00000000000..b2f126ff477 --- /dev/null +++ b/scripts/sdui-manifest.sources/components.json @@ -0,0 +1 @@ +{"package":"ui/packages/components","sources":226,"sha256":"7c50070c8bc750d5717fce0a081038855777bfb749567757047e672262517886","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/core.json b/scripts/sdui-manifest.sources/core.json new file mode 100644 index 00000000000..43d8e41c0cd --- /dev/null +++ b/scripts/sdui-manifest.sources/core.json @@ -0,0 +1 @@ +{"package":"ui/packages/core","sources":110,"sha256":"c4ca58ee32a488b2139abc179f34d5606ba0d89b86fccf8bd5c4d7b69d07f654","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/data-objectstack.json b/scripts/sdui-manifest.sources/data-objectstack.json new file mode 100644 index 00000000000..f0a6be1d056 --- /dev/null +++ b/scripts/sdui-manifest.sources/data-objectstack.json @@ -0,0 +1 @@ +{"package":"ui/packages/data-objectstack","sources":10,"sha256":"83f94a01a5e9704df262bda723b1dd58c619d66ec70f9dc22476cc7146e8d97a","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/fields.json b/scripts/sdui-manifest.sources/fields.json new file mode 100644 index 00000000000..5434b657f1f --- /dev/null +++ b/scripts/sdui-manifest.sources/fields.json @@ -0,0 +1 @@ +{"package":"ui/packages/fields","sources":96,"sha256":"89537e9f86037ddb8d0c6ce1a9fbcfbc8d756fd38b1a990c2c5ccc277a359d1a","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/i18n.json b/scripts/sdui-manifest.sources/i18n.json new file mode 100644 index 00000000000..2f1398f2c8c --- /dev/null +++ b/scripts/sdui-manifest.sources/i18n.json @@ -0,0 +1 @@ +{"package":"ui/packages/i18n","sources":31,"sha256":"683731eac82c00f4ffa54852f8ebfd947b5c95ad70847d8e2dc55ff66adbe25a","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/mobile.json b/scripts/sdui-manifest.sources/mobile.json new file mode 100644 index 00000000000..e513411ece6 --- /dev/null +++ b/scripts/sdui-manifest.sources/mobile.json @@ -0,0 +1 @@ +{"package":"ui/packages/mobile","sources":16,"sha256":"c0a5f99d3fcf9231f398a16b7c62814de505b2de6118ed667da4f93d7cf9ba9a","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/permissions.json b/scripts/sdui-manifest.sources/permissions.json new file mode 100644 index 00000000000..b2584dca225 --- /dev/null +++ b/scripts/sdui-manifest.sources/permissions.json @@ -0,0 +1 @@ +{"package":"ui/packages/permissions","sources":11,"sha256":"098953c65195fea2549322059e6b1e823aa18c69e1c2452393e3294273376aeb","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-calendar.json b/scripts/sdui-manifest.sources/plugin-calendar.json new file mode 100644 index 00000000000..f2aa7100032 --- /dev/null +++ b/scripts/sdui-manifest.sources/plugin-calendar.json @@ -0,0 +1 @@ +{"package":"ui/packages/plugin-calendar","sources":5,"sha256":"98e2d93a71f452da9b1bb0b340c8626a77d75ec1ae220378a97ee4c3d7c6f0da","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-charts.json b/scripts/sdui-manifest.sources/plugin-charts.json new file mode 100644 index 00000000000..0ff7a1dac09 --- /dev/null +++ b/scripts/sdui-manifest.sources/plugin-charts.json @@ -0,0 +1 @@ +{"package":"ui/packages/plugin-charts","sources":9,"sha256":"9843a388c9d272389f1d44ab7bcccfd51c106109941f698c0438a86cc224e9a9","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-dashboard.json b/scripts/sdui-manifest.sources/plugin-dashboard.json new file mode 100644 index 00000000000..8d006a093fa --- /dev/null +++ b/scripts/sdui-manifest.sources/plugin-dashboard.json @@ -0,0 +1 @@ +{"package":"ui/packages/plugin-dashboard","sources":33,"sha256":"c8aaf01c370229a85b2dedb0a4927d9edca345d3f029c35e6d732ca637b6dc08","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-detail.json b/scripts/sdui-manifest.sources/plugin-detail.json new file mode 100644 index 00000000000..2586fca555c --- /dev/null +++ b/scripts/sdui-manifest.sources/plugin-detail.json @@ -0,0 +1 @@ +{"package":"ui/packages/plugin-detail","sources":54,"sha256":"21ccb887f5b2ccb7f48b871632b94f43e15f2264810fa6d08f6ba107367dd7f6","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-form.json b/scripts/sdui-manifest.sources/plugin-form.json new file mode 100644 index 00000000000..dd6b3165525 --- /dev/null +++ b/scripts/sdui-manifest.sources/plugin-form.json @@ -0,0 +1 @@ +{"package":"ui/packages/plugin-form","sources":40,"sha256":"2dd3a6efc0f5fca7cd001fef05d3279abd947a36f895ce04cd3dc6b7d71d3b66","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-gantt.json b/scripts/sdui-manifest.sources/plugin-gantt.json new file mode 100644 index 00000000000..edaaf51fb22 --- /dev/null +++ b/scripts/sdui-manifest.sources/plugin-gantt.json @@ -0,0 +1 @@ +{"package":"ui/packages/plugin-gantt","sources":10,"sha256":"49a60fe107e65e986d9ba4da711c3dffdd0acb4330c0670957919617721e0cf9","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-grid.json b/scripts/sdui-manifest.sources/plugin-grid.json new file mode 100644 index 00000000000..5ba96731fe6 --- /dev/null +++ b/scripts/sdui-manifest.sources/plugin-grid.json @@ -0,0 +1 @@ +{"package":"ui/packages/plugin-grid","sources":38,"sha256":"aa105ba15cde7ad140409a327a1d63ddf704662aa2ee53712009950d85527000","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-kanban.json b/scripts/sdui-manifest.sources/plugin-kanban.json new file mode 100644 index 00000000000..4369a6366f8 --- /dev/null +++ b/scripts/sdui-manifest.sources/plugin-kanban.json @@ -0,0 +1 @@ +{"package":"ui/packages/plugin-kanban","sources":17,"sha256":"ff448a0909873b5551d0819081287276326b3364a6014e5baa08b40b6c342f3d","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-list.json b/scripts/sdui-manifest.sources/plugin-list.json new file mode 100644 index 00000000000..e1b9bdb96a4 --- /dev/null +++ b/scripts/sdui-manifest.sources/plugin-list.json @@ -0,0 +1 @@ +{"package":"ui/packages/plugin-list","sources":11,"sha256":"0fbf394f74817e4ce55949400f54b28ccc93fb0896aac6614e9c943b2922332d","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-map.json b/scripts/sdui-manifest.sources/plugin-map.json new file mode 100644 index 00000000000..632e105402c --- /dev/null +++ b/scripts/sdui-manifest.sources/plugin-map.json @@ -0,0 +1 @@ +{"package":"ui/packages/plugin-map","sources":4,"sha256":"9987a8a9895284b5e3c44407a44f284462c5f522616cf9f88f9d9bdd7e0b8d90","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-markdown.json b/scripts/sdui-manifest.sources/plugin-markdown.json new file mode 100644 index 00000000000..120a379bd9f --- /dev/null +++ b/scripts/sdui-manifest.sources/plugin-markdown.json @@ -0,0 +1 @@ +{"package":"ui/packages/plugin-markdown","sources":6,"sha256":"496ae13d7775da75eb4dacb4e87beac1dd4d74c49da30bb4de530c5f8c68b85c","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-report.json b/scripts/sdui-manifest.sources/plugin-report.json new file mode 100644 index 00000000000..218194d58f1 --- /dev/null +++ b/scripts/sdui-manifest.sources/plugin-report.json @@ -0,0 +1 @@ +{"package":"ui/packages/plugin-report","sources":12,"sha256":"ef4f974fa309113e502c528913ae78a3dbc1c94c11df628f01b91c9acdc99e62","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-timeline.json b/scripts/sdui-manifest.sources/plugin-timeline.json new file mode 100644 index 00000000000..a6af9361bad --- /dev/null +++ b/scripts/sdui-manifest.sources/plugin-timeline.json @@ -0,0 +1 @@ +{"package":"ui/packages/plugin-timeline","sources":5,"sha256":"c674f749869fda86e541972bfabe2989e91024265f3ab49af0b8fa109905d691","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-tree.json b/scripts/sdui-manifest.sources/plugin-tree.json new file mode 100644 index 00000000000..0f121ebfed8 --- /dev/null +++ b/scripts/sdui-manifest.sources/plugin-tree.json @@ -0,0 +1 @@ +{"package":"ui/packages/plugin-tree","sources":2,"sha256":"44cc4da6c216b44f1d6522f3f9b271082f00ba3dc2637eba83cd4642528df195","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-view.json b/scripts/sdui-manifest.sources/plugin-view.json new file mode 100644 index 00000000000..5f347f28e90 --- /dev/null +++ b/scripts/sdui-manifest.sources/plugin-view.json @@ -0,0 +1 @@ +{"package":"ui/packages/plugin-view","sources":13,"sha256":"561fc47c7e565e85d44ce40ce96be7eb0996fe36b07110c837442edb1e5b0546","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/providers.json b/scripts/sdui-manifest.sources/providers.json new file mode 100644 index 00000000000..2c3b8a0480d --- /dev/null +++ b/scripts/sdui-manifest.sources/providers.json @@ -0,0 +1 @@ +{"package":"ui/packages/providers","sources":2,"sha256":"a95dca7cc13d413117aa8ae0ebdb368f01b5b749d4a7b8d8bfd3b1e811909219","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/react-runtime.json b/scripts/sdui-manifest.sources/react-runtime.json new file mode 100644 index 00000000000..e41d5efcacb --- /dev/null +++ b/scripts/sdui-manifest.sources/react-runtime.json @@ -0,0 +1 @@ +{"package":"ui/packages/react-runtime","sources":1,"sha256":"34830cfa7608d79e9c7adbce446de3815be2d27eb47faa97b94666614bdba753","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/react.json b/scripts/sdui-manifest.sources/react.json new file mode 100644 index 00000000000..1af268c0a57 --- /dev/null +++ b/scripts/sdui-manifest.sources/react.json @@ -0,0 +1 @@ +{"package":"ui/packages/react","sources":70,"sha256":"1d1036b80f21e46d0b90bb61ba8cb285336f396ac85ce66daa42073e8682c3e2","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/sdui-parser.json b/scripts/sdui-manifest.sources/sdui-parser.json new file mode 100644 index 00000000000..d8c156cf0b9 --- /dev/null +++ b/scripts/sdui-manifest.sources/sdui-parser.json @@ -0,0 +1 @@ +{"package":"ui/packages/sdui-parser","sources":9,"sha256":"08704fc188cb4749560f2a3faedd77b60820c1990f03311bcad21f283b2d7be4","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/types.json b/scripts/sdui-manifest.sources/types.json new file mode 100644 index 00000000000..511efb3d3c3 --- /dev/null +++ b/scripts/sdui-manifest.sources/types.json @@ -0,0 +1 @@ +{"package":"ui/packages/types","sources":81,"sha256":"64d03d9bda9e93948031fd5e4246e406b10f413027fec12c83c8fc326174b3ba","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} From 086ff79e2190f24640190f8fb1953d347b7dc051 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 13:02:01 +0000 Subject: [PATCH 4/5] devx(sdui): re-record the three packages main's #25987 moved (i18n, plugin-grid, plugin-list) Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01SMJGPXxFBpbgAgXdnsXKVS --- scripts/sdui-manifest.sources/i18n.json | 2 +- scripts/sdui-manifest.sources/plugin-grid.json | 2 +- scripts/sdui-manifest.sources/plugin-list.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/sdui-manifest.sources/i18n.json b/scripts/sdui-manifest.sources/i18n.json index 2f1398f2c8c..873a389b2b8 100644 --- a/scripts/sdui-manifest.sources/i18n.json +++ b/scripts/sdui-manifest.sources/i18n.json @@ -1 +1 @@ -{"package":"ui/packages/i18n","sources":31,"sha256":"683731eac82c00f4ffa54852f8ebfd947b5c95ad70847d8e2dc55ff66adbe25a","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} +{"package":"ui/packages/i18n","sources":31,"sha256":"9cd75a1d5d100dec0273df9e1018574cc9188811a2af4963f04abc901ca95a9c","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-grid.json b/scripts/sdui-manifest.sources/plugin-grid.json index 5ba96731fe6..712681a1aa5 100644 --- a/scripts/sdui-manifest.sources/plugin-grid.json +++ b/scripts/sdui-manifest.sources/plugin-grid.json @@ -1 +1 @@ -{"package":"ui/packages/plugin-grid","sources":38,"sha256":"aa105ba15cde7ad140409a327a1d63ddf704662aa2ee53712009950d85527000","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} +{"package":"ui/packages/plugin-grid","sources":38,"sha256":"035da9ddea388a34473ed81a27daa7b5d0036d5a7bafc277c5c55df6552dfdc9","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-list.json b/scripts/sdui-manifest.sources/plugin-list.json index e1b9bdb96a4..d30109e3a4d 100644 --- a/scripts/sdui-manifest.sources/plugin-list.json +++ b/scripts/sdui-manifest.sources/plugin-list.json @@ -1 +1 @@ -{"package":"ui/packages/plugin-list","sources":11,"sha256":"0fbf394f74817e4ce55949400f54b28ccc93fb0896aac6614e9c943b2922332d","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} +{"package":"ui/packages/plugin-list","sources":11,"sha256":"cdf9c194818cb098997160896e43e00b977ee7f09f77b9a77bf9d21a885f1cfe","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} From bc64912dc806b0bc3884bd3b6e8531f859839b70 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 13:20:28 +0000 Subject: [PATCH 5/5] devx(sdui): re-record types, which main's #26019 moved Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01SMJGPXxFBpbgAgXdnsXKVS --- scripts/sdui-manifest.sources/types.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sdui-manifest.sources/types.json b/scripts/sdui-manifest.sources/types.json index 511efb3d3c3..b4a5258ce1e 100644 --- a/scripts/sdui-manifest.sources/types.json +++ b/scripts/sdui-manifest.sources/types.json @@ -1 +1 @@ -{"package":"ui/packages/types","sources":81,"sha256":"64d03d9bda9e93948031fd5e4246e406b10f413027fec12c83c8fc326174b3ba","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} +{"package":"ui/packages/types","sources":81,"sha256":"30832d40db85588aa490e7d3f7b2e2ff689071f4e84176bcab96bacf74060d4f","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}