diff --git a/packages/spec/scripts/check-react-blocks-declaration-parity.test.ts b/packages/spec/scripts/check-react-blocks-declaration-parity.test.ts index 2b92631643..f53bfe5340 100644 --- a/packages/spec/scripts/check-react-blocks-declaration-parity.test.ts +++ b/packages/spec/scripts/check-react-blocks-declaration-parity.test.ts @@ -42,6 +42,12 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { + REPO_MANIFEST_RELATIVE, + manifestPrescription, + repoManifestIsCheckedIn, + repoManifestPath, +} from './manifest-prescription'; const HERE = path.dirname(fileURLToPath(import.meta.url)); const PKG = path.resolve(HERE, '..'); @@ -296,9 +302,14 @@ describe('check:react-declaration-parity — the gate CAN go red (#4690)', () => expect(status, output).toBe(1); expect(output).toMatch(/did NOT run/); expect(output).not.toMatch(/skipping/); - // The refusal has to be actionable: the manifest's producer lives in another repo. - expect(output).toContain('pnpm sdui:manifest'); - expect(output).toContain('OBJECTUI_ROOT=../objectui'); + // The refusal has to be actionable. WHICH prescription is actionable depends on + // the repository the script is standing in, so this only asserts the branch that + // is true HERE — a manifest is checked in at the root — and the prescription's + // own tests below cover both. Asserting the dump path unconditionally is how + // #16715 stayed invisible: the assertion agreed with the prose, and both were + // stale in the same direction. + expect(output).toContain('MANIFEST="$PWD/sdui.manifest.json"'); + expect(output).toContain('pnpm --filter @objectstack/spec check:react-declaration-parity'); }); it('a MANIFEST path that does not exist fails loudly, naming the path', { timeout: SPAWN_TIMEOUT_MS }, () => { @@ -491,3 +502,123 @@ describe('check:react-declaration-parity — the node contract, and its calibrat expect(status, output).toBe(0); }); }); + +/** + * THE REFUSAL'S PRESCRIPTION IS PROBED, AND BOTH WORLDS ARE PINNED (#16715). + * + * The gate's refusal text used to be a constant asserting "this repository contains + * no copy of it" and sending the reader to build objectui and dump one in a browser. + * That was false from #13446 onward — `sdui.manifest.json` is checked in at the root + * and `lint.yml` runs this gate against it — and it is read at the exact moment + * someone is deciding whether the gate can run at all. Twice measured (#16489 / PR + * #16697, and PR #16777) a dev believed it and filed a locally-runnable gate as + * `EXTERNAL_INPUT_REQUIRED` / NOT MEASURED; one of those declarations reached a + * deliverable, on a head where setting one variable gave exit 0. + * + * ⭐ THE ONE-BRANCH SHAPE IS THE DEFECT, so testing one branch would rebuild it. The + * probe is a real filesystem check against a temp root here — both answers are + * producible — and the wiring (that the script probes at all, and prints the + * paste-ready command in THIS repository) is asserted through a real spawn above and + * below. Neither branch may assert the other's world: that is what each `not` pins. + */ +describe('check:react-declaration-parity — the prescription is probed, not asserted (#16715)', () => { + /** A root that has a manifest, and one that does not — the two worlds, on disk. */ + function withRoots(fn: (roots: { withManifest: string; without: string }) => void): void { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'react-parity-prescription-')); + try { + const withManifest = path.join(dir, 'has-one'); + const without = path.join(dir, 'has-none'); + fs.mkdirSync(withManifest); + fs.mkdirSync(without); + fs.writeFileSync(path.join(withManifest, REPO_MANIFEST_RELATIVE), '{"components":{}}', 'utf8'); + fn({ withManifest, without }); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + } + + it('the probe answers from the filesystem, both ways', () => { + withRoots(({ withManifest, without }) => { + expect(repoManifestIsCheckedIn(withManifest)).toBe(true); + expect(repoManifestIsCheckedIn(without)).toBe(false); + expect(repoManifestPath(withManifest)).toBe(path.join(withManifest, 'sdui.manifest.json')); + }); + }); + + it('BRANCH: manifest checked in — hands over a paste-ready command, not a production errand', () => { + withRoots(({ withManifest }) => { + const text = manifestPrescription({ repoRoot: withManifest, checkedIn: true }); + // The command CI runs, spelled as CI spells it, plus the `cd` that makes `$PWD` + // true — so the block works pasted from any working directory. + expect(text).toContain('MANIFEST="$PWD/sdui.manifest.json"'); + expect(text).toContain(`cd ${withManifest}`); + expect(text).toContain('pnpm --filter @objectstack/spec check:react-declaration-parity'); + expect(text).toContain('--baseline react-declaration-parity.baseline.json --strict'); + expect(text).toContain(path.join(withManifest, 'sdui.manifest.json')); + // ⭐ The negative half: the false sentence, and the errand it justified, are gone. + expect(text).not.toContain('contains no copy of it'); + expect(text).not.toContain('pnpm objectui:build'); + expect(text).not.toContain('OBJECTUI_ROOT=../objectui'); + // ⭐ And it must name the RIGHT regenerator. `pnpm sdui:manifest` is + // scripts/gen-sdui-manifest.sh, whose TARGET is packages/console/dist — it never + // writes the root artefact or scripts/sdui-manifest.record.json. + // `node scripts/gen-sdui-manifest-node.mjs` owns both, which is what the record's + // own `generator` field and check-sdui-manifest.mjs both say. Sending a reader to + // the wrong one is this card's defect class again: a confident false claim about + // the tree, in the text consulted when deciding what to run. + expect(text).toContain('gen-sdui-manifest-node.mjs'); + // The wrong tool may appear ONLY inside the correction that names it wrong — + // nowhere else, command line or prose. The LOOKAHEAD is what enforces the whole of + // that sentence, and it was measured before it was written: the original defective + // claim ('`pnpm sdui:manifest` rewrites it when .objectui-sha moves') was itself + // MID-LINE, so a line-anchored form does not match it at all and would have left + // this comment promising more than the code delivers — the same over-claim, one + // layer up, in a card about exactly that. A flat `not.toContain` is not available + // either: it would forbid the correction itself. This assertion belongs to the + // present branch alone — the absent branch legitimately opens command lines with + // that spelling, so it must not be hoisted out of this leg. + expect(text).not.toMatch(/pnpm sdui:manifest(?!` does NOT rewrite)/); + expect(text).toContain('`pnpm sdui:manifest` does NOT rewrite this file'); + // …and it says, in words, what the two devs got wrong. + expect(text).toMatch(/NOT MEASURED/); + }); + }); + + it('BRANCH: no manifest — still hands over the whole production path', () => { + withRoots(({ without }) => { + const text = manifestPrescription({ repoRoot: without, checkedIn: false }); + expect(text).toContain('pnpm objectui:build'); + expect(text).toContain('pnpm sdui:manifest'); + expect(text).toContain('OBJECTUI_ROOT=../objectui'); + expect(text).toContain('MANIFEST=/path/to/sdui.manifest.json'); + // It names the path it actually looked at, so "no copy" is a reading and not a + // claim about repositories in general — the exact over-reach that caused #16715. + expect(text).toContain(path.join(without, 'sdui.manifest.json')); + // ⭐ The negative half: it must not tell a reader a file is there when it is not. + expect(text).not.toContain('IS checked in'); + expect(text).not.toContain('MANIFEST="$PWD/sdui.manifest.json"'); + }); + }); + + it('the two branches are genuinely different text, not one string with a toggle', () => { + withRoots(({ withManifest }) => { + const present = manifestPrescription({ repoRoot: withManifest, checkedIn: true }); + const absent = manifestPrescription({ repoRoot: withManifest, checkedIn: false }); + expect(present).not.toEqual(absent); + }); + }); + + /** + * The wiring, end to end: the script must PROBE, and in this repository the probe + * must find the committed manifest. The unit tests above would all pass against a + * script that never called the function. + */ + it('the real gate prints the checked-in branch in THIS repository', { timeout: SPAWN_TIMEOUT_MS }, () => { + const { status, output } = runExit({}); + expect(status, output).toBe(1); + expect(output).toContain('IS checked in at this repository\'s root'); + expect(output).toContain('MANIFEST="$PWD/sdui.manifest.json"'); + expect(output).not.toContain('contains no copy of it'); + expect(output).not.toContain('OBJECTUI_ROOT=../objectui'); + }); +}); diff --git a/packages/spec/scripts/check-react-blocks-declaration-parity.ts b/packages/spec/scripts/check-react-blocks-declaration-parity.ts index 850d5a6ec0..b7c1777539 100644 --- a/packages/spec/scripts/check-react-blocks-declaration-parity.ts +++ b/packages/spec/scripts/check-react-blocks-declaration-parity.ts @@ -77,14 +77,22 @@ // (scripts/gen-sdui-manifest.sh) is the wrapper that builds objectui at // `.objectui-sha`, dumps the manifest, and then runs THIS gate against it. // -// This repository carries no manifest to fall back on. Measured, so the next reader -// does not have to re-derive it: `packages/console/dist/` is gitignored (the package -// tracks 4 files, none of them a dist), `scripts/build-console.sh` deliberately does -// not produce one — it must not drag a browser into the console build, and says so — -// and the published `@objectstack/console` tarball has none either (16.1.0: 513 -// files, zero `sdui` matches; its `dist/manifest.json` is the PWA manifest), so even -// the CLI's `@objectstack/console/dist/sdui.manifest.json` fallback resolves to -// nothing. +// NOTHING HERE PRODUCES one — but since #13446 one is CHECKED IN. The production +// half is unchanged and still measured: `packages/console/dist/` is gitignored (the +// package tracks 4 files, none of them a dist), `scripts/build-console.sh` +// deliberately does not produce one — it must not drag a browser into the console +// build, and says so — and the published `@objectstack/console` tarball has none +// either (16.1.0: 513 files, zero `sdui` matches; its `dist/manifest.json` is the PWA +// manifest), so even the CLI's `@objectstack/console/dist/sdui.manifest.json` +// fallback resolves to nothing. +// +// What DID change is the fallback: `sdui.manifest.json` is committed at the +// repository root, `scripts/sdui-manifest.record.json` pins it to the `.objectui-sha` +// it was dumped from, and `lint.yml` runs this gate against it on every PR +// (`MANIFEST="$PWD/sdui.manifest.json"`). ⛔ So do not read a non-zero exit here as +// "no input available": the input is in the tree, and the refusal text now probes for +// it rather than asserting its absence (#16715 — `./manifest-prescription.ts`, which +// carries what that stale assertion cost). // // Therefore "no manifest" never means "nothing to check" here. It means THIS GATE // DID NOT RUN — reported as exit 1, not as a `⚠` and exit 0. Until #4690 the two @@ -113,7 +121,10 @@ process.env.OS_EAGER_SCHEMAS = '1'; import fs from 'fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { z } from 'zod'; +import { manifestPrescription, repoManifestIsCheckedIn } from './manifest-prescription'; import { REACT_BLOCKS } from '../src/ui/react-blocks'; import { ComponentPropsMap } from '../src/ui/component.zod'; import { PageComponentSchema } from '../src/ui/page.zod'; @@ -294,35 +305,25 @@ function manifestInputs(manifest: any, schemaType: string): string[] | null { } /** - * The prescription every "could not run" exit carries. + * The repository root, resolved from THIS FILE's own location. + * + * Not from `process.cwd()` and not from an env var: the prescription's whole job is + * to tell a reader where the manifest is, and a cwd-derived answer would be wrong for + * exactly the reader who is lost. `packages/spec/scripts/` → three levels up. + */ +const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..'); + +/** + * The prescription every "could not run" exit carries — probed, not asserted. * - * A refusal is only better than a skip if the reader can act on it. The manifest's - * provenance is two repos away from whoever hits this — it is dumped from objectui's - * registry in a browser — so the exit that replaced the skip has to hand over the - * whole path, not just the missing variable's name. + * Text and probe live in `./manifest-prescription.ts` so both branches (a manifest + * checked in at the root, and none) are unit-testable without a repository that has + * to be in two states at once. #16715 is what a single unprobed branch cost. */ -const MANIFEST_PRESCRIPTION = [ - '', - ' The registry side of this comparison is objectui\'s sdui.manifest.json, and this', - ' repository contains no copy of it: packages/console/dist/ is gitignored, the console', - ' build deliberately does not produce one (it must not pull in a browser), and the', - ' published @objectstack/console ships none either. Produce one, then re-run:', - '', - ' pnpm objectui:build # build + vendor the console at the pinned .objectui-sha', - ' pnpm sdui:manifest # dump the manifest in a browser AND run this ratchet', - '', - ' Against a sibling objectui checkout, point the build at it first:', - '', - ' OBJECTUI_ROOT=../objectui pnpm objectui:build && pnpm sdui:manifest', - '', - ' Or, with a manifest already in hand:', - '', - ' MANIFEST=/path/to/sdui.manifest.json \\', - ' pnpm --filter @objectstack/spec check:react-declaration-parity \\', - ' --baseline react-declaration-parity.baseline.json --strict', - '', - ' (the dump needs a browser: pnpm exec playwright install chromium-headless-shell)', -].join('\n'); +const MANIFEST_PRESCRIPTION = manifestPrescription({ + repoRoot: REPO_ROOT, + checkedIn: repoManifestIsCheckedIn(REPO_ROOT), +}); /** * Exit loudly because the gate could not run — never because it ran and disagreed. diff --git a/packages/spec/scripts/manifest-prescription.ts b/packages/spec/scripts/manifest-prescription.ts new file mode 100644 index 0000000000..11642cb73f --- /dev/null +++ b/packages/spec/scripts/manifest-prescription.ts @@ -0,0 +1,111 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// The prescription every "could not run" exit of `check:react-declaration-parity` +// carries — and the PROBE that decides which prescription is true here. +// +// A refusal is only better than a skip if the reader can act on it. The manifest's +// provenance is two repos away from whoever hits this — it is dumped from objectui's +// registry in a browser — so the exit that replaced the skip has to hand over the +// whole path, not just the missing variable's name. +// +// WHY THIS IS A FUNCTION OF A PROBE AND NOT A CONSTANT (#16715). +// +// The single constant this file replaced asserted, unconditionally, that "this +// repository contains no copy of it" and sent the reader off to build objectui and +// dump one in a browser. That stopped being true at #13446, when `sdui.manifest.json` +// was checked in at the repository root and `lint.yml` began running this gate +// against it with `MANIFEST="$PWD/sdui.manifest.json"`. +// +// The prose kept its old certainty, and it is read at the exact moment a reader is +// deciding whether the gate can run at all — the shape that gets believed. Measured +// cost, twice: on #16489 / PR #16697 and again on PR #16777 a dev read it, reported +// the gate as `EXTERNAL_INPUT_REQUIRED` / NOT MEASURED, and one of those declarations +// reached a deliverable — on a head where setting that one variable and re-running +// gave exit 0 and "no new DECLARATION divergence vs accepted baseline". +// +// So: neither branch may assert the other's world. When the file is there the reader +// gets a command to paste; only when it is genuinely absent does the objectui dump +// path appear. Both branches are pinned in +// `check-react-blocks-declaration-parity.test.ts` — one branch tested is how this +// defect was built in the first place. + +import fs from 'node:fs'; +import path from 'node:path'; + +/** Where the manifest is committed, relative to the repository root. */ +export const REPO_MANIFEST_RELATIVE = 'sdui.manifest.json'; + +/** + * Is a manifest checked in at `repoRoot`? + * + * A filesystem probe and nothing else: no env var, no config key, no build state. + * The question is "can the reader point MANIFEST at a file that exists right now", + * and only the filesystem answers that. + */ +export function repoManifestPath(repoRoot: string): string { + return path.join(repoRoot, REPO_MANIFEST_RELATIVE); +} + +export function repoManifestIsCheckedIn(repoRoot: string): boolean { + return fs.existsSync(repoManifestPath(repoRoot)); +} + +/** + * The prescription text, for whichever world the probe found. + * + * `repoRoot` is printed as a `cd` so the block is paste-ready from any working + * directory while still spelling the variable the way CI does — `$PWD` is only + * correct once you are standing at the root, so the command that sets it says how + * to get there. + */ +export function manifestPrescription(opts: { repoRoot: string; checkedIn: boolean }): string { + const { repoRoot, checkedIn } = opts; + + if (checkedIn) { + return [ + '', + " The registry side of this comparison is objectui's sdui.manifest.json, and a copy", + ` IS checked in at this repository's root (${repoManifestPath(repoRoot)}) — the very`, + ' artefact CI feeds this gate. Nothing has to be produced. Run it as CI runs it:', + '', + ` cd ${repoRoot}`, + ' MANIFEST="$PWD/sdui.manifest.json" \\', + ' pnpm --filter @objectstack/spec check:react-declaration-parity \\', + ' --baseline react-declaration-parity.baseline.json --strict', + '', + ' That is a complete local run: no browser, no objectui checkout, no dump step. So a', + ' non-zero exit from THIS gate is a reading, not an unavailable input — do not report', + ' it as EXTERNAL_INPUT_REQUIRED or NOT MEASURED (#16715).', + '', + ' The committed manifest tracks the objectui pin: when .objectui-sha moves, regenerate it', + ' and its record with `node scripts/gen-sdui-manifest-node.mjs` (check-sdui-manifest.mjs', + ' prints the exact call). ⛔ `pnpm sdui:manifest` does NOT rewrite this file: it dumps to', + ' packages/console/dist/, not to the root.', + ].join('\n'); + } + + return [ + '', + " The registry side of this comparison is objectui's sdui.manifest.json, and no copy of", + ` it is checked in at ${repoManifestPath(repoRoot)}. Nothing here can produce one:`, + ' packages/console/dist/ is gitignored, the console build deliberately does not produce', + ' one (it must not pull in a browser), and the published @objectstack/console ships none', + ' either. Produce one, then re-run:', + '', + ` cd ${repoRoot}`, + ' pnpm objectui:build # build + vendor the console at the pinned .objectui-sha', + ' pnpm sdui:manifest # dump the manifest in a browser AND run this ratchet', + '', + ' Against a sibling objectui checkout, point the build at it first:', + '', + ' OBJECTUI_ROOT=../objectui pnpm objectui:build && pnpm sdui:manifest', + '', + ' Or, with a manifest already in hand:', + '', + ' MANIFEST=/path/to/sdui.manifest.json \\', + ' pnpm --filter @objectstack/spec check:react-declaration-parity \\', + ' --baseline react-declaration-parity.baseline.json --strict', + '', + ' (the dump needs a browser: pnpm exec playwright install chromium-headless-shell)', + ].join('\n'); +} diff --git a/packages/spec/src/ui/object-refinement-check-exports.test.ts b/packages/spec/src/ui/object-refinement-check-exports.test.ts index f2a9c6e55e..a9b6e3b151 100644 --- a/packages/spec/src/ui/object-refinement-check-exports.test.ts +++ b/packages/spec/src/ui/object-refinement-check-exports.test.ts @@ -346,10 +346,24 @@ describe('each schema attaches its export BY IDENTIFIER — no inline copy', () const attachments = (src: string, name: string): number => src.match(new RegExp(`^[ \\t]*\\.superRefine\\(${name}\\)`, 'gm'))?.length ?? 0; + // …and a NAME is only a sound key for that count if the module declares it exactly + // once. `attachments()` and the `toContain` lines below both key on the spelling: + // a second, shadowing binding of the same name — a local `function checkX` inside + // another function, say — satisfies every one of them while the door chains a + // different function object, and if it happened to agree on the fixture matrix it + // would satisfy leg 2 as well. Measured on the diff that added this: four names, + // one declaration each — so this closes a residual hole in the pin, it does not fix + // a live shadowing (#16715). It must stay green. + const declarations = (src: string, name: string): number => + src.match(new RegExp(`^\\s*(export )?function ${name}\\b`, 'gm'))?.length ?? 0; + it('view.zod.ts declares both exports and chains them onto ListViewShapeSchema for ListViewSchema', () => { const src = read('view.zod.ts'); expect(src).toContain('export function checkListViewPageMount('); expect(src).toContain('export function checkListViewCalendarVisualization('); + // Exactly one declaration each — the counts below key on these names. + expect(declarations(src, 'checkListViewPageMount')).toBe(1); + expect(declarations(src, 'checkListViewCalendarVisualization')).toBe(1); // The mirrored door, exactly: shape → page-mount check → calendar check. expect(src).toMatch( /ListViewShapeSchema\s*\.superRefine\(checkListViewPageMount\)\s*\.superRefine\(checkListViewCalendarVisualization\)/, @@ -365,12 +379,14 @@ describe('each schema attaches its export BY IDENTIFIER — no inline copy', () it('page.zod.ts declares the export and attaches it to PageSchema', () => { const src = read('page.zod.ts'); expect(src).toContain('export function checkPageSourceCompleteness('); + expect(declarations(src, 'checkPageSourceCompleteness')).toBe(1); expect(attachments(src, 'checkPageSourceCompleteness')).toBe(1); }); it('dashboard.zod.ts declares the export and attaches it to GlobalFilterSchema', () => { const src = read('dashboard.zod.ts'); expect(src).toContain('export function checkGlobalFilterDateDefaultValue('); + expect(declarations(src, 'checkGlobalFilterDateDefaultValue')).toBe(1); expect(attachments(src, 'checkGlobalFilterDateDefaultValue')).toBe(1); }); });