diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe398c87d89..0b51979a2d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4148,12 +4148,12 @@ jobs: # 81bba6c3f), then it enumerates the registry under plain Node (~8 s; ~6 s # for the whole run when nothing moved). It serialises through the # @objectstack/sdui-parser the step above built, so a parser edit moves - # this verdict too, which no committed digest does. On merge_group it runs - # on the queue's combined tree, the one run that sees two entries' effect - # on the manifest together. Only the artefact is compared: the records the - # generator rewrites beside it are held by `check-sdui-manifest` in - # lint.yml. The Console build below reads the registry through its src/ - # aliases, so the dist this step builds does not reach it. + # this verdict too. On merge_group it runs on the queue's combined tree, + # the one run that sees two entries' effect on the manifest together. + # Only the artefact is compared: the record the generator rewrites beside + # it is held by `check-sdui-manifest` in lint.yml. The Console build below + # reads the registry through its src/ aliases, so the dist this step + # builds does not reach it. - name: The committed SDUI manifest is the one this tree generates run: | node scripts/gen-sdui-manifest-node.mjs diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0acbfe56381..0b1f21fa21a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -966,13 +966,11 @@ jobs: # repo-root `sdui.manifest.json` is absent or unreadable, so nothing in # the production path ever says "the gate lost its whitelist" — this step # is the thing that notices. Offline record checks only (existence, shape, - # sha256 vs the provenance record, and with no pin each registry - # package's source digest vs its record): the regenerate-and-compare half - # runs where the registry is built, as ci.yml's Build & E2E UI step "The - # committed SDUI manifest is the one this tree generates" (#26137), on - # pull_request and on the merge queue's combined tree. Unfiltered: - # filtering it to the artefact would go dormant on exactly the PR that - # moves the artefact's inputs and not the artefact. + # sha256 vs the provenance record): freshness is decided where the + # registry is built, by ci.yml's Build & E2E UI step "The committed SDUI + # manifest is the one this tree generates" (#26137), on pull_request and + # on the merge queue's combined tree. Unfiltered: it builds nothing and + # runs in seconds, and an absent artefact is the failure it exists for. # Direct invocation per the GATE INVOCATION IDIOM at the top of this file. - name: SDUI manifest is present, intact and fresh at the objectui pin id: sdui-manifest-is-present-intact-and-fresh diff --git a/AGENTS.md b/AGENTS.md index 70b6e63b189..529c970a6e1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -159,11 +159,11 @@ The Studio/Console UI lives in this workspace under `ui/` (`@object-ui/*`; the C and `ui/AGENTS.md` adds that tree's own rules to this file's. `pnpm console:build` builds the Console and copies its dist into `packages/console/` (`packages/console/build-dist.sh`). ⛔ Never hand-edit `packages/console/dist/`. -**Moving the pin has a second half: regenerate the committed manifest** — `pnpm objectui:build`, then -`node scripts/gen-sdui-manifest-node.mjs`, which reads objectui's BUILT tree at the pin (never an npm install, whose -version string names an older commit) and takes no arguments; `scripts/check-sdui-manifest.mjs` reds until you do. -`objectui:bump` and `objectui:refresh` print the step, and the console build ships that same tracked file in -`packages/console/dist/`. Full procedure: `docs/releases-maintenance.md`. +**A registry change has a second half: regenerate the committed manifest** with +`node scripts/gen-sdui-manifest-node.mjs`, which builds the registry under `ui/`, enumerates it from `ui/apps/console` +(never an npm install) and takes no arguments; commit what it writes. CI's `Build & E2E UI` runs it too and fails +when the committed `sdui.manifest.json` differs, the merge queue's combined tree included, and the console build +ships that same tracked file in `packages/console/dist/`. Full procedure: `docs/releases-maintenance.md`. **Fast iteration on `ui/` src (no rebuild loop):** run the Console dev server — `pnpm --filter @object-ui/console dev` (Vite on **:5180**, HMR). Its `/api` proxy targets diff --git a/docs/releases-maintenance.md b/docs/releases-maintenance.md index 27068030a28..17c945cb970 100644 --- a/docs/releases-maintenance.md +++ b/docs/releases-maintenance.md @@ -55,12 +55,11 @@ 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, #26051) +#### After a registry change: regenerate the committed manifest (#12924, #17735, #25712, #26137) The repo-root `sdui.manifest.json` (ADR-0080, the Console's public-tier component -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: +manifest) and its provenance record `scripts/sdui-manifest.record.json` are +generated, and one command writes them: ```bash node scripts/gen-sdui-manifest-node.mjs # no arguments @@ -69,31 +68,26 @@ 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`). 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 files on **every PR**: -`node scripts/check-sdui-manifest.mjs` checks the shape, the `sha256` against the -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. - -CI also regenerates the manifest and compares it with the committed bytes (#26137): +`pnpm --filter @objectstack/sdui-parser build`). The record moves only when the +artefact does, so a PR that does not change the manifest touches neither file. +⛔ There is no npm install route to the manifest; that was deleted by ruling 丙 on +#17735. + +CI regenerates the manifest and compares it with the committed bytes (#26137): the `Build & E2E UI` step "The committed SDUI manifest is the one this tree generates" runs the same command after the registry is built, on every PR that touches `ui/`, one of the seven packages objectui consumes (the parser among them) or the manifest's own files, and again on the merge queue's combined tree. A -difference fails the step with the diff and the command to run. That covers what -the source digests cannot see: an edit to `@objectstack/sdui-parser`, to a package -the registry imports, and two queued PRs whose effect on the manifest only shows -together. +difference fails the step with the diff and the command to run. Two PRs conflict +on the manifest only when both change it, and the later one merges `main` and +re-runs the command; two queued PRs whose effect on the manifest only shows +together fail in the queue. + +A required lint step, `node scripts/check-sdui-manifest.mjs`, reads the tracked +files on **every PR**: the artefact is present and is a component manifest, and +its `sha256` and component count are the record's. It builds nothing, so it does +not judge freshness. (The per-package source records under +`scripts/sdui-manifest.sources/` that it used to recompute, #26051, are retired.) **One producer, one artefact.** `packages/console/build-dist.sh` copies the tracked `sdui.manifest.json` into `packages/console/dist/`, so the published console carries the @@ -234,9 +228,9 @@ What it does, in order: re-reads `main`, so **`main` may keep moving for the whole run**. 2. Builds the workspace, both scopes, then the Console (`pnpm run build`, then `pnpm run console:build`) from that snapshot. The dist carries the tracked - `sdui.manifest.json`, whose freshness the required lint job already held on the - PR that changed it. A Console that cannot build stops the cut before anything is - written. + `sdui.manifest.json`, whose freshness the required `Build & E2E UI` job already + held on the PR that changed it and in the merge queue. A Console that cannot build + stops the cut before anything is written. 3. Runs the gates that read `.changeset/*`, before versioning consumes it. 4. Runs `pnpm run version` — the repo script, never a bare `changeset version` — and **fails unless the computed version equals the one you typed**. diff --git a/scripts/check-changeset-no-major.mjs b/scripts/check-changeset-no-major.mjs index e33bd3a7f15..f6625466120 100644 --- a/scripts/check-changeset-no-major.mjs +++ b/scripts/check-changeset-no-major.mjs @@ -1365,9 +1365,8 @@ 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` 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 +// `scripts/sdui-manifest.record.json` is measured OUT for that reason rather +// than by choice: the build does not copy it, so it is not part of the shipped // artefact. // // ⚠️ Residual, stated rather than implied: the reader understands the plain @@ -4876,8 +4875,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' || 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)}`, + !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)}`, ); // ── The owner of a destination ──────────────────────────────────────── @@ -4957,13 +4956,11 @@ 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 records move with regeneration but ship nothing: the shared one, and - // the one-line record of the registry package a ui PR re-recorded. + // The record moves with regeneration but ships nothing. const recordOnly = levelOf( 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']]), }, }), @@ -4971,7 +4968,7 @@ function selfTest() { ); assert( recordOnly.touched.packages.length === 0 && recordOnly.touched.unreadable.length === 0 && recordOnly.result.verdict === 'clean', - `control: the records alone move no published file — got ${JSON.stringify(recordOnly.touched)} / ${recordOnly.result.verdict}`, + `control: the record alone moves 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/check-sdui-manifest.mjs b/scripts/check-sdui-manifest.mjs index 1759ae0dd34..f7a5026665a 100644 --- a/scripts/check-sdui-manifest.mjs +++ b/scripts/check-sdui-manifest.mjs @@ -64,32 +64,16 @@ * 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 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 HERE is the case - * where two PRs on different packages each leave the artefact unchanged - * and their combination would not. The regeneration in ci.yml (below) - * runs on the merge queue's combined tree, and that is where it is - * caught (#26137). + * 3. none HERE. Whether the committed artefact is the one this tree + * generates is decided by generating it, which needs the registry + * built: ci.yml's Build & E2E UI step (below) does that on every PR the + * registry's inputs move and on the merge queue's combined tree + * (#26137). It replaced a source digest per registry package, checked + * here against one-line records under `scripts/sdui-manifest.sources/` + * (#26051): two PRs on one package conflicted on its record even when + * neither moved the artefact, and no digest saw the parser, the + * `@objectstack` packages the registry imports, or two queued PRs + * together. A record that still names those records is RED (1–2). * 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. @@ -106,9 +90,9 @@ * compares the committed artefact with its output byte for byte, on * pull_request and on the merge queue's combined tree. It serialises through * the built `@objectstack/sdui-parser`, so an edit to this repo's OWN adapter - * (`manifestFromConfigs`) moves its verdict, which no digest here does. - * Regeneration is byte-deterministic (two runs over one built tree, identical - * bytes), so that comparison and the remedy this gate prints both converge. + * (`manifestFromConfigs`) moves its verdict too. Regeneration is + * byte-deterministic (two runs over one built tree, identical bytes), so that + * comparison and the remedy this gate prints both converge. * With a pin, the pinned commit's registry is immutable, so content drift * per-PR is not a live axis there. * @@ -153,7 +137,6 @@ import { existsSync, mkdirSync, mkdtempSync, - readdirSync, readFileSync, realpathSync, renameSync, @@ -165,7 +148,6 @@ import { tmpdir } from 'node:os'; import { dirname, join, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { gitFreeEnv } from './git-env.mjs'; import { isEntrypoint } from './invoked-as.mjs'; const DEFAULT_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); @@ -176,28 +158,18 @@ const UI_TREE = 'ui'; /** The in-tree console the generator reads when the tree has no pin (#25712). */ const UI_CONSOLE = 'ui/apps/console'; -/** Where the `@object-ui/*` workspace packages live in the merged layout. */ +/** Where the `@object-ui/*` workspace packages live in the merged layout (display only). */ 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), - * the registry packages (the digests) and their per-package records. + * the same artefact and record plus the in-tree console's manifest (presence). */ const READ_PATHS = { artefact: 'sdui.manifest.json', record: join('scripts', 'sdui-manifest.record.json'), pin: '.objectui-sha', console: join(UI_CONSOLE, 'package.json'), - packages: UI_PACKAGES, - sources: join(...REGISTRY_RECORDS.split('/')), }; /** @@ -232,40 +204,22 @@ 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', 'registryRecords']; +const IN_TREE_FIELDS = ['modulesRoot', 'objectuiWorkspaceVersion']; /** - * 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. + * `inTree` fields of retired freshness layouts. A record still carrying one is + * RED, never read as the same claim: `registryPackages`, `registrySources` and + * `registrySourcesSha256` are the single digest over every registry package + * (retired by #26051), and `registryRecords` named the per-package records + * that replaced it (retired by #26137, when CI began regenerating the + * artefact instead). */ -const RETIRED_IN_TREE_FIELDS = ['registryPackages', 'registrySources', 'registrySourcesSha256']; +const RETIRED_IN_TREE_FIELDS = ['registryPackages', 'registrySources', 'registrySourcesSha256', 'registryRecords']; /** The one command that clears each mode's freshness reds. */ const PIN_REGENERATE = 'pnpm objectui:build && node scripts/gen-sdui-manifest-node.mjs'; const IN_TREE_REGENERATE = 'node scripts/gen-sdui-manifest-node.mjs'; -/** The registry the generator enumerates, besides `@object-ui/core` itself — its `REGISTRY_MODULES`, in its order. */ -const REGISTRY_CORE = '@object-ui/core'; -export const REGISTRY_MODULES = Object.freeze([ - '@object-ui/components', - '@object-ui/plugin-grid', - '@object-ui/plugin-form', - '@object-ui/plugin-view', - '@object-ui/plugin-list', - '@object-ui/plugin-detail', - '@object-ui/plugin-dashboard', - '@object-ui/plugin-charts', - '@object-ui/plugin-kanban', - '@object-ui/plugin-calendar', - '@object-ui/plugin-gantt', - '@object-ui/plugin-timeline', - '@object-ui/plugin-map', - '@object-ui/plugin-markdown', - '@object-ui/plugin-report', - '@object-ui/plugin-tree', -]); - /** * The one producer, repo-relative. Read by the SELF-TEST only — copied into a * fixture tree and run there (see the header) — never by `checkTree`, so it is @@ -274,9 +228,6 @@ export const REGISTRY_MODULES = Object.freeze([ */ const PRODUCER = 'scripts/gen-sdui-manifest-node.mjs'; -/** The repo helpers the producer (and this gate, which it imports in-tree) import: copied beside it in the self-test's fixtures. */ -const PRODUCER_IMPORTS = ['scripts/git-env.mjs', 'scripts/invoked-as.mjs']; - /** * The population this gate reads, declared for `scripts/pm/dispatch-gates.mjs`. * @@ -297,11 +248,10 @@ const PRODUCER_IMPORTS = ['scripts/git-env.mjs', 'scripts/invoked-as.mjs']; * to nothing else. * * ⛔ Not a whole-tree marker, and not `scripts/**`: three files are three files. - * 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. The per-package records (#26051) are a directory, - * declared with the subtree suffix. + * Without a pin it also reads the in-tree console's manifest (#25712); on a + * tree with no `ui/` that hint matches nothing and only displays. A registry + * package edit no longer derives this gate: no digest walks `ui/packages/` + * since #26137, and the CI regeneration holds freshness instead. * * 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 @@ -312,8 +262,6 @@ const ROOT_WATCH_HINTS = [ 'scripts/sdui-manifest.record.json', '.objectui-sha/**', 'ui/apps/console/package.json', - 'ui/packages/**', - 'scripts/sdui-manifest.sources/**', ]; /** @@ -519,12 +467,14 @@ 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 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. + * above, keyed to the in-tree producer's record, and the record's `inTree` + * block in the producer's shape. There is no check 3 here: whether the + * artefact is the one this tree generates is decided by generating it, in + * ci.yml's Build & E2E UI step (#26137). There is no check 4 either: 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 = []; @@ -532,7 +482,7 @@ function checkTreeWithoutPin(root) { problems.push( `.objectui-sha is MISSING and there is no in-tree console (${UI_CONSOLE}) either — no oracle to judge freshness\n` + ' against. With the pin, the manifest is held to objectui\'s build at that commit; without it, to the\n' + - ` registry sources under ${UI_TREE}/. A tree with neither cannot be called fresh.`, + ` registry under ${UI_TREE}/. A tree with neither cannot be called fresh.`, ); return problems; } @@ -551,238 +501,20 @@ function checkTreeWithoutPin(root) { 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.', + `record \`inTree\` still carries ${retired.map((field) => `\`${field}\``).join(', ')} — a retired freshness layout (the\n` + + ' single registry digest, #26051, or the per-package records that replaced it, #26137). No committed digest\n' + + " judges freshness now: ci.yml's Build & E2E UI step regenerates the artefact and compares the bytes.", ); } 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 = registrySourceDigests(root); - if (now.problem) { - problems.push(`cannot digest the registry sources under ${UI_TREE}/: ${now.problem}`); - return problems; - } - 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( - `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}`, - ); } return problems; } -/** Paths under a registry package's `src/` that never reach its build: tests and their mocks. */ -const TEST_SOURCE = /(^|\/)(__tests__|__mocks__)\/|\.(test|spec)\.[cm]?[jt]sx?$/; - -/** - * 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: [{ 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 registrySourceDigests(root) { - const base = join(root, UI_PACKAGES); - if (!existsSync(base)) return { problem: `${UI_PACKAGES}/ does not exist` }; - const byName = new Map(); - for (const dir of readdirSync(base).sort()) { - const manifestPath = join(base, dir, 'package.json'); - if (!existsSync(manifestPath)) continue; - let manifest; - try { - manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); - } catch (e) { - return { problem: `${UI_PACKAGES}/${dir}/package.json does not parse: ${e.message}` }; - } - if (typeof manifest?.name === 'string') byName.set(manifest.name, { dir: `${UI_PACKAGES}/${dir}`, manifest }); - } - - const seeds = [REGISTRY_CORE, ...REGISTRY_MODULES]; - const missing = seeds.filter((name) => !byName.has(name)); - if (missing.length > 0) return { problem: `registry module(s) ${missing.join(', ')} are not packages under ${UI_PACKAGES}/` }; - const reached = new Set(); - const queue = [...seeds]; - while (queue.length > 0) { - const name = queue.shift(); - if (reached.has(name)) continue; - reached.add(name); - const { manifest } = byName.get(name); - for (const field of ['dependencies', 'peerDependencies', 'optionalDependencies']) { - for (const dep of Object.keys(manifest[field] ?? {})) if (byName.has(dep)) queue.push(dep); - } - } - const dirs = [...reached].map((name) => byName.get(name).dir).sort(); - - let listing; - try { - 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(), - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'pipe'], - maxBuffer: 64 * 1024 * 1024, - }); - } catch (e) { - 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') - .filter(Boolean) - .map((line) => { - const tab = line.indexOf('\t'); - return { path: line.slice(tab + 1), blob: line.slice(0, tab).split(' ')[1] }; - }) - .filter(({ path }) => !TEST_SOURCE.test(path)); - 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 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 // refuses anything else: a `return` that leaves the function above that line // prints nothing and still exits 0 — a self-test that never finished, reported @@ -829,22 +561,14 @@ const SELF_TEST_BATTERIES = Object.freeze({ '--require-objectui turns an unreachable checkout RED': 1, 'a pinned commit with no packages/core/package.json is RED': 1, 'the producer, run under a TMPDIR behind a symlink, writes a tree this gate passes': 1, - 'no pin: an in-tree record over unchanged registry sources passes': 1, + 'no pin: an in-tree record 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 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 record from the per-package 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, + 'no pin: a producer run whose artefact did not move leaves the record as it was': 1, }); // DELETING an entry silences that battery's floor exactly as effectively as @@ -853,7 +577,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 = 29; +const SELF_TEST_BATTERY_FLOOR = 21; function selfTest() { const mk = (mutate, { pin = 'a'.repeat(40), version = '0.0.0-selftest' } = {}) => { @@ -955,7 +679,7 @@ function selfTest() { } const producerSource = readFileSync(join(DEFAULT_ROOT, PRODUCER), 'utf8'); - for (const file of [PRODUCER, ...PRODUCER_IMPORTS]) copyFileSync(join(DEFAULT_ROOT, file), join(root, file)); + copyFileSync(join(DEFAULT_ROOT, PRODUCER), join(root, PRODUCER)); const parserDist = join(root, 'packages', 'sdui-parser', 'dist'); mkdirSync(parserDist, { recursive: true }); writeFileSync( @@ -997,101 +721,77 @@ function selfTest() { const produced = mkProduced(); /** - * The merged layout (#25712): no pin, an in-tree console, and one package - * per registry name under `ui/packages/`, each with one source — plus - * `@object-ui/types`, which `@object-ui/core` reaches through a dependency - * (so the closure, not only the seed list, is under test), and - * `@object-ui/unrelated`, which nothing reaches. A real git repository: the - * digest reads the INDEX, so a stub would leave exactly that reading - * unexercised. Every git child here runs with `gitFreeEnv()` (#16644). + * The merged layout (#25712): no pin, an in-tree console, an artefact and + * the in-tree producer's record. Nothing under `ui/packages/`: this gate + * reads none of it since #26137. */ - const UI_FIXTURE_NAMES = [REGISTRY_CORE, ...REGISTRY_MODULES, '@object-ui/types', '@object-ui/unrelated']; - const pkgDir = (name) => join(UI_PACKAGES, name.slice('@object-ui/'.length)); - const gitIn = (root) => (...args) => - execFileSync('git', ['-C', root, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], env: gitFreeEnv() }); - const mkUiSources = (prefix) => { - const root = mkdtempSync(join(tmpdir(), prefix)); - const git = gitIn(root); - git('init', '-q'); - writeFileSync(join(root, '.gitignore'), 'node_modules/\n'); + const mkInTree = (mutate) => { + const root = mkdtempSync(join(tmpdir(), 'sdui-manifest-in-tree-')); mkdirSync(join(root, UI_CONSOLE), { recursive: true }); + mkdirSync(join(root, 'scripts'), { recursive: true }); writeFileSync(join(root, READ_PATHS.console), JSON.stringify({ name: '@object-ui/console', private: true }, null, 2)); - for (const name of UI_FIXTURE_NAMES) { - const dir = join(root, pkgDir(name)); - mkdirSync(join(dir, 'src'), { recursive: true }); - const dependencies = name === REGISTRY_CORE ? { '@object-ui/types': 'workspace:*' } : {}; - writeFileSync(join(dir, 'package.json'), JSON.stringify({ name, version: '0.0.0-selftest', dependencies }, null, 2)); - writeFileSync(join(dir, 'src', 'index.ts'), `export const id = ${JSON.stringify(name)};\n`); - } - git('add', '-A'); - return { root, git }; - }; - const mkInTree = (mutate) => { - 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, READ_PATHS.sources), { recursive: true }); writeFileSync(join(root, READ_PATHS.artefact), raw); - 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, + sha256: createHash('sha256').update(raw).digest('hex'), components: 1, - inTree: { modulesRoot: UI_CONSOLE, objectuiWorkspaceVersion: '0.0.0-selftest', registryRecords: REGISTRY_RECORDS }, + inTree: { modulesRoot: UI_CONSOLE, objectuiWorkspaceVersion: '0.0.0-selftest' }, }, null, 2, ), ); - mutate?.(root, git); + mutate?.(root); return root; }; const editRecord = (root, edit) => { 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 - * two helpers they import, copied into the merged-layout fixture beside a - * stub adapter and a stub `node_modules/.bin/turbo`. The stub is what writes - * `@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. + * The PRODUCER's in-tree run (no pin): the real generator, copied into a + * merged-layout fixture beside a stub adapter and a stub + * `node_modules/.bin/turbo`. The stub is what writes `@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. The stub names are read off the producer's source, as + * `mkProduced` reads them. * * With `rerunAfter`, the producer runs a second time after that edit, and - * the returned `moved` lists every path the producer writes whose bytes the - * second run changed — the re-record row's reading. + * the returned `moved` lists each file the producer writes whose bytes the + * second run changed — the carry row's reading. */ const mkProducedInTree = ({ rerunAfter } = {}) => { - const { root, git } = mkUiSources('sdui-manifest-in-tree-producer-'); + const root = mkdtempSync(join(tmpdir(), 'sdui-manifest-in-tree-producer-')); + mkdirSync(join(root, UI_CONSOLE), { recursive: true }); 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)); - } + writeFileSync(join(root, READ_PATHS.console), JSON.stringify({ name: '@object-ui/console', private: true }, null, 2)); + const producerSource = readFileSync(join(DEFAULT_ROOT, PRODUCER), 'utf8'); + copyFileSync(join(DEFAULT_ROOT, PRODUCER), join(root, PRODUCER)); const parserDist = join(root, 'packages', 'sdui-parser', 'dist'); mkdirSync(parserDist, { recursive: true }); writeFileSync( join(parserDist, 'index.mjs'), 'export const manifestFromConfigs = (configs) => ({ components: Object.fromEntries(configs.map((c) => [c.type, { type: c.type, inputs: [] }])) });\n', ); - for (const name of [REGISTRY_CORE, ...REGISTRY_MODULES]) { + const core = '@object-ui/core'; + const names = new Set([core, ...[...producerSource.matchAll(/'(@object-ui\/[a-z0-9-]+)'/g)].map((m) => m[1])]); + for (const name of names) { const pkg = join(root, UI_CONSOLE, 'node_modules', ...name.split('/')); mkdirSync(join(pkg, 'dist'), { recursive: true }); writeFileSync( join(pkg, 'package.json'), JSON.stringify({ name, version: '0.0.0-selftest', type: 'module', exports: { '.': { import: './dist/index.js' } } }), ); - if (name !== REGISTRY_CORE) writeFileSync(join(pkg, 'dist', 'index.js'), 'export {};\n'); + if (name !== core) writeFileSync(join(pkg, 'dist', 'index.js'), 'export {};\n'); } - const coreDist = `${UI_CONSOLE}/node_modules/${REGISTRY_CORE}/dist`; + const coreDist = `${UI_CONSOLE}/node_modules/${core}/dist`; mkdirSync(join(root, 'node_modules', '.bin'), { recursive: true }); writeFileSync( join(root, 'node_modules', '.bin', 'turbo'), @@ -1100,7 +800,7 @@ function selfTest() { { mode: 0o755 }, ); const produce = () => { - const run = spawnSync(process.execPath, [join(root, PRODUCER)], { encoding: 'utf8', env: gitFreeEnv() }); + const run = spawnSync(process.execPath, [join(root, PRODUCER)], { encoding: 'utf8' }); 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}`); @@ -1108,41 +808,38 @@ function selfTest() { }; 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])); - }; - rerunAfter(root, git); + const written = () => + new Map( + [READ_PATHS.artefact, READ_PATHS.record].map((file) => [file.split(sep).join('/'), existsSync(join(root, file)) ? readFileSync(join(root, file), 'utf8') : null]), + ); + rerunAfter(root); const first = written(); produce(); const second = written(); - const moved = [...new Set([...first.keys(), ...second.keys()])].filter((file) => first.get(file) !== second.get(file)).sort(); + const moved = [...first.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, 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'); + // The carry row: the record describes an ARTEFACT, so a run whose artefact + // did not move must leave the record as it was, or every ui PR that + // regenerates rewrites the one shared file and any two of them conflict on + // it. Between the runs the record is back-dated, as a run on an earlier day + // left it, and the built `@object-ui/core` moves to another version, as a + // release of the fixed group moves it: `generatedAt` and the provenance + // version are carried while the artefact is byte-identical, so a producer + // that stamps today's date or re-reads the version names the record here. + const carried = mkProducedInTree({ + rerunAfter: (r) => { const p = join(r, READ_PATHS.record); writeFileSync(p, JSON.stringify({ ...JSON.parse(readFileSync(p, 'utf8')), generatedAt: '2000-01-01' }, null, 2) + '\n'); + const core = join(r, UI_CONSOLE, 'node_modules', '@object-ui', 'core', 'package.json'); + writeFileSync(core, JSON.stringify({ ...JSON.parse(readFileSync(core, 'utf8')), version: '0.0.1-moved' })); }, }); - const rerecordedWant = [`${REGISTRY_RECORDS}/plugin-grid.json`]; - const rerecordedProblems = JSON.stringify(rerecorded.moved) === JSON.stringify(rerecordedWant) + const carriedProblems = carried.moved.length === 0 ? [] - : [`the second producer run changed ${JSON.stringify(rerecorded.moved)}, not exactly ${JSON.stringify(rerecordedWant)}.`]; + : [`the second producer run, over an artefact that did not move, changed ${JSON.stringify(carried.moved)}.`]; const cases = [ ['green fixture passes', mk(), 0, blind], @@ -1194,7 +891,7 @@ function selfTest() { // The fifth column is text every problem of a RED row must carry between // them: a red that does not name both missing oracles, or the one command // that clears it, is a red its reader cannot act on. - ['no pin: an in-tree record over unchanged registry sources passes', mkInTree(), 0, blind], + ['no pin: an in-tree record passes', mkInTree(), 0, blind], ['no pin and no in-tree console is RED, naming both', mkInTree((r) => rmSync(join(r, UI_CONSOLE), { recursive: true })), 1, blind, [READ_PATHS.pin, UI_CONSOLE]], // The tree the pin deletion lands on before it regenerates: the record is // the pin producer's, whole and intact, and still not this mode's. @@ -1204,67 +901,19 @@ function selfTest() { modulesRoot: `.cache/objectui-${'a'.repeat(12)}/apps/console`, objectuiWorkspaceVersion: '0.0.0-selftest', }))), 1, blind, ['`inTree`', IN_TREE_REGENERATE]], - // 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, ['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'); - }), 0, blind], - ['no pin: a test file or a package the registry does not reach is not a registry change', mkInTree((r, git) => { - mkdirSync(join(r, pkgDir('@object-ui/plugin-grid'), 'src', '__tests__'), { recursive: true }); - writeFileSync(join(r, pkgDir('@object-ui/plugin-grid'), 'src', '__tests__', 'grid.ts'), 'export {};\n'); - writeFileSync(join(r, pkgDir('@object-ui/types'), 'src', 'types.spec.tsx'), 'export {};\n'); - writeFileSync(join(r, pkgDir('@object-ui/unrelated'), 'src', 'more.ts'), 'export {};\n'); - git('add', '-A'); - }), 0, blind], - ['no pin: a new workspace edge into the registry is RED', mkInTree((r) => { - 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, ['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]], + // A branch cut before #26137 that regenerated keeps writing the field that + // named the per-package records; its record is that layout's claim, RED. + ['no pin: a record from the per-package layout is RED with the in-tree command', mkInTree((r) => editRecord(r, (rec) => ({ + ...rec, + inTree: { ...rec.inTree, registryRecords: 'scripts/sdui-manifest.sources' }, + }))), 1, blind, ['`registryRecords`', 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.root, 0, blind], - ['no pin: the producer re-records only the package whose sources moved', rerecorded.root, 0, { ...blind, extra: rerecordedProblems }], + ['no pin: a producer run whose artefact did not move leaves the record as it was', carried.root, 0, { ...blind, extra: carriedProblems }], ]; // The ledger this self-test's floor is evaluated against (#13489). @@ -1274,20 +923,17 @@ function selfTest() { }; let failures = 0; - for (const [name, root, want, { extra = [], ...options } = {}, mustName = [], mustNotName = []] of cases) { + for (const [name, root, want, { extra = [], ...options } = {}, mustName = []] of cases) { registerCase(name); const problems = [...checkTree(root, options), ...extra]; const got = problems.length ? 1 : 0; const unnamed = got ? mustName.filter((text) => !problems.join('\n').includes(text)) : []; - const misnamed = got ? mustNotName.filter((text) => problems.join('\n').includes(text)) : []; - if (got !== want || unnamed.length > 0 || misnamed.length > 0) { + if (got !== want || unnamed.length > 0) { failures++; console.error( got !== want ? `✗ self-test: ${name} — expected ${want ? 'RED' : 'GREEN'}, got ${got ? 'RED' : 'GREEN'}` - : 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(', ')}`, + : `✗ self-test: ${name} — RED, but no problem names ${unnamed.map((text) => JSON.stringify(text)).join(', ')}`, ); for (const p of problems) console.error(` ${p}`); } @@ -1311,15 +957,6 @@ function selfTest() { declFail('a repo-ROOT file is declared without the subtree suffix — a bare single-segment literal is ' + 'refused by hintCovers as too generic, so it would contribute no hint at all.'); } - // The digest is only a reading of what the producer enumerates while the two - // registry lists are one list. The producer refuses a mismatch on its own - // in-tree run; this holds it on every run, with or without a pin. - const producerList = /const REGISTRY_MODULES = \[([^\]]*)\];/.exec(readFileSync(join(DEFAULT_ROOT, PRODUCER), 'utf8')); - const producerModules = producerList ? [...producerList[1].matchAll(/'(@object-ui\/[a-z0-9-]+)'/g)].map((m) => m[1]) : []; - if (JSON.stringify(producerModules) !== JSON.stringify(REGISTRY_MODULES)) { - declFail(`REGISTRY_MODULES here and in ${PRODUCER} disagree (or its list is unreadable) — the in-tree digest ` - + 'would describe a different registry than the one the producer enumerates.'); - } // ── The floor: every declared row RAN, and ran its case (#13489) ─────── // @@ -1384,11 +1021,10 @@ 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 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).', + 'the producer, run under a symlinked TMPDIR, writes a tree that passes; with no pin, an in-tree record passes, a pin-mode ' + + 'record, a record from either retired freshness layout and a hand-edited artefact are RED with the in-tree command, no pin ' + + 'and no console is RED naming both, and the in-tree producer builds, writes a tree that passes, and leaves the record as ' + + 'it was when the artefact did not move).', ); return SELF_TEST_VERDICT; @@ -1417,7 +1053,7 @@ if (isEntrypoint(import.meta.url)) { } const record = JSON.parse(readFileSync(join(DEFAULT_ROOT, 'scripts', 'sdui-manifest.record.json'), 'utf8')); if (!existsSync(join(DEFAULT_ROOT, READ_PATHS.pin))) { - // No pin: checkTree ran the in-tree leg, which has no check 4 to report. + // No pin: checkTree ran the in-tree leg, which has no check 3 or 4 to report. const { inTree } = record; let declared = 'unreadable'; try { @@ -1425,14 +1061,13 @@ 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: ${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).`, + `✓ check-sdui-manifest: sdui.manifest.json is present and intact (sha256 ${String(record.sha256).slice(0, 12)}…, ` + + `${record.components} components), recorded by the in-tree producer from ${inTree.modulesRoot} (no .objectui-sha in this tree).`, + ); + console.log( + " Not judged here: whether it is the manifest this tree generates. ci.yml's Build & E2E UI step regenerates it and " + + 'compares the bytes (#26137).', ); 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 6b523119638..4bc01b2e0a7 100644 --- a/scripts/gen-sdui-manifest-node.mjs +++ b/scripts/gen-sdui-manifest-node.mjs @@ -71,31 +71,29 @@ * Once objectui's tree lives under `ui/` and `.objectui-sha` is gone (#25677), * the modules root is `ui/apps/console` and the registry is this tree's own. * The run is then one command with no prior step: it builds the registry - * 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 where the per-package records live — and + * closure with turbo (`buildInTree`, a cache hit when nothing moved), then + * enumerates exactly as above. The record gains an `inTree` block — the + * modules root and the version read off the build (provenance only) — 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) + * ## The record moves only with the artefact * - * 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. + * The record describes an ARTEFACT, so an in-tree run whose artefact is + * byte-identical to the recorded one leaves it as it was: `generatedAt` and the + * provenance version are carried. A ui PR that does not change the manifest + * then touches neither file, and two such PRs do not conflict. Two PRs that + * both change it conflict on it, on purpose: only a regeneration on the merged + * tree can say what the manifest of the combination is. CI runs this script on + * every PR the registry's inputs move and on the merge queue's combined tree, + * and fails when the committed artefact differs from what it writes (ci.yml's + * Build & E2E UI step "The committed SDUI manifest is the one this tree + * generates", #26137). That comparison replaced the per-package source records + * under `scripts/sdui-manifest.sources/` (#26051), which this script wrote and + * `scripts/check-sdui-manifest.mjs` recomputed. * - * 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 + * It BUILDS on every run: the build and the enumeration are the only oracle + * that says whether the artefact moved. 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 @@ -104,13 +102,11 @@ import { execFileSync, spawnSync } from 'node:child_process'; import { createHash } from 'node:crypto'; -import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'; +import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { pathToFileURL, fileURLToPath } from 'node:url'; -import { gitFreeEnv } from './git-env.mjs'; - const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const RECORD_PATH = join(ROOT, 'scripts', 'sdui-manifest.record.json'); const OUT_PATH = join(ROOT, 'sdui.manifest.json'); @@ -159,42 +155,23 @@ 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. 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.', + "@objectstack/sdui-parser. CI regenerates it in ci.yml's Build & E2E UI step and fails when the committed bytes differ", + "(#26137); `scripts/check-sdui-manifest.mjs` holds this record's shape and sha256. This record moves only with the", + 'artefact. `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', ]; -/** Lines of `git status --porcelain` under `dirs` that the INDEX does not hold: unstaged edits and untracked files. */ -function unindexed(dirs) { - const status = execFileSync('git', ['-C', ROOT, 'status', '--porcelain', '--untracked-files=all', '--', ...dirs], { - // #16644: `-C ROOT` alone names the repository — the gate's self-test runs - // this file inside a mkdtemp fixture, where an inherited GIT_DIR outranks it. - env: gitFreeEnv(), - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'pipe'], - maxBuffer: 64 * 1024 * 1024, - }); - return status.split('\n').filter((line) => line.length > 2 && line[1] !== ' '); -} - /** * No pin — the merged layout (#25712): build the registry's packages from this - * 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. + * tree's own sources, then read that build. * - * 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 - * cache hit. Refused before anything is built: no in-tree console, a registry - * list that disagrees with the gate's, unreadable sources, or bytes under them - * the index does not hold (the digest reads the index, the build the working - * tree). Refused after: a build that moved them. + * The build is THIS script's step here, not the caller's: only a build of the + * sources as they stand makes the artefact describe them, and a run over a + * stale dist would write a stale manifest. Turbo's input hashing makes an + * up-to-date build a cache hit. Refused before anything is built: no in-tree + * console, or no turbo. Refused after: a build that left the console without + * the registry's dist. */ async function buildInTree() { if (!existsSync(join(ROOT, ...UI_CONSOLE.split('/'), 'package.json'))) { @@ -203,47 +180,20 @@ async function buildInTree() { ' With the pin this reads objectui\'s build at that commit; without it, the console under ui/.', ); } - const gate = await import(new URL('./check-sdui-manifest.mjs', import.meta.url).href); - if (JSON.stringify(gate.REGISTRY_MODULES) !== JSON.stringify(REGISTRY_MODULES)) { - fail( - 'REGISTRY_MODULES here and in scripts/check-sdui-manifest.mjs disagree, so the digest the gate recomputes\n' + - ' would describe a different registry than the one enumerated here. Make the two lists equal.', - ); - } - 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 notIndexed = unindexed(sources); - if (notIndexed.length > 0) { - fail( - 'the registry sources carry bytes the index does not hold, so the build would read what the digest does not name:\n' + - notIndexed.slice(0, 20).map((line) => ` ${line}`).join('\n') + - (notIndexed.length > 20 ? `\n … and ${notIndexed.length - 20} more` : '') + - '\n Stage or discard them, then re-run.', - ); - } const turbo = join(ROOT, 'node_modules', '.bin', 'turbo'); 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.packages.reduce((sum, { files }) => sum + files, 0)} source(s) (turbo — a cache hit when nothing moved)`, - ); + console.error(`→ building the registry from this tree: @object-ui/core and ${REGISTRY_MODULES.length} module(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, stdio: ['ignore', 2, 2], }); if (build.status !== 0) fail(`the in-tree build exited ${build.status ?? build.signal}; its output is above.`); - 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 { gate, packages: before.packages }; } if (process.argv.length > 2) { @@ -264,7 +214,6 @@ const pinned = existsSync(pinPath); let pin = null; let MODULES_ROOT_REL; -let inTree = null; if (pinned) { if (!existsSync(PARSER_DIST)) fail(PARSER_DIST_MISSING); @@ -296,7 +245,7 @@ if (pinned) { ); } } else { - inTree = await buildInTree(); + await buildInTree(); if (!existsSync(PARSER_DIST)) fail(PARSER_DIST_MISSING); MODULES_ROOT_REL = UI_CONSOLE; } @@ -399,8 +348,8 @@ const sha256 = createHash('sha256').update(json).digest('hex'); // 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. +// diff of a ui PR that does not change the manifest. `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 = { @@ -418,34 +367,9 @@ const nextRecord = { : { modulesRoot: MODULES_ROOT_REL, objectuiWorkspaceVersion: carried.inTree?.objectuiWorkspaceVersion ?? workspaceVersion, - registryRecords: inTree.gate.REGISTRY_RECORDS, }, }; -// 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); @@ -456,7 +380,6 @@ if (pinned) { `@object-ui/core workspace version ${workspaceVersion}`, ); } else { - const { packages } = inTree; 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}` + @@ -464,11 +387,4 @@ if (pinned) { '; ' + (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.`, - ); } diff --git a/scripts/sdui-manifest.record.json b/scripts/sdui-manifest.record.json index b4080228dd7..7a9a51a232c 100644 --- a/scripts/sdui-manifest.record.json +++ b/scripts/sdui-manifest.record.json @@ -5,12 +5,10 @@ "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. 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.", + "@objectstack/sdui-parser. CI regenerates it in ci.yml's Build & E2E UI step and fails when the committed bytes differ", + "(#26137); `scripts/check-sdui-manifest.mjs` holds this record's shape and sha256. This record moves only with the", + "artefact. `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", @@ -20,7 +18,6 @@ "components": 106, "inTree": { "modulesRoot": "ui/apps/console", - "objectuiWorkspaceVersion": "18.1.0", - "registryRecords": "scripts/sdui-manifest.sources" + "objectuiWorkspaceVersion": "18.1.0" } } diff --git a/scripts/sdui-manifest.sources/components.json b/scripts/sdui-manifest.sources/components.json deleted file mode 100644 index e705b060750..00000000000 --- a/scripts/sdui-manifest.sources/components.json +++ /dev/null @@ -1 +0,0 @@ -{"package":"ui/packages/components","sources":226,"sha256":"2321bcd998e35f7367cbab8745cb1837768ff5dec8a5684709b8e06e13be6c69","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/core.json b/scripts/sdui-manifest.sources/core.json deleted file mode 100644 index 8fe6fe5a4f1..00000000000 --- a/scripts/sdui-manifest.sources/core.json +++ /dev/null @@ -1 +0,0 @@ -{"package":"ui/packages/core","sources":112,"sha256":"810fe0e8f6db868fd443540cb2e30a85f691a48a08d58ba7ba8db0c577d579e7","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/fields.json b/scripts/sdui-manifest.sources/fields.json deleted file mode 100644 index 112d644e064..00000000000 --- a/scripts/sdui-manifest.sources/fields.json +++ /dev/null @@ -1 +0,0 @@ -{"package":"ui/packages/fields","sources":96,"sha256":"14f77ca68180dba0ba0226c934cc41bbb0c4d317a1472fb3512d75cf3b4ab7ac","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/i18n.json b/scripts/sdui-manifest.sources/i18n.json deleted file mode 100644 index 2d5791a08d1..00000000000 --- a/scripts/sdui-manifest.sources/i18n.json +++ /dev/null @@ -1 +0,0 @@ -{"package":"ui/packages/i18n","sources":31,"sha256":"a5459185e0836b6ae5f87ff81d0e75d2be96455c5f18661c47c12c8f49d81871","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/mobile.json b/scripts/sdui-manifest.sources/mobile.json deleted file mode 100644 index e513411ece6..00000000000 --- a/scripts/sdui-manifest.sources/mobile.json +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index 3ea3b1a1507..00000000000 --- a/scripts/sdui-manifest.sources/permissions.json +++ /dev/null @@ -1 +0,0 @@ -{"package":"ui/packages/permissions","sources":11,"sha256":"ac8f73ebbacf1fcb6939ac59bf5969403e111303de0e93febbaaeebbe9c1edf2","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-calendar.json b/scripts/sdui-manifest.sources/plugin-calendar.json deleted file mode 100644 index f2aa7100032..00000000000 --- a/scripts/sdui-manifest.sources/plugin-calendar.json +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index 0ff7a1dac09..00000000000 --- a/scripts/sdui-manifest.sources/plugin-charts.json +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index 5371ccddf7a..00000000000 --- a/scripts/sdui-manifest.sources/plugin-dashboard.json +++ /dev/null @@ -1 +0,0 @@ -{"package":"ui/packages/plugin-dashboard","sources":33,"sha256":"20c947f73887cf0051d66da0c9daca4045ead2401c7adf47a0b15cad9c51efc7","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-detail.json b/scripts/sdui-manifest.sources/plugin-detail.json deleted file mode 100644 index 8a4ae13d597..00000000000 --- a/scripts/sdui-manifest.sources/plugin-detail.json +++ /dev/null @@ -1 +0,0 @@ -{"package":"ui/packages/plugin-detail","sources":54,"sha256":"f0ccf59f3607d2208796fa3fbbd3d176b7ad09850cbd1e6feca0cc46eb108895","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-form.json b/scripts/sdui-manifest.sources/plugin-form.json deleted file mode 100644 index f6970ea07e2..00000000000 --- a/scripts/sdui-manifest.sources/plugin-form.json +++ /dev/null @@ -1 +0,0 @@ -{"package":"ui/packages/plugin-form","sources":41,"sha256":"ce9bbd902f3df3f7ac87d6102fcb3489440234ec1741aff5c5518bb94e648687","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-gantt.json b/scripts/sdui-manifest.sources/plugin-gantt.json deleted file mode 100644 index edaaf51fb22..00000000000 --- a/scripts/sdui-manifest.sources/plugin-gantt.json +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index 0e5567fe367..00000000000 --- a/scripts/sdui-manifest.sources/plugin-grid.json +++ /dev/null @@ -1 +0,0 @@ -{"package":"ui/packages/plugin-grid","sources":46,"sha256":"c0a562a633d4901dcde8bad54819f468c8ed4d7ca4f002cf7defa764ebfcd7d8","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-kanban.json b/scripts/sdui-manifest.sources/plugin-kanban.json deleted file mode 100644 index 385520ee88e..00000000000 --- a/scripts/sdui-manifest.sources/plugin-kanban.json +++ /dev/null @@ -1 +0,0 @@ -{"package":"ui/packages/plugin-kanban","sources":17,"sha256":"ce612105cd39bf19c3666925ffc9f77669f466feb09fc85f69fa3ebe558863cf","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-list.json b/scripts/sdui-manifest.sources/plugin-list.json deleted file mode 100644 index d30109e3a4d..00000000000 --- a/scripts/sdui-manifest.sources/plugin-list.json +++ /dev/null @@ -1 +0,0 @@ -{"package":"ui/packages/plugin-list","sources":11,"sha256":"cdf9c194818cb098997160896e43e00b977ee7f09f77b9a77bf9d21a885f1cfe","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-map.json b/scripts/sdui-manifest.sources/plugin-map.json deleted file mode 100644 index 00045bf6d45..00000000000 --- a/scripts/sdui-manifest.sources/plugin-map.json +++ /dev/null @@ -1 +0,0 @@ -{"package":"ui/packages/plugin-map","sources":4,"sha256":"7e7bcc627a2cf9334b56fe20b1b97ab2732eec9f1e5fe77ede4e570bde95d6b1","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/plugin-markdown.json b/scripts/sdui-manifest.sources/plugin-markdown.json deleted file mode 100644 index 120a379bd9f..00000000000 --- a/scripts/sdui-manifest.sources/plugin-markdown.json +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index 218194d58f1..00000000000 --- a/scripts/sdui-manifest.sources/plugin-report.json +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index a6af9361bad..00000000000 --- a/scripts/sdui-manifest.sources/plugin-timeline.json +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index 0f121ebfed8..00000000000 --- a/scripts/sdui-manifest.sources/plugin-tree.json +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index 5f347f28e90..00000000000 --- a/scripts/sdui-manifest.sources/plugin-view.json +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index 2c3b8a0480d..00000000000 --- a/scripts/sdui-manifest.sources/providers.json +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index e41d5efcacb..00000000000 --- a/scripts/sdui-manifest.sources/react-runtime.json +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index fa655b7e874..00000000000 --- a/scripts/sdui-manifest.sources/react.json +++ /dev/null @@ -1 +0,0 @@ -{"package":"ui/packages/react","sources":70,"sha256":"d7a6fb1f906784fd1c2d04592bf54d095db68a222bc5dce8b4747f944288a944","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/sdui-parser.json b/scripts/sdui-manifest.sources/sdui-parser.json deleted file mode 100644 index 32deb03f6a7..00000000000 --- a/scripts/sdui-manifest.sources/sdui-parser.json +++ /dev/null @@ -1 +0,0 @@ -{"package":"ui/packages/sdui-parser","sources":10,"sha256":"57c2952e2ffa03ee0144f1b26fb0af73272c684004f8e8dd689b2b061243509e","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"} diff --git a/scripts/sdui-manifest.sources/types.json b/scripts/sdui-manifest.sources/types.json deleted file mode 100644 index ea9118118b3..00000000000 --- a/scripts/sdui-manifest.sources/types.json +++ /dev/null @@ -1 +0,0 @@ -{"package":"ui/packages/types","sources":81,"sha256":"f8450c19832a17c2f45e0164ec9e6e68cb247efdc00c81998c77e8c42c11da65","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}