From ce348aefeaa967f843633d3ca98d9eeb23ffa3e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 14:46:02 +0000 Subject: [PATCH] chore(sdui-parser): retire the objectui lockstep gate and its vendored record One parser is kept (`@objectstack/sdui-parser`), so the gate that held two copies byte-equal has nothing left to compare once the renderer's copy goes. Removes `scripts/check-sdui-lockstep.mjs`, `packages/sdui-parser/objectui-lockstep.json`, the `check:sdui-lockstep` / `gen:sdui-lockstep` scripts, the lint.yml step, the NOT_DRIVER_MANAGED row and the measurement-triage row, and drops the prose that pointed at the gate. Claude-Session: https://claude.ai/code/session_01Xe3LjBvLrRVA6dmFUEGskm Co-authored-by: Claude --- .github/workflows/lint.yml | 23 - package.json | 2 - packages/sdui-parser/objectui-lockstep.json | 68 - scripts/check-sdui-lockstep.mjs | 1634 ------------------- scripts/check-sdui-manifest.mjs | 12 +- scripts/gen-sdui-manifest-node.mjs | 6 +- scripts/pm/measurement-claim-triage.mjs | 2 - scripts/pm/os-regen-merge.sh | 5 +- scripts/regen-artifacts.mjs | 16 - 9 files changed, 10 insertions(+), 1758 deletions(-) delete mode 100644 packages/sdui-parser/objectui-lockstep.json delete mode 100644 scripts/check-sdui-lockstep.mjs diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 272e424827b..8ce96782d03 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -984,29 +984,6 @@ jobs: continue-on-error: true run: pnpm check:widget-option-census - # The sdui-parser LOCKSTEP invariant (#12719 / #13150). Two copies of the - # constrained-JSX parser exist: objectui's runs in the RENDERER, this one - # runs in the SAVE GATE, and #12719 states that they must byte-agree on - # the accepted grammar and on the diagnostic codes — an invariant that had - # been enforced by NOTHING through three manual ports, two of which landed - # only because someone happened to file a card naming the drift. - # - # Compared against a VENDORED RECORD of objectui's side, so this step - # needs no network, no build and no objectui checkout. Read the gate's - # header for what that buys and what it costs: it sees this copy moving - # away from the recorded objectui state; it does NOT see objectui moving - # afterwards, and only a `.objectui-sha` bump — the moment a divergence - # starts SHIPPING — brings that direction back into view. - # - # Unfiltered on purpose. The record lives under packages/sdui-parser/, the - # pin is a root dotfile, and this workflow declares no paths: filtering it - # to the parser subtree would go dormant on exactly the PR that moves the - # pin. Cost is one TypeScript parse of seven small files, ~0.4 s. - - name: sdui-parser stays in lockstep with objectui's copy - id: sdui-parser-stays-in-lockstep-with-objectui - continue-on-error: true - run: pnpm check:sdui-lockstep - # The checked-in SDUI public-tier manifest (#12924, ruled 2026-08-29: wire # it). `resolveSduiManifest()` degrades to parse-only SILENTLY when the # repo-root `sdui.manifest.json` is absent or unreadable, so nothing in diff --git a/package.json b/package.json index 56529b33acd..5c2cb938da8 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "docs:start": "pnpm --filter @objectstack/docs start", "os": "node packages/cli/bin/run.js", "console:build": "turbo run build:dist --filter=@objectstack/console", - "gen:sdui-lockstep": "node scripts/check-sdui-lockstep.mjs --update", "lint": "node --stack-size=4000 node_modules/eslint/bin/eslint.js . --no-inline-config", "i18n:extract": "tsx packages/cli/bin/run-dev.js i18n extract packages/platform-objects/scripts/i18n-extract.config.ts --locales=zh-CN,ja-JP,es-ES --fill=default --source-hashes --out=packages/platform-objects/src/apps/translations", "check:i18n": "node scripts/check-i18n-bundles.mjs --self-test && node scripts/check-i18n-bundles.mjs", @@ -39,7 +38,6 @@ "check:bash32-floor": "node scripts/check-bash32-floor.mjs --self-test && node scripts/check-bash32-floor.mjs", "check:stack-collection-maps": "node scripts/check-stack-collection-maps.mjs --self-test && node scripts/check-stack-collection-maps.mjs", "check:widget-option-census": "node scripts/check-widget-option-census.mjs --self-test && node scripts/check-widget-option-census.mjs", - "check:sdui-lockstep": "node scripts/check-sdui-lockstep.mjs --self-test && node scripts/check-sdui-lockstep.mjs", "check:doc-authoring": "node scripts/check-doc-authoring.mjs --self-test && node scripts/check-doc-authoring.mjs", "check:doc-anchors": "node scripts/check-doc-anchors.mjs --self-test && node scripts/check-doc-anchors.mjs", "check:docs-single-h1": "node scripts/check-docs-single-h1.mjs --self-test && node scripts/check-docs-single-h1.mjs", diff --git a/packages/sdui-parser/objectui-lockstep.json b/packages/sdui-parser/objectui-lockstep.json deleted file mode 100644 index 9586f6d5286..00000000000 --- a/packages/sdui-parser/objectui-lockstep.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "$comment": [ - "GENERATED by `pnpm gen:sdui-lockstep`. Do not hand-edit.", - "objectui's side of the sdui-parser lockstep (#12719), recorded at one objectui revision.", - "ONE SIDE ONLY: this file holds no diff and blesses no divergence — see scripts/check-sdui-lockstep.mjs." - ], - "objectui": { - "repo": "https://github.com/objectstack-ai/objectui.git", - "rev": "b41ed7f68956c8cfda21bc790548361af81b09ac", - "revDate": "2026-10-25T01:50:02+00:00", - "source": "packages/sdui-parser/src", - "files": [ - "packages/sdui-parser/src/body-dialect.ts", - "packages/sdui-parser/src/codegen.ts", - "packages/sdui-parser/src/dashboard-widget-options.ts", - "packages/sdui-parser/src/index.ts", - "packages/sdui-parser/src/input-type.ts", - "packages/sdui-parser/src/parse.ts", - "packages/sdui-parser/src/provenance.ts", - "packages/sdui-parser/src/types.ts", - "packages/sdui-parser/src/validate.ts" - ] - }, - "recordedAgainstPin": "b41ed7f68956c8cfda21bc790548361af81b09ac", - "grammarRegion": { - "file": "packages/sdui-parser/src/parse.ts", - "delimiter": "/* ---------------------- the JS literal subset (#6614) ---------------------- */", - "lines": 214, - "blob": "0131f27cf86dbabec09b4ae55ae131403667e7e9" - }, - "diagnosticCodes": [ - "bad-attr", - "bad-attr-value", - "bad-self-close", - "bad-tag", - "expected-element", - "expression-child", - "forbidden-attr", - "forbidden-tag", - "inert-expression", - "invalid-enum", - "member-type-mismatch", - "mismatched-tag", - "missing-required-prop", - "multiple-roots", - "no-root", - "not-a-container", - "type-mismatch", - "unclosed-comment", - "unclosed-element", - "unconsumed-widget-option", - "unknown-component", - "unknown-prop", - "unterminated-brace", - "unterminated-open-tag", - "unterminated-string" - ], - "containmentPredicate": { - "file": "packages/sdui-parser/src/validate.ts", - "code": "not-a-container", - "reaches": [ - "CHILD_LIST_KEY", - "acceptsChildren" - ], - "blob": "76c18fb95d1fd04223e3d18deea77223408cc0a8", - "text": "if (node . children ?. length && ! acceptsChildren ( comp ))\nexport const CHILD_LIST_KEY = 'children' ;\nexport function acceptsChildren ( comp : Pick < ManifestComponent , 'inputs' > ) : boolean { return comp . inputs . some ( ( input ) => input . name === CHILD_LIST_KEY ) ; }\n" - } -} diff --git a/scripts/check-sdui-lockstep.mjs b/scripts/check-sdui-lockstep.mjs deleted file mode 100644 index bc12d3a8b25..00000000000 --- a/scripts/check-sdui-lockstep.mjs +++ /dev/null @@ -1,1634 +0,0 @@ -#!/usr/bin/env node -// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. - -/** - * check-sdui-lockstep — the `sdui-parser` LOCKSTEP invariant, mechanised. - * - * node scripts/check-sdui-lockstep.mjs # compare this tree against the record - * node scripts/check-sdui-lockstep.mjs --self-test # verify the checker itself - * node scripts/check-sdui-lockstep.mjs --update # re-record objectui's side (needs a checkout) - * - * ## The invariant, and why nothing enforced it - * - * Two copies of the constrained-JSX parser exist: objectui's - * `packages/sdui-parser` (which the browser RENDERER runs) and this repo's copy - * (which the SAVE GATE runs). #12719 states what they owe each other: - * - * > both copies byte-agree on the accepted grammar and on diagnostic codes. - * - * If they drift, the save gate and the renderer speak different dialects and a - * page SAVES CLEAN AND RENDERS INERT — a failure that is surface-dependent, so - * intermittent from the author's point of view and not reliably pushed up by - * complaints. The invariant was enforced by nothing: three manual ports had - * been done by a human reading two files side by side, two of them landing only - * because someone happened to file a card naming the drift. - * - * ## What this compares — and what it CANNOT see (read before trusting a green) - * - * The comparison is against a VENDORED RECORD of objectui's side - * (`packages/sdui-parser/objectui-lockstep.json`), taken at a named objectui - * revision. At check time there is no network, no build, and no objectui - * checkout. So: - * - * CAN see this repo's copy moving away from the recorded objectui state — - * a port that is not byte-faithful, a local edit to the lockstep - * region, a codemod that reformats it, a new diagnostic code added - * on one side only, a code deleted here. - * - * CANNOT see objectui moving AFTER the record was taken. That direction - * needs objectui at check time, which is the shape this repo - * declined (a network dependency in a lint job, unrunnable locally - * without credentials). It is not covered here and must not be - * read as covered. - * - * The `.objectui-sha` clause below is the one offline instrument that reaches - * into the second class, and it is deliberate rather than incidental — see it. - * - * ## The third comparison: the containment PREDICATE (#19969) - * - * Codes and the literal region cannot see a change in WHEN a code fires. - * objectui#9910 moved `not-a-container` off `comp.isContainer` onto the - * declared `children` input without adding or removing a code and without - * touching `parse.ts`, so both comparisons above stayed green across a change - * that made the save gate and the renderer disagree on five served - * components. So the record also carries objectui's containment predicate: - * the condition of the ONE `if` in `validate.ts` whose body stamps - * `not-a-container`, plus every module-level declaration of that file the - * condition reaches (transitively — `acceptsChildren`, then `CHILD_LIST_KEY`), - * reduced to its TOKENS (comments and whitespace dropped), so a comment or a - * re-wrap moves nothing and a changed token does. `[containment-drift]` prints - * both token texts. - * - * What it CANNOT see: how `comp` is looked up before the branch, and the body - * of a helper imported from another module (the closure stops at the file - * boundary; an imported name is still printed as written, so swapping it - * shows). A `validate.ts` with zero or two such branches REFUSES — measuring - * one of two is the silent-short-set failure the code extractor refuses for - * the same reason. - * - * ## Why `--update` cannot launder a divergence - * - * `--update` re-reads OBJECTUI's side only. It never reads this tree's parser, - * so it cannot record "whatever we have now" as accepted. Run it after a port - * and one of two things happens: the port was byte-faithful and the gate goes - * green, or it was not and the gate stays RED naming the difference. That is - * the opposite of an accepted-divergence baseline (the retired - * `packages/spec/react-declaration-parity.baseline.json` recorded a diff and - * ratcheted it); this record holds ONE SIDE, so there is no diff in it to bless. - * - * ## And why `--update` REFUSES off the pin - * - * `--update` reads the objectui checkout at HEAD but stamps `recordedAgainstPin` - * from `PIN_FILE`, so those two used to be joined by nothing: a checkout not - * sitting on the pin wrote a record that NAMED THE PIN WHILE DESCRIBING A - * DIFFERENT TREE, silently, at exit 0. The record was self-certifying — stamped - * with the identity of something other than what it described, with the only - * check that compared anything (`judge()`'s `pin-moved`) comparing the STAMP - * against the pin file, two values from one file. So the generator now refuses - * unless HEAD is the pin; see `judgeRecordability`, which carries the - * measurement and the reason the resolve-it-for-them alternative was declined. - * - * ## Why the pin is read (`.objectui-sha`) - * - * `.objectui-sha` is the objectui commit whose `@object-ui/console` build this - * platform SHIPS — i.e. it names the renderer that will run against pages this - * repo's save gate accepted. So a pin bump is the moment a parser divergence - * stops being latent and starts being shipped, and it is also the only moment - * at which somebody is guaranteed to have an objectui checkout in hand (the pin - * cannot be bumped without one — see `scripts/bump-objectui.sh`). The record - * therefore stores the pin in force when it was taken, and this gate REFUSES - * when the live pin has moved past it: the remedy is one command, `pnpm - * gen:sdui-lockstep`, run against the checkout the bump already required. - * - * ⚠️ This is NOT a resurrection of the retired `Console Pin Freshness` gate - * (#10134, maintainer ruling 2026-08-20: which objectui revision we pin is a - * decision recorded in an objectstack issue, never derived from objectui - * `main`). Nothing here reads objectui `main`, nothing here proposes a pin, and - * a pin bump is never called stale by this gate. It asks a different question: - * given that YOU are moving the pin, has the parser parity been re-verified - * against what you are moving it to. - * - * ## With no pin: the in-tree oracle (#25712) - * - * objectui's tree is imported under `ui/` (#25677), and the pin goes away with - * it: the renderer that ships is then the copy at `ui/packages/sdui-parser/src`, - * built from the same commit as this one. So the gate picks its oracle by what - * the tree has (`oracleFor`), and a tree WITH `.objectui-sha` is judged exactly - * as above, record and messages unchanged: - * - * pin present → the vendored record, as described above; - * pin absent, `ui/` copy → that copy, read directly with the same reader - * `--update` records with (same file set, region, - * code set and containment predicate). The record is - * not consulted, and both directions are visible: the - * in-tree copy moving shows here too; - * neither → REFUSED, naming both. A tree with no renderer copy - * to agree with is an exit 1, never a pass. - * - * Retiring the lockstep itself (one parser, the record and this gate deleted) - * is #25712 item 3, not this mode. - * - * ## The decomposition that keeps this from crying wolf on day one - * - * The same diagnostic code is a quoted literal in this repo and a CONSTANT - * REFERENCE on objectui's side: - * - * this repo code: 'unconsumed-widget-option' - * objectui code: UNCONSUMED_WIDGET_OPTION - * - * (The divergence is deliberate and documented at the site: this repo runs - * `check:dispatcher-error-vocabulary`, whose literal grammar a kebab-case - * constant cannot satisfy.) A literal-only scan reports a false 24-vs-23 - * difference the first time it runs. So the extractor resolves an identifier at - * a code position through the module-level `const NAME = '...'` declarations of - * the same package, and REFUSES on one it cannot resolve — silently skipping - * the unresolvable one is the same false difference with the evidence removed. - * Both halves are pinned in `--self-test`. - * - * ## Absence is loud, everywhere - * - * Every input this gate needs is asserted before any verdict: the record file, - * its shape, the region delimiter in this tree, a non-empty code set, a - * resolvable identifier at every code position, and the pin. `--update` asserts - * the same way before any RECORD: a readable pin, a HEAD that parses, and the - * two being equal. A missing one exits 1 saying which — never a `⚠` and exit 0. A guard whose success condition and - * whose total-failure condition are the same exit code is worse than no guard - * (#13014, and #4690 for the incident that named the class). - */ - -import { createHash } from 'node:crypto'; -import { execFileSync } from 'node:child_process'; -import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { dirname, join, relative, sep } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -import { isEntrypoint } from './invoked-as.mjs'; -import { parseSourceFile } from './ts-parse.mjs'; -import { requireDefaultExport } from './import-prerequisite.mjs'; -const ts = await requireDefaultExport('typescript', () => import('typescript'), import.meta.url); - -const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); - -/** Where both copies keep the parser. Relative, because both trees spell it identically. */ -const PARSER_SRC = 'packages/sdui-parser/src'; - -/** The file the ported grammar region lives in, in both copies. */ -const REGION_FILE = 'packages/sdui-parser/src/parse.ts'; - -/** - * The literal comment that OPENS the ported block, carried verbatim by both - * copies — which is what makes the region addressable without a parser: it runs - * from this line to end of file. - * - * ⚠️ Changing this string is changing what "the ported block" means. It is a - * deliberate act that belongs in the same port as the block itself, and the - * gate refuses (rather than measuring a shorter region) when it is not found. - */ -const REGION_DELIMITER = '/* ---------------------- the JS literal subset (#6614) ---------------------- */'; - -/** The file whose `not-a-container` branch is the containment predicate, in both copies. */ -const CONTAINMENT_FILE = 'packages/sdui-parser/src/validate.ts'; - -/** The diagnostic code whose guarding condition IS the containment predicate. */ -const CONTAINMENT_CODE = 'not-a-container'; - -/** The vendored record of OBJECTUI's side. One side only — see the header. */ -const RECORD_FILE = 'packages/sdui-parser/objectui-lockstep.json'; - -/** The objectui commit whose console build this platform ships. */ -const PIN_FILE = '.objectui-sha'; - -/** The directory objectui's tree is imported under (#25677), as a bare segment. */ -const UI_TREE = 'ui'; - -/** - * The renderer's copy in the merged layout — the in-tree oracle. Spelled as one - * literal so that `scripts/pm/dispatch-gates.mjs` derives this gate for a diff - * under it; the self-test holds it equal to `${UI_TREE}/${PARSER_SRC}`. - */ -const UI_PARSER_SRC = 'ui/packages/sdui-parser/src'; - -/** Where `--update` looks for an objectui checkout, after `OBJECTUI_ROOT`. */ -const OBJECTUI_SIBLING = '../objectui'; - -/** Provenance for the record, and the clone line the refusal prints. */ -const OBJECTUI_REMOTE = 'https://github.com/objectstack-ai/objectui.git'; - -/** - * The `ROOT_DIR_WATCH_HINTS` idiom — provenance for `scripts/pm/dispatch-gates.mjs`, - * never read by this gate. - * - * #12956 asked that a `.objectui-sha` diff DERIVE the gates a pin bump exists to - * run, and this gate is one of them by construction: the pin names the objectui - * build whose renderer ships, so a bump is when a parser divergence starts being - * shipped. Deriving it needs a watch hint, and the plain `'.objectui-sha'` - * literal above builds none — measured on this tree at 96 hint literals: - * `extractWatchHints` admits a leading-dot literal only from a fixed allowlist - * (`.claude`, `.changeset`, `.github`, `.gitattributes`), and a root dotfile - * outside it falls through both that admission and `hintCovers`' bare-word - * refusal. The subtree spelling is the documented escape for a repo-ROOT FILE, - * carried by `check-agent-test-spelling.mjs` as `AGENTS.md/**`: - * `hintCovers('.objectui-sha/**', '.objectui-sha')` is true, and it overclaims - * nothing because the pin has no nested namesakes. - * - * ⛔ Nothing here reads this array — the glob form is not a path any read in - * this file could resolve. `--self-test` pins it to `PIN_FILE` in both - * directions, so a declaration cannot drift from what the gate opens. - */ -export const ROOT_DIR_WATCH_HINTS = ['.objectui-sha/**']; - -// ───────────────────────────────────────────────────────────────────────────── -// Measuring one side - -/** git's own blob id for a byte string, so a reading here is comparable to `git hash-object`. */ -export function gitBlobHash(text) { - const body = Buffer.from(text, 'utf8'); - const header = Buffer.from(`blob ${body.length}\0`, 'utf8'); - return createHash('sha1').update(Buffer.concat([header, body])).digest('hex'); -} - -/** Every non-test TypeScript source under one copy's `src/`, sorted, repo-relative. */ -export function parserSources(root) { - const base = join(root, PARSER_SRC); - const out = []; - const walk = (dir) => { - for (const entry of readdirSync(dir).sort()) { - const full = join(dir, entry); - if (statSync(full).isDirectory()) { - if (entry !== '__tests__') walk(full); - } else if (/\.tsx?$/.test(entry) && !/\.test\.tsx?$/.test(entry)) { - out.push(relative(root, full).split(sep).join('/')); - } - } - }; - walk(base); - return out; -} - -/** - * The grammar region of one `parse.ts`: the delimiter line to end of file. - * `null` when the delimiter is absent — the caller REFUSES on that; it never - * measures a region it could not find the start of. - */ -export function readRegion(text) { - const lines = text.split('\n'); - const at = lines.findIndex((line) => line.trim() === REGION_DELIMITER); - if (at === -1) return null; - const region = lines.slice(at).join('\n'); - // The trailing newline of the last line is part of the file, so a region that - // runs to EOF must carry it or its blob id is not the file's own tail. - const lineCount = region.endsWith('\n') ? region.split('\n').length - 1 : region.split('\n').length; - return { lines: lineCount, blob: gitBlobHash(region), text: region }; -} - -/** - * Every diagnostic code one copy can stamp, and every code position it could - * not resolve. - * - * Read from the AST, never from text: a text scan picks the phrase `code: - * UNCONSUMED_WIDGET_OPTION` out of the very comment that DOCUMENTS the - * divergence, and cannot tell `code: 'x'` (a stamped code) from `code: string` - * (the Diagnostic type) or from `error(code: string, …)` (the emitter's own - * parameter). - * - * Two stamping shapes, which is all either copy uses: - * - a `code:` property in an object literal — `{ code: 'inert-expression' }` - * - the emitter's first argument — `this.error('no-root', …)` - * with an identifier in either position resolved through the package's own - * module-level string constants. - */ -export function extractDiagnosticCodes({ files, read }) { - const trees = files.map((rel) => [rel, parseSourceFile(rel, read(rel))]); - - const constants = new Map(); - for (const [, sourceFile] of trees) { - const visit = (node) => { - if ( - ts.isVariableDeclaration(node) - && ts.isIdentifier(node.name) - && node.initializer - && ts.isStringLiteralLike(node.initializer) - ) { - constants.set(node.name.text, node.initializer.text); - } - ts.forEachChild(node, visit); - }; - visit(sourceFile); - } - - const codes = new Map(); - const unresolved = []; - for (const [rel, sourceFile] of trees) { - const where = (node) => `${rel}:${sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1}`; - const take = (expr, node) => { - if (ts.isStringLiteralLike(expr)) { - if (!codes.has(expr.text)) codes.set(expr.text, []); - codes.get(expr.text).push(where(node)); - return; - } - if (ts.isIdentifier(expr) && constants.has(expr.text)) { - const value = constants.get(expr.text); - if (!codes.has(value)) codes.set(value, []); - codes.get(value).push(`${where(node)} (via ${expr.text})`); - return; - } - unresolved.push(`${where(node)}: ${expr.getText(sourceFile)}`); - }; - - const visit = (node) => { - if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && node.name.text === 'code') { - take(node.initializer, node); - } - if (ts.isCallExpression(node) && node.arguments.length > 0) { - const callee = node.expression; - const name = ts.isPropertyAccessExpression(callee) - ? callee.name.text - : ts.isIdentifier(callee) - ? callee.text - : null; - if (name === 'error') take(node.arguments[0], node); - } - ts.forEachChild(node, visit); - }; - visit(sourceFile); - } - - return { codes: [...codes.keys()].sort(), sites: codes, unresolved }; -} - -/** - * The containment predicate of one `validate.ts`, as canonical text plus its - * blob id, or `{ predicate: null, why }` when it cannot be read whole. - * - * The predicate is the condition of the ONE `if` whose then-branch stamps - * `code: 'not-a-container'`, closed over every module-level function or - * variable declaration of the same file that the condition names — and that - * those name in turn. A property NAME (`node.children`, `input.name`) is not a - * reference; a local (`comp`, `node`) resolves to no module-level declaration - * and is left as written. Each piece is reduced to its token sequence — the - * AST's leaf tokens joined by one space, JSDoc nodes skipped, trivia never - * reached — one line per piece, declarations in name order, so the text - * depends on tokens and not on comments, wrapping or declaration order. (The - * TypeScript printer was tried first and is not this: it keeps a source line - * break before a `.` in a call chain, so a re-wrap moved the reading.) - */ -export function extractContainmentPredicate(text, rel = CONTAINMENT_FILE) { - const sourceFile = parseSourceFile(rel, text); - const print = (root) => { - const tokens = []; - const walk = (node) => { - if (ts.isJSDoc(node)) return; - const children = node.getChildren(sourceFile); - if (children.length === 0) { - if (node.kind !== ts.SyntaxKind.EndOfFileToken) tokens.push(node.getText(sourceFile)); - return; - } - children.forEach(walk); - }; - walk(root); - return tokens.filter((t) => t !== '').join(' '); - }; - - const topLevel = new Map(); - for (const statement of sourceFile.statements) { - if (ts.isFunctionDeclaration(statement) && statement.name) topLevel.set(statement.name.text, statement); - if (ts.isVariableStatement(statement)) { - for (const decl of statement.declarationList.declarations) { - if (ts.isIdentifier(decl.name)) topLevel.set(decl.name.text, statement); - } - } - } - - const stampsContainment = (root) => { - let found = false; - const visit = (node) => { - if ( - ts.isPropertyAssignment(node) - && ts.isIdentifier(node.name) - && node.name.text === 'code' - && ts.isStringLiteralLike(node.initializer) - && node.initializer.text === CONTAINMENT_CODE - ) { - found = true; - } - if (!found) ts.forEachChild(node, visit); - }; - visit(root); - return found; - }; - - const branches = []; - const findBranches = (node) => { - if (ts.isIfStatement(node) && stampsContainment(node.thenStatement)) branches.push(node); - ts.forEachChild(node, findBranches); - }; - findBranches(sourceFile); - if (branches.length !== 1) { - return { - predicate: null, - why: `${rel} has ${branches.length} \`if\` branch(es) stamping \`${CONTAINMENT_CODE}\`, and exactly one is the predicate`, - }; - } - const condition = branches[0].expression; - - const referencedFrom = (root) => { - const names = []; - const visit = (node) => { - if (ts.isIdentifier(node)) { - const parent = node.parent; - const isPropertyName = parent - && ((ts.isPropertyAccessExpression(parent) && parent.name === node) - || (ts.isPropertyAssignment(parent) && parent.name === node)); - if (!isPropertyName && topLevel.has(node.text)) names.push(node.text); - } - ts.forEachChild(node, visit); - }; - visit(root); - return names; - }; - const reached = new Set(); - const queue = referencedFrom(condition); - while (queue.length > 0) { - const name = queue.shift(); - if (reached.has(name)) continue; - reached.add(name); - queue.push(...referencedFrom(topLevel.get(name))); - } - - const reaches = [...reached].sort(); - const printedDecls = [...new Set(reaches.map((name) => topLevel.get(name)))].map(print); - const canonical = `${[`if (${print(condition)})`, ...printedDecls].join('\n')}\n`; - return { predicate: { text: canonical, blob: gitBlobHash(canonical), reaches }, why: null }; -} - -/** One copy's whole parity fingerprint: the region reading, the code set and the containment predicate. */ -export function fingerprintOf(root) { - const files = parserSources(root); - const read = (rel) => readFileSync(join(root, rel), 'utf8'); - const region = readRegion(read(REGION_FILE)); - const { codes, unresolved } = extractDiagnosticCodes({ files, read }); - const containment = extractContainmentPredicate(read(CONTAINMENT_FILE)); - return { files, region, codes, unresolved, containment }; -} - -// ───────────────────────────────────────────────────────────────────────────── -// Comparing - -/** The record's shape, asserted before anything is compared against it. */ -export function recordProblems(record) { - const problems = []; - if (record === null || typeof record !== 'object') return ['it is not a JSON object']; - const rev = record.objectui?.rev; - if (typeof rev !== 'string' || !/^[0-9a-f]{40}$/.test(rev)) { - problems.push('`objectui.rev` is not a 40-character commit id'); - } - if (typeof record.recordedAgainstPin !== 'string' || !/^[0-9a-f]{40}$/.test(record.recordedAgainstPin)) { - problems.push('`recordedAgainstPin` is not a 40-character commit id'); - } - if (typeof record.grammarRegion?.lines !== 'number' || record.grammarRegion.lines <= 0) { - problems.push('`grammarRegion.lines` is not a positive line count'); - } - if (typeof record.grammarRegion?.blob !== 'string' || !/^[0-9a-f]{40}$/.test(record.grammarRegion.blob)) { - problems.push('`grammarRegion.blob` is not a 40-character blob id'); - } - if (!Array.isArray(record.diagnosticCodes) || record.diagnosticCodes.length === 0) { - problems.push('`diagnosticCodes` is not a non-empty array'); - } - if ( - typeof record.containmentPredicate?.blob !== 'string' - || !/^[0-9a-f]{40}$/.test(record.containmentPredicate.blob) - || typeof record.containmentPredicate?.text !== 'string' - ) { - problems.push('`containmentPredicate` does not carry a 40-character `blob` and its `text`'); - } - return problems; -} - -/** - * The verdict. Pure — every input is passed in, so `--self-test` drives the - * same function the production run does rather than a paraphrase of it. - */ -export function judge({ record, ours, livePin }) { - const findings = []; - - const shape = recordProblems(record); - if (shape.length > 0) { - for (const problem of shape) findings.push(`[record-unusable] ${RECORD_FILE}: ${problem}`); - return findings; // nothing below can be measured against a record that is not one - } - - findings.push(...unreadableHere(ours)); - if (findings.length > 0) return findings; - - if (record.recordedAgainstPin !== livePin) { - findings.push( - `[pin-moved] ${PIN_FILE} is ${livePin.slice(0, 12)}, and the parity record was taken while it was\n` - + ` ${record.recordedAgainstPin.slice(0, 12)}. The pin names the objectui build whose RENDERER ships, so a\n` - + ' bump is exactly when a parser divergence stops being latent and starts being shipped.\n' - + ' Re-record against the objectui checkout the bump already required: pnpm gen:sdui-lockstep', - ); - } - - const rev = record.objectui.rev.slice(0, 12); - findings.push(...driftFindings({ - ours, - theirs: { region: record.grammarRegion, codes: record.diagnosticCodes, containment: record.containmentPredicate }, - side: { - label: 'recorded', - region: `objectui's at\n ${rev}`, - containment: `objectui's copy\n runs at ${rev}`, - codes: `objectui's at ${rev}`, - only: 'only objectui', - }, - })); - return findings; -} - -/** - * The verdict with no pin (the merged layout): this copy against the renderer's - * copy under `ui/`, read whole by `fingerprintOf` — no record in between. Pure, - * like `judge`; the in-tree side is held to the refusals `--update` holds - * objectui's side to before it records, so an unreadable copy is never a match. - */ -export function judgeInTree({ ours, theirs }) { - const findings = []; - if (theirs.region === null) { - findings.push( - `[ui-region-unreachable] ${UI_TREE}/${REGION_FILE} does not contain the delimiter that opens the ported block:\n` - + ` ${REGION_DELIMITER}\n` - + ' Nothing was compared. The renderer\'s copy must carry the block this copy is held to.', - ); - } - if (theirs.unresolved.length > 0 || theirs.codes.length === 0) { - findings.push( - `[ui-code-set-unreadable] the diagnostic-code set under ${UI_PARSER_SRC} could not be read whole, so no\n` - + ' comparison was made:\n' - + (theirs.codes.length === 0 ? ' no code found at all\n' : '') - + theirs.unresolved.map((u) => ` unresolvable: ${u}`).join('\n'), - ); - } - if (theirs.containment?.predicate == null) { - findings.push( - `[ui-containment-unreachable] ${theirs.containment?.why ?? `no containment reading was taken of ${UI_TREE}/${CONTAINMENT_FILE}`}.\n` - + ' Nothing was compared.', - ); - } - findings.push(...unreadableHere(ours)); - if (findings.length > 0) return findings; - - return driftFindings({ - ours, - theirs: { region: theirs.region, codes: theirs.codes, containment: theirs.containment.predicate }, - side: { - label: 'in-tree ', - region: `the in-tree copy's\n (${UI_TREE}/${REGION_FILE})`, - containment: `the in-tree copy\n (${UI_TREE}/${CONTAINMENT_FILE}) runs`, - codes: `the in-tree copy's (${UI_PARSER_SRC})`, - only: 'only ui/ ', - }, - }); -} - -/** This copy's inputs, asserted before any comparison: each finding is a refusal, never a drift. */ -function unreadableHere(ours) { - const findings = []; - if (ours.region === null) { - findings.push( - `[region-unreachable] ${REGION_FILE} does not contain the delimiter that opens the ported block:\n` - + ` ${REGION_DELIMITER}\n` - + ' Nothing was measured. If the block was renamed on both sides, update REGION_DELIMITER in this gate\n' - + ' as part of that port; if it was deleted here, that IS the drift.', - ); - } - if (ours.unresolved.length > 0) { - findings.push( - '[code-unresolvable] a diagnostic code is stamped from something this gate cannot reduce to a literal,\n' - + ' so the code set it would compare is incomplete and no comparison was made:\n' - + ours.unresolved.map((u) => ` ${u}`).join('\n'), - ); - } - if (ours.codes.length === 0) { - findings.push( - `[code-set-empty] no diagnostic code was found under ${PARSER_SRC} — this copy stamps two dozen, so the\n` - + ' extractor read nothing rather than finding nothing.', - ); - } - if (ours.containment?.predicate == null) { - findings.push( - `[containment-unreachable] ${ours.containment?.why ?? `no containment reading was taken of ${CONTAINMENT_FILE}`}.\n` - + ' Nothing was compared. The predicate that decides `not-a-container` is part of the lockstep (#19969),\n' - + ' and a copy whose predicate cannot be located is not a copy that agrees.', - ); - } - return findings; -} - -/** - * The three drift comparisons against one reading of the renderer's copy — the - * record (pin) or `ui/` itself (in-tree). `side` carries only the words that - * name which, so both verdicts compare the same way. - */ -function driftFindings({ ours, theirs, side }) { - const findings = []; - if (ours.region.blob !== theirs.region.blob) { - findings.push( - `[grammar-drift] the ported grammar region of ${REGION_FILE} is not byte-identical to ${side.region}.\n` - + ` here ${ours.region.lines} line(s), blob ${ours.region.blob}\n` - + ` ${side.label} ${theirs.region.lines} line(s), blob ${theirs.region.blob}\n` - + ' Either this copy was edited without the port, or the port was not byte-faithful. Diff the two\n' - + ` copies' ${REGION_FILE} from the delimiter down.`, - ); - } - - if (ours.containment.predicate.blob !== theirs.containment.blob) { - const indent = (t) => t.trimEnd().split('\n').map((l) => ` ${l}`).join('\n'); - findings.push( - `[containment-drift] the \`${CONTAINMENT_CODE}\` predicate in ${CONTAINMENT_FILE} is not the one ${side.containment}, so the save gate and the renderer disagree about which\n` - + ' components accept a child list.\n' - + ` here blob ${ours.containment.predicate.blob}\n${indent(ours.containment.predicate.text)}\n` - + ` ${side.label} blob ${theirs.containment.blob}\n${indent(theirs.containment.text)}`, - ); - } - - const recorded = new Set(theirs.codes); - const mine = new Set(ours.codes); - const onlyHere = ours.codes.filter((c) => !recorded.has(c)); - const onlyThere = theirs.codes.filter((c) => !mine.has(c)); - if (onlyHere.length > 0 || onlyThere.length > 0) { - findings.push( - `[code-drift] the diagnostic-code sets differ from ${side.codes}.\n` - + (onlyHere.length ? ` only here ${onlyHere.join(', ')}\n` : '') - + (onlyThere.length ? ` ${side.only} ${onlyThere.join(', ')}\n` : '') - + ' A code on one side only is a dialect: the save gate and the renderer disagree about what a page means.', - ); - } - - return findings; -} - -// ───────────────────────────────────────────────────────────────────────────── -// Choosing the oracle - -/** - * Which copy of the renderer's parser this tree is judged against. The pin wins - * whenever it is present, so a tree that has both (the merged tree before the pin - * is deleted) is judged exactly as before the merge. - */ -export function oracleFor({ pinPresent, uiPresent }) { - if (pinPresent) return 'pin'; - return uiPresent ? 'in-tree' : 'none'; -} - -/** - * Gate a tree that has no pin: `{ refusal }` when it has no in-tree copy either, - * otherwise both fingerprints and `judgeInTree`'s findings. - */ -export function checkWithoutPin(root) { - if (oracleFor({ pinPresent: false, uiPresent: existsSync(join(root, UI_PARSER_SRC)) }) === 'none') { - return { - refusal: - `this tree has neither ${PIN_FILE} nor ${UI_PARSER_SRC}, so there is no renderer copy to compare\n` - + ` with. With the pin, the renderer's copy is judged through the vendored record (${RECORD_FILE});\n` - + ` without it, the renderer's copy is the in-tree one under ${UI_TREE}/, read directly. Nothing was compared.`, - }; - } - const ours = fingerprintOf(root); - const theirs = fingerprintOf(join(root, UI_TREE)); - return { ours, theirs, findings: judgeInTree({ ours, theirs }) }; -} - -// ───────────────────────────────────────────────────────────────────────────── -// Re-recording objectui's side - -/** Resolve an objectui checkout, or say why there is none. */ -export function resolveObjectui(env = process.env) { - const candidate = env.OBJECTUI_ROOT ? env.OBJECTUI_ROOT : join(ROOT, OBJECTUI_SIBLING); - if (!existsSync(join(candidate, '.git'))) return { root: null, why: `no git checkout at ${candidate}` }; - if (!existsSync(join(candidate, PARSER_SRC))) return { root: null, why: `${candidate} has no ${PARSER_SRC}` }; - return { root: candidate, why: null }; -} - -/** - * Whether an objectui checkout may be RECORDED FROM: its `HEAD` must be the - * commit `.objectui-sha` names. Empty means it may; otherwise one `[tag]` - * finding per reason, in `judge()`'s shape so both are read the same way. - * - * ## Why the generator needs this and the checker cannot supply it - * - * The record's CONTENT is read from whatever the objectui checkout has at HEAD, - * while `recordedAgainstPin` is stamped from `PIN_FILE`. Nothing joined those - * two, so a checkout not sitting on the pin wrote a record that NAMED THE PIN - * WHILE DESCRIBING A DIFFERENT TREE — silently, at exit 0. - * - * ⚠️ And `judge()`'s `pin-moved` clause cannot catch it, which is the whole - * point: that clause compares `record.recordedAgainstPin` against the live pin, - * i.e. the STAMPED side against the pin file — two values that came from the - * same file and therefore agree by construction. The record was self-certifying: - * it was stamped with the identity of something other than what it described, - * and the one check comparing anything compared the stamp. The join has to - * happen HERE, at the only moment both the tree and the pin are in hand. - * - * ⛔ Never repair that by stamping HEAD instead. It would make the field honest - * and useless — the field exists to say WHICH PIN the description belongs to — - * and it would make `judge()`'s comparison pass forever, destroying a working - * check along with the record. - * - * Measured, deterministically, on the `a472b07167a3` → `53ded82bf7a4` bump: read - * at objectui tip the record carried 25 diagnostic codes, read at the pin it - * carried 24. ⚠️ `parse.ts` is byte-identical between those two revisions while - * the `PARSER_SRC` tree is not, and that is exactly where the difference lives — - * so a reader checking the obvious file sees nothing wrong, and an equality - * assertion is the only thing that does. - * - * Refusing rather than resolving the checkout to the pin ourselves is deliberate: - * a read from the pin's tree needs git WRITES inside somebody else's checkout, - * whose behaviour over uncommitted work there would have to be defined. The - * operator parking their own checkout needs no new machinery. - */ -export function judgeRecordability({ head, livePin }) { - const findings = []; - if (typeof livePin !== 'string' || !/^[0-9a-f]{40}$/.test(livePin)) { - findings.push( - `[pin-unusable] ${PIN_FILE} does not hold a 40-character commit id, so nothing can be checked\n` - + ` against it. It reads: ${JSON.stringify(livePin)}.`, - ); - return findings; - } - if (typeof head !== 'string' || !/^[0-9a-f]{40}$/.test(head)) { - findings.push( - `[head-unusable] \`git rev-parse HEAD\` in the objectui checkout did not yield a 40-character\n` - + ` commit id, so it cannot be compared with the pin. It yielded: ${JSON.stringify(head)}.`, - ); - return findings; - } - if (head !== livePin) { - findings.push( - `[head-off-pin] the objectui checkout is at ${head.slice(0, 12)}, and ${PIN_FILE} names\n` - + ` ${livePin.slice(0, 12)}. Recording now would write a record stamped with the pin while\n` - + ' describing a different tree — the exact silent-wrong-record this refusal exists to prevent.', - ); - } - return findings; -} - -function update() { - const { root: objectui, why } = resolveObjectui(); - if (objectui === null) { - console.error( - `\ncheck-sdui-lockstep --update: REFUSED — ${why}.\n\n` - + ' This re-records OBJECTUI\'s side, so it needs objectui on disk. Clone it beside this repo, or point\n' - + ' OBJECTUI_ROOT at an existing checkout:\n\n' - + ` git clone ${OBJECTUI_REMOTE} ${OBJECTUI_SIBLING}\n` - + ' OBJECTUI_ROOT=/path/to/objectui pnpm gen:sdui-lockstep\n', - ); - process.exit(1); - } - - const git = (...args) => execFileSync('git', ['-C', objectui, ...args], { encoding: 'utf8' }).trim(); - - // The HEAD-vs-pin join, BEFORE anything is read or written. It comes first - // because on the wrong revision every later reading is a reading of the wrong - // tree, and a dirty-tree or missing-delimiter complaint about it is noise. - let livePin = null; - try { - livePin = readFileSync(join(ROOT, PIN_FILE), 'utf8').trim(); - } catch (error) { - console.error( - `\ncheck-sdui-lockstep --update: REFUSED — ${PIN_FILE} could not be read (${error.message}).\n` - + ' Nothing was recorded. The pin is what the record is stamped against, so a record taken\n' - + ' without it would name no revision at all.\n', - ); - process.exit(1); - } - const recordability = judgeRecordability({ head: git('rev-parse', 'HEAD'), livePin }); - if (recordability.length > 0) { - console.error('\ncheck-sdui-lockstep --update: REFUSED — nothing was recorded.\n'); - for (const finding of recordability) console.error(` ${finding}\n`); - console.error( - ` Park the checkout on the pin and re-run, from ${objectui}:\n\n` - + ` git -C ${objectui} fetch origin && git -C ${objectui} checkout ${livePin}\n` - + ' pnpm gen:sdui-lockstep\n\n' - + ' Or, if you meant to record a DIFFERENT revision, move the pin to it first — that is a\n' - + ` decision recorded in an issue, never a side effect of this generator:\n\n` - + ' scripts/bump-objectui.sh \n', - ); - process.exit(1); - } - - const dirty = git('status', '--porcelain', '--', PARSER_SRC); - if (dirty !== '') { - console.error( - `\ncheck-sdui-lockstep --update: REFUSED — ${objectui}/${PARSER_SRC} has uncommitted changes, so the\n` - + ' revision recorded would not describe the bytes recorded:\n' - + dirty.split('\n').map((l) => ` ${l}`).join('\n') - + '\n', - ); - process.exit(1); - } - - const theirs = fingerprintOf(objectui); - if (theirs.region === null) { - console.error( - `\ncheck-sdui-lockstep --update: REFUSED — ${objectui}/${REGION_FILE} does not contain the delimiter\n` - + ` that opens the ported block (${REGION_DELIMITER}).\n` - + ' Nothing was recorded. That checkout is at a revision from BEFORE the block existed, or the block was\n' - + ' renamed there — either way the record must not claim a region nobody found.\n', - ); - process.exit(1); - } - if (theirs.containment.predicate === null) { - console.error( - `\ncheck-sdui-lockstep --update: REFUSED — objectui's containment predicate could not be read: ${theirs.containment.why}.\n` - + ' Nothing was recorded.\n', - ); - process.exit(1); - } - if (theirs.unresolved.length > 0 || theirs.codes.length === 0) { - console.error( - '\ncheck-sdui-lockstep --update: REFUSED — objectui\'s diagnostic-code set could not be read whole:\n' - + (theirs.codes.length === 0 ? ' no code found at all\n' : '') - + theirs.unresolved.map((u) => ` unresolvable: ${u}`).join('\n') - + '\n', - ); - process.exit(1); - } - - const record = { - $comment: [ - 'GENERATED by `pnpm gen:sdui-lockstep`. Do not hand-edit.', - "objectui's side of the sdui-parser lockstep (#12719), recorded at one objectui revision.", - 'ONE SIDE ONLY: this file holds no diff and blesses no divergence — see scripts/check-sdui-lockstep.mjs.', - ], - objectui: { - repo: OBJECTUI_REMOTE, - rev: git('rev-parse', 'HEAD'), - revDate: git('show', '-s', '--format=%cI', 'HEAD'), - source: PARSER_SRC, - files: theirs.files, - }, - // The same bytes the refusal above compared HEAD against — re-reading the file here - // would reopen the gap between what was CHECKED and what is STAMPED. - recordedAgainstPin: livePin, - grammarRegion: { - file: REGION_FILE, - delimiter: REGION_DELIMITER, - lines: theirs.region.lines, - blob: theirs.region.blob, - }, - diagnosticCodes: theirs.codes, - containmentPredicate: { - file: CONTAINMENT_FILE, - code: CONTAINMENT_CODE, - reaches: theirs.containment.predicate.reaches, - blob: theirs.containment.predicate.blob, - text: theirs.containment.predicate.text, - }, - }; - writeFileSync(join(ROOT, RECORD_FILE), `${JSON.stringify(record, null, 2)}\n`); - console.log( - `check-sdui-lockstep --update: recorded objectui@${record.objectui.rev.slice(0, 12)} ` - + `(${record.objectui.revDate}) — ${record.grammarRegion.lines} region line(s), blob ` - + `${record.grammarRegion.blob.slice(0, 12)}, ${record.diagnosticCodes.length} diagnostic code(s), ` - + `containment predicate ${record.containmentPredicate.blob.slice(0, 12)}, ` - + `against pin ${record.recordedAgainstPin.slice(0, 12)}.`, - ); - console.log(` Wrote ${RECORD_FILE}. Now run \`pnpm check:sdui-lockstep\` — a RED there is the port you still owe.`); -} - -// ───────────────────────────────────────────────────────────────────────────── - -const FIXTURE_LITERAL_SIDE = ` -export interface Diagnostic { code: string; message: string } -class P { - private error(code: string, message: string): void { void code; void message; } - run(): void { this.error('no-root', 'nothing'); } -} -export const stamp = { code: 'unconsumed-widget-option', message: 'x' }; -// objectui writes \`code: PHANTOM_CODE\` here — prose, and prose is not a stamp. -export const prose = 'code: ALSO_NOT_A_STAMP'; -`; - -const FIXTURE_CONSTANT_SIDE = ` -export interface Diagnostic { code: string; message: string } -export const UNCONSUMED_WIDGET_OPTION = 'unconsumed-widget-option'; -class P { - private error(code: string, message: string): void { void code; void message; } - run(): void { this.error('no-root', 'nothing'); } -} -export const stamp = { code: UNCONSUMED_WIDGET_OPTION, message: 'x' }; -`; - -// The containment fixtures: the two predicates objectui#9910 moved between, -// shaped like `validate.ts` (a branch INSIDE the walker, the helper and its -// constant at module level, comments on both). -const FIXTURE_CONTAINMENT_SLOT = ` -export const CHILD_LIST_KEY = 'children'; -/** Does it accept a child list? */ -export function acceptsChildren(comp: { inputs: Array<{ name: string }> }): boolean { - return comp.inputs.some((input) => input.name === CHILD_LIST_KEY); -} -const UNRELATED = 'not-reached'; -export function validateTree(node: any, comp: any, diagnostics: any[]): void { - // containment - if (node.children?.length && !acceptsChildren(comp)) { - diagnostics.push({ severity: 'warning', code: 'not-a-container', message: 'x' }); - } - if (node.x) diagnostics.push({ code: 'unknown-prop', message: UNRELATED }); -} -`; - -const FIXTURE_CONTAINMENT_FLAG = ` -export function validateTree(node: any, comp: any, diagnostics: any[]): void { - if (node.children?.length && !comp.isContainer) { - diagnostics.push({ severity: 'warning', code: 'not-a-container', message: 'x' }); - } -} -`; - -const FIXTURE_UNRESOLVABLE = ` -import { SOMEWHERE_ELSE } from './elsewhere.js'; -export const stamp = { code: SOMEWHERE_ELSE, message: 'x' }; -`; - -// 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 -// as one that passed (#13798). -const SELF_TEST_VERDICT = 'check-sdui-lockstep self-test reached its verdict'; - -// ── The self-test's own battery roster and floor (#13489) ────────────────── -// -// `failures.length === 0` used to be this self-test's ONLY success condition, so -// "every case held" and "the cases never ran" printed the same line. Closed the -// way PR #13487 validated on check-doc-authoring: what is pinned is the -// registered NAMES, not a number. Every section opens with `battery('')`, -// every assertion is attributed to the battery most recently opened, and the -// floor requires the OPENED set to equal the DECLARED set with each battery at -// or above its own count. -// -// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 -// keeps a total "right" the moment a sibling grows. -// -// The counts are a FLOOR, not an equality — adding cases is ordinary work and -// must not red. A battery BELOW its floor means cases stopped running; the -// remedy is to find what stopped registering. -const SELF_TEST_BATTERIES = Object.freeze({ - 'The constant-vs-literal decomposition (the false 24-vs-23)': 7, - 'The region reader': 7, - 'The verdict': 4, - 'Absence is loud: every degraded input REFUSES rather than passing': 8, - 'The generator will not record a tree the pin does not name': 7, - 'The wiring this gate needs to be reachable at all': 12, - 'The containment predicate (#19969)': 10, - 'The in-tree oracle (no pin: the merged layout)': 18, -}); - -// DELETING an entry silences that battery's floor exactly as effectively as -// zeroing it, so the roster's own size is pinned too. -const SELF_TEST_BATTERY_FLOOR = 8; - -// The key an assertion is filed under when no battery is open. It is not a -// declared battery, so it reds by the same set difference rather than silently -// inflating whichever battery happened to run last. -const UNATTRIBUTED_BATTERY = '(no battery open)'; - -export function selfTest() { - // The battery ledger this self-test's floor is evaluated against (#13489). - // `battery()` opens a battery; every assertion below is attributed to the one - // most recently opened, so a section that stops running stops registering and - // names ITSELF at the floor rather than going quiet. - const seen = new Map(); - let openBattery = null; - const battery = (name) => { - openBattery = name; - }; - const registerCase = () => { - const b = openBattery ?? UNATTRIBUTED_BATTERY; - seen.set(b, (seen.get(b) ?? 0) + 1); - }; - const failures = []; - const check = (label, ok, detail) => { - registerCase(); - if (!ok) failures.push(detail === undefined ? label : `${label} — ${detail}`); - }; - const extract = (sources) => - extractDiagnosticCodes({ - files: Object.keys(sources), - read: (rel) => sources[rel], - }); - - // ── The constant-vs-literal decomposition (the false 24-vs-23) ──────────── - battery('The constant-vs-literal decomposition (the false 24-vs-23)'); - const literalSide = extract({ 'a.ts': FIXTURE_LITERAL_SIDE }); - const constantSide = extract({ 'b.ts': FIXTURE_CONSTANT_SIDE }); - check( - 'a code spelled as a literal on one side and a constant on the other reads the SAME on both', - literalSide.codes.join(',') === constantSide.codes.join(','), - `${literalSide.codes.join(',')} vs ${constantSide.codes.join(',')}`, - ); - check( - 'both sides read exactly the two codes the fixtures stamp', - literalSide.codes.join(',') === 'no-root,unconsumed-widget-option', - literalSide.codes.join(','), - ); - check( - 'the emitter\'s own `code: string` parameter is not a stamp', - !literalSide.codes.includes('string'), - literalSide.codes.join(','), - ); - check( - 'the Diagnostic type\'s `code: string` member is not a stamp', - literalSide.codes.length === 2, - `${literalSide.codes.length} code(s)`, - ); - check( - 'a `code: IDENT` written in PROSE or in a string is not a stamp', - !literalSide.codes.includes('PHANTOM_CODE') && !literalSide.codes.includes('ALSO_NOT_A_STAMP'), - literalSide.codes.join(','), - ); - // The instrument returns non-zero: drop the constant declaration and the two - // sides stop agreeing — which is the false difference this gate must not have. - const withoutDeclaration = extract({ - 'b.ts': FIXTURE_CONSTANT_SIDE.replace("export const UNCONSUMED_WIDGET_OPTION = 'unconsumed-widget-option';", ''), - }); - check( - 'without the constant declaration the code is NOT silently dropped — it is reported unresolvable', - withoutDeclaration.codes.join(',') === 'no-root' && withoutDeclaration.unresolved.length === 1, - `${withoutDeclaration.codes.join(',')} / ${withoutDeclaration.unresolved.length} unresolved`, - ); - const imported = extract({ 'c.ts': FIXTURE_UNRESOLVABLE }); - check( - 'a code stamped from an imported constant is unresolvable, not absent', - imported.codes.length === 0 && imported.unresolved.length === 1, - `${imported.codes.length} code(s), ${imported.unresolved.length} unresolved`, - ); - - // ── The region reader ──────────────────────────────────────────────────── - battery('The region reader'); - check('git blob id for an empty file', gitBlobHash('') === 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'); - check('git blob id for "x\\n"', gitBlobHash('x\n') === '587be6b4c3f93f93c489c0111bba5596147a26cb'); - check( - 'git blob id for "the JS literal subset\\n"', - gitBlobHash('the JS literal subset\n') === '75fa806558a3921e1cda522d5730f98129980925', - ); - const withRegion = `head\n${REGION_DELIMITER}\nalpha\nbeta\n`; - const region = readRegion(withRegion); - check('the region runs from the delimiter to end of file', region?.lines === 3, `${region?.lines} line(s)`); - check('a file without the delimiter yields null, never a shorter region', readRegion('head\nbody\n') === null); - check( - 'one changed character in the region changes its blob id', - readRegion(`head\n${REGION_DELIMITER}\nalpha\nbetb\n`)?.blob !== region?.blob, - ); - check( - 'text ABOVE the delimiter does not change the region blob id', - readRegion(`different head\n${REGION_DELIMITER}\nalpha\nbeta\n`)?.blob === region?.blob, - ); - - // ── The verdict ────────────────────────────────────────────────────────── - battery('The verdict'); - const slotReading = extractContainmentPredicate(FIXTURE_CONTAINMENT_SLOT, 'slot.ts'); - const flagReading = extractContainmentPredicate(FIXTURE_CONTAINMENT_FLAG, 'flag.ts'); - const goodRecord = { - objectui: { rev: 'a'.repeat(40) }, - recordedAgainstPin: 'b'.repeat(40), - grammarRegion: { lines: 3, blob: region.blob }, - diagnosticCodes: ['no-root', 'unconsumed-widget-option'], - containmentPredicate: { blob: slotReading.predicate?.blob, text: slotReading.predicate?.text }, - }; - const goodOurs = { - region, - codes: ['no-root', 'unconsumed-widget-option'], - unresolved: [], - containment: slotReading, - }; - const kinds = (f) => f.map((x) => x.slice(1, x.indexOf(']'))).join(','); - check('agreement is silent', judge({ record: goodRecord, ours: goodOurs, livePin: 'b'.repeat(40) }).length === 0); - check( - 'a moved pin is a finding', - kinds(judge({ record: goodRecord, ours: goodOurs, livePin: 'c'.repeat(40) })) === 'pin-moved', - ); - check( - 'a one-character grammar change is a finding', - kinds(judge({ - record: goodRecord, - ours: { ...goodOurs, region: readRegion(`head\n${REGION_DELIMITER}\nalpha\nbetb\n`) }, - livePin: 'b'.repeat(40), - })) === 'grammar-drift', - ); - check( - 'a code present on one side only is a finding, in either direction', - kinds(judge({ - record: goodRecord, - ours: { ...goodOurs, codes: ['no-root'] }, - livePin: 'b'.repeat(40), - })) === 'code-drift' - && kinds(judge({ - record: goodRecord, - ours: { ...goodOurs, codes: [...goodOurs.codes, 'brand-new'] }, - livePin: 'b'.repeat(40), - })) === 'code-drift', - ); - // ── Absence is loud: every degraded input REFUSES rather than passing ───── - battery('Absence is loud: every degraded input REFUSES rather than passing'); - check( - 'a missing record is a finding, not a pass', - kinds(judge({ record: null, ours: goodOurs, livePin: 'b'.repeat(40) })) === 'record-unusable', - ); - for (const [label, mutate] of [ - ['no rev', (r) => ({ ...r, objectui: {} })], - ['no pin', (r) => ({ ...r, recordedAgainstPin: undefined })], - ['no blob', (r) => ({ ...r, grammarRegion: { lines: 3 } })], - ['empty code list', (r) => ({ ...r, diagnosticCodes: [] })], - ]) { - check( - `a record with ${label} is unusable, not a pass`, - kinds(judge({ record: mutate(goodRecord), ours: goodOurs, livePin: 'b'.repeat(40) })) === 'record-unusable', - label, - ); - } - check( - 'a missing delimiter in THIS tree refuses instead of comparing', - kinds(judge({ record: goodRecord, ours: { ...goodOurs, region: null }, livePin: 'b'.repeat(40) })) - === 'region-unreachable', - ); - check( - 'an unresolvable code position refuses instead of comparing a short set', - kinds(judge({ - record: goodRecord, - ours: { ...goodOurs, unresolved: ['x.ts:1: SOMEWHERE_ELSE'] }, - livePin: 'b'.repeat(40), - })) === 'code-unresolvable', - ); - check( - 'an empty code set refuses instead of reporting agreement with a record that has codes', - kinds(judge({ record: goodRecord, ours: { ...goodOurs, codes: [] }, livePin: 'b'.repeat(40) })) - === 'code-set-empty', - ); - - // ── The generator will not record a tree the pin does not name ─────────── - // - // `judge()`'s `pin-moved` clause cannot reach this: it compares the STAMPED - // side against the pin file, two values from the same file. So the agreeing - // case below is asserted alongside the firing ones — a guard that fires on - // everything is as useless as one that fires on nothing, and the REVERSE - // direction is what says the good path still records. - battery('The generator will not record a tree the pin does not name'); - const PIN_A = '5'.repeat(40); - const PIN_B = '7'.repeat(40); - const recKinds = (f) => f.map((x) => x.slice(1, x.indexOf(']'))).join(','); - check( - '⭐ REVERSE: a checkout sitting ON the pin is recordable — no finding, so recording proceeds', - judgeRecordability({ head: PIN_A, livePin: PIN_A }).length === 0, - recKinds(judgeRecordability({ head: PIN_A, livePin: PIN_A })), - ); - check( - 'a checkout one commit off the pin is refused, not recorded', - recKinds(judgeRecordability({ head: PIN_B, livePin: PIN_A })) === 'head-off-pin', - ); - // `?? ''` rather than `[0].includes(...)`: with the guard ablated the array is - // EMPTY, and an uncaught TypeError there is a crash where a named failure - // belongs — the self-test would red for the right reason with the wrong words. - const offPinText = judgeRecordability({ head: PIN_B, livePin: PIN_A })[0] ?? ''; - check( - 'the refusal names BOTH revisions, so the operator can see which way to move', - offPinText.includes(PIN_B.slice(0, 12)) && offPinText.includes(PIN_A.slice(0, 12)), - offPinText || '(no finding at all)', - ); - // Absence and malformation are loud here too, and they are DISTINGUISHED: a - // pin that cannot be compared must not read as a checkout off the pin. - for (const [label, pin] of [ - ['empty', ''], - ['a short sha', PIN_A.slice(0, 12)], - ['undefined', undefined], - ]) { - check( - `a pin that is ${label} refuses as pin-unusable, never as head-off-pin`, - recKinds(judgeRecordability({ head: PIN_A, livePin: pin })) === 'pin-unusable', - recKinds(judgeRecordability({ head: PIN_A, livePin: pin })), - ); - } - check( - 'a HEAD that did not parse to a commit id refuses as head-unusable, never as head-off-pin', - recKinds(judgeRecordability({ head: 'HEAD', livePin: PIN_A })) === 'head-unusable', - recKinds(judgeRecordability({ head: 'HEAD', livePin: PIN_A })), - ); - - // ── The wiring this gate needs to be reachable at all ──────────────────── - battery('The wiring this gate needs to be reachable at all'); - const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')); - check( - 'the package script runs the self-test before the gate', - /--self-test/.test(pkg.scripts?.['check:sdui-lockstep'] ?? '') - && /check-sdui-lockstep\.mjs/.test(pkg.scripts?.['check:sdui-lockstep'] ?? ''), - pkg.scripts?.['check:sdui-lockstep'], - ); - check( - 'the regeneration alias exists and points at --update', - /--update/.test(pkg.scripts?.['gen:sdui-lockstep'] ?? ''), - pkg.scripts?.['gen:sdui-lockstep'], - ); - const lint = readFileSync(join(ROOT, '.github/workflows/lint.yml'), 'utf8'); - const wired = lint.split('\n').filter((line) => /run: pnpm check:sdui-lockstep\s*$/.test(line)); - check('lint.yml runs the gate exactly once', wired.length === 1, `found ${wired.length}`); - // The path literals are what `scripts/pm/dispatch-gates.mjs` derives this gate - // from. `.objectui-sha` is the one the card required by name (#12956): a pin - // bump must NAME this gate, not merely be covered by CI's unfiltered lint job. - const own = readFileSync(join(ROOT, 'scripts/check-sdui-lockstep.mjs'), 'utf8'); - const inCode = own.slice(own.indexOf('\nimport ')); - for (const literal of [PARSER_SRC, RECORD_FILE, REGION_FILE]) { - check( - `\`${literal}\` is a path literal in this gate's code, so the derivation can name it`, - inCode.includes(`'${literal}'`), - ); - } - // The pin is the one population a plain literal cannot carry — see - // ROOT_DIR_WATCH_HINTS. Both directions, so the declaration can neither go - // missing nor start claiming a file this gate does not open. - check( - 'the pin is declared in the subtree spelling the derivation can see', - ROOT_DIR_WATCH_HINTS.includes(`${PIN_FILE}/**`), - ROOT_DIR_WATCH_HINTS.join(','), - ); - check( - 'and nothing is declared that this gate does not read', - ROOT_DIR_WATCH_HINTS.every((hint) => hint.replace(/\/\*+$/, '') === PIN_FILE), - ROOT_DIR_WATCH_HINTS.join(','), - ); - check( - 'the plain literal is still spelled too, so the file the gate opens is named as written', - inCode.includes(`'${PIN_FILE}'`), - ); - // ⚠️ The two cases below are scoped to `update()`'s OWN BODY, never to the - // whole file. Measured: a whole-file scan for `judgeRecordability({ head:` - // matches the battery above, so deleting the generator's call left the - // self-test GREEN — the assertion was pinning its own test code. Every - // "is it wired" case has that shape; the slice is what makes it real. - const updateOpen = inCode.indexOf('\nfunction update() {'); - const updateClose = inCode.indexOf('\n}\n', updateOpen); - check( - "the generator's body is still addressable, so the cases below are not vacuous", - updateOpen !== -1 && updateClose > updateOpen && updateClose - updateOpen > 1000, - `open ${updateOpen}, close ${updateClose}`, - ); - const updateSrc = inCode.slice(updateOpen, updateClose); - // A `judgeRecordability` nothing calls is a phantom check: every case in the - // battery above stays green while the generator records from any tree it likes. - check( - 'the generator CALLS the recordability judge, so that battery is not phantom', - /judgeRecordability\(/.test(updateSrc), - ); - check( - 'and it stamps `recordedAgainstPin` from the value it compared, not from a second read', - /recordedAgainstPin: livePin,/.test(updateSrc) && !/recordedAgainstPin: readFileSync/.test(updateSrc), - ); - - // ── The containment predicate (#19969) ─────────────────────────────────── - // - // The comparison the code set and the region cannot make: WHEN a code fires. - // The first case is the reason the battery exists — the two predicates - // objectui#9910 moved between must read DIFFERENT, or the pin is blind to the - // very change that motivated it. - battery('The containment predicate (#19969)'); - check( - '⭐ the flag predicate and the declared-slot predicate read DIFFERENT', - slotReading.predicate !== null && flagReading.predicate !== null - && slotReading.predicate.blob !== flagReading.predicate.blob, - `${slotReading.predicate?.blob} vs ${flagReading.predicate?.blob}`, - ); - check( - 'the closure reaches the helper AND the constant the helper names, and nothing unreferenced', - slotReading.predicate?.reaches.join(',') === 'CHILD_LIST_KEY,acceptsChildren', - slotReading.predicate?.reaches.join(','), - ); - check( - 'a property NAME is not a reference (`input.name`, `node.children`), a flag read reaches nothing', - flagReading.predicate?.reaches.length === 0, - flagReading.predicate?.reaches.join(','), - ); - check( - 'comments and wrapping do not move the reading', - extractContainmentPredicate( - FIXTURE_CONTAINMENT_SLOT - .replace('/** Does it accept a child list? */', '/** Reworded entirely. */') - .replace('return comp.inputs.some((input) => input.name === CHILD_LIST_KEY);', - 'return comp.inputs\n .some((input) =>\n input.name === CHILD_LIST_KEY);'), - 'slot.ts', - ).predicate?.blob === slotReading.predicate?.blob, - ); - check( - 'a change two hops away — the constant the helper reads — moves the reading', - extractContainmentPredicate( - FIXTURE_CONTAINMENT_SLOT.replace("export const CHILD_LIST_KEY = 'children';", "export const CHILD_LIST_KEY = 'body';"), - 'slot.ts', - ).predicate?.blob !== slotReading.predicate?.blob, - ); - check( - 'a validate.ts with NO containment branch refuses, never reads as an empty predicate', - extractContainmentPredicate('export const x = 1;\n', 'none.ts').predicate === null, - ); - check( - 'a validate.ts with TWO containment branches refuses, never measures one of them', - extractContainmentPredicate(`${FIXTURE_CONTAINMENT_FLAG}\n${FIXTURE_CONTAINMENT_FLAG.replace('validateTree', 'again')}`, 'two.ts') - .predicate === null, - ); - check( - 'a predicate that differs from the record is a finding', - kinds(judge({ - record: goodRecord, - ours: { ...goodOurs, containment: flagReading }, - livePin: 'b'.repeat(40), - })) === 'containment-drift', - ); - check( - 'an unreadable predicate in THIS tree refuses instead of comparing', - kinds(judge({ - record: goodRecord, - ours: { ...goodOurs, containment: { predicate: null, why: 'two branches' } }, - livePin: 'b'.repeat(40), - })) === 'containment-unreachable', - ); - check( - 'a record without the predicate is unusable, not a pass', - kinds(judge({ - record: { ...goodRecord, containmentPredicate: undefined }, - ours: goodOurs, - livePin: 'b'.repeat(40), - })) === 'record-unusable', - ); - - // ── The in-tree oracle (no pin: the merged layout) ──────────────────────── - // - // Driven through `checkWithoutPin` over throwaway trees, so the path the - // production run takes — the presence test, `fingerprintOf` on both roots, - // the verdict — is the path asserted. The two ⭐ rows are the pair the mode - // owes: no pin with an agreeing `ui/` copy passes, no pin with no `ui/` copy - // REFUSES naming both. The `ui/` copy spells its code as a constant, the way - // objectui's does, so the decomposition is crossed between trees too. - battery('The in-tree oracle (no pin: the merged layout)'); - check('the pin wins when a tree has both', oracleFor({ pinPresent: true, uiPresent: true }) === 'pin'); - check('the pin alone is the pin oracle', oracleFor({ pinPresent: true, uiPresent: false }) === 'pin'); - check('no pin and a ui/ copy is the in-tree oracle', oracleFor({ pinPresent: false, uiPresent: true }) === 'in-tree'); - check('no pin and no ui/ copy is no oracle at all', oracleFor({ pinPresent: false, uiPresent: false }) === 'none'); - const PARSE_FIXTURE = `export const head = 1;\n${REGION_DELIMITER}\nexport const alpha = 2;\nexport const beta = 3;\n`; - const trees = []; - const inTree = ({ ui = {}, omitUi = false } = {}) => { - const root = mkdtempSync(join(tmpdir(), 'sdui-lockstep-tree-')); - trees.push(root); - const write = (dir, files) => { - mkdirSync(join(root, dir), { recursive: true }); - for (const [name, text] of Object.entries(files)) writeFileSync(join(root, dir, name), text); - }; - write(PARSER_SRC, { 'parse.ts': PARSE_FIXTURE, 'validate.ts': FIXTURE_CONTAINMENT_SLOT, 'codes.ts': FIXTURE_LITERAL_SIDE }); - if (!omitUi) { - write(UI_PARSER_SRC, { 'parse.ts': PARSE_FIXTURE, 'validate.ts': FIXTURE_CONTAINMENT_SLOT, 'codes.ts': FIXTURE_CONSTANT_SIDE, ...ui }); - } - // A throw is caught and named, so an ablated guard reds THIS row with its - // label rather than crashing the run before the rest report. - try { - return checkWithoutPin(root); - } catch (error) { - return { threw: String(error?.message ?? error).split('\n')[0] }; - } - }; - const treeKinds = (result) => { - if (result.threw) return `threw: ${result.threw}`; - return result.refusal ? 'refused' : kinds(result.findings); - }; - const agreeing = inTree(); - check( - '⭐ no pin + a ui/ copy that agrees: no finding, so the gate exits 0', - treeKinds(agreeing) === '', - treeKinds(agreeing) || '(none)', - ); - const noOracle = inTree({ omitUi: true }); - check('⭐ no pin + no ui/ copy: REFUSED, so the gate exits 1, never a pass', treeKinds(noOracle) === 'refused', treeKinds(noOracle)); - check( - 'and the refusal names BOTH oracles it looked for', - (noOracle.refusal ?? '').includes(PIN_FILE) && (noOracle.refusal ?? '').includes(UI_PARSER_SRC), - noOracle.refusal ?? '(no refusal)', - ); - const grammarDrift = inTree({ ui: { 'parse.ts': PARSE_FIXTURE.replace('beta', 'betb') } }); - check('a one-character grammar change in the ui/ copy is a finding', treeKinds(grammarDrift) === 'grammar-drift', treeKinds(grammarDrift)); - check( - 'and the finding names the in-tree copy, never a recorded objectui revision', - (grammarDrift.findings?.[0] ?? '').includes(`${UI_TREE}/${REGION_FILE}`) && !(grammarDrift.findings?.[0] ?? '').includes("objectui's"), - grammarDrift.findings?.[0] ?? '(no finding)', - ); - const codeDrift = inTree({ ui: { 'extra.ts': "export const later = { code: 'brand-new', message: 'x' };\n" } }); - check( - 'a code only the ui/ copy stamps is a finding — the direction the record could not see', - treeKinds(codeDrift) === 'code-drift', - treeKinds(codeDrift), - ); - const predicateDrift = inTree({ - ui: { 'validate.ts': FIXTURE_CONTAINMENT_SLOT.replace('!acceptsChildren(comp)', '!comp.isContainer') }, - }); - check('a changed containment predicate in the ui/ copy is a finding', treeKinds(predicateDrift) === 'containment-drift', treeKinds(predicateDrift)); - const noRegion = inTree({ ui: { 'parse.ts': 'export const head = 1;\n// no delimiter here\n' } }); - check( - 'a ui/ copy without the region delimiter refuses, never compares a shorter region', - treeKinds(noRegion) === 'ui-region-unreachable', - treeKinds(noRegion), - ); - const unresolvableThere = inTree({ ui: { 'extra.ts': FIXTURE_UNRESOLVABLE } }); - check( - 'an unresolvable code position in the ui/ copy refuses instead of comparing a short set', - treeKinds(unresolvableThere) === 'ui-code-set-unreadable', - treeKinds(unresolvableThere), - ); - const noPredicate = inTree({ ui: { 'validate.ts': 'export const x = 1;\n' } }); - check( - 'a ui/ copy whose predicate cannot be located refuses instead of comparing', - treeKinds(noPredicate) === 'ui-containment-unreachable', - treeKinds(noPredicate), - ); - const unresolvableHere = inTree(); - check( - 'this copy is held to its own refusals in this mode too', - kinds(judgeInTree({ ours: { ...unresolvableHere.ours, unresolved: ['x.ts:1: SOMEWHERE_ELSE'] }, theirs: unresolvableHere.theirs })) - === 'code-unresolvable', - ); - for (const root of trees) rmSync(root, { recursive: true, force: true }); - check('the in-tree path is spelled from the same two parts the reads join', UI_PARSER_SRC === `${UI_TREE}/${PARSER_SRC}`, UI_PARSER_SRC); - check( - `\`${UI_PARSER_SRC}\` is a path literal in this gate's code, so a diff under it derives this gate`, - inCode.includes(`'${UI_PARSER_SRC}'`), - ); - const mainOpen = inCode.indexOf('\nfunction main() {'); - const mainSrc = inCode.slice(mainOpen, inCode.indexOf('\n}\n', mainOpen)); - check( - 'main() sends a pin-less tree to the in-tree run BEFORE it reads the record', - mainOpen !== -1 && mainSrc.includes('mainWithoutPin();') - && mainSrc.indexOf('mainWithoutPin();') < mainSrc.indexOf('readFileSync(join(ROOT, RECORD_FILE)'), - ); - const noPinOpen = inCode.indexOf('\nfunction mainWithoutPin() {'); - check( - 'and the in-tree run judges through checkWithoutPin, so the rows above are not phantom', - noPinOpen !== -1 && inCode.slice(noPinOpen, inCode.indexOf('\n}\n', noPinOpen)).includes('checkWithoutPin(ROOT)'), - ); - - // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── - // - // Evaluated after every battery has had its chance and BEFORE the verdict, so - // the success line below can only be printed by a run in which the set of - // batteries that registered assertions EQUALS the set declared. A set - // difference names WHICH battery stopped; a count says only that something did. - const floorFailure = (message) => { - failures.push(message); - }; - const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); - let floorBreached = false; - if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { - floorBreached = true; - floorFailure( - `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + - `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, - ); - } - for (const [name, count] of seen) { - if (declaredBatteries.includes(name)) continue; - floorBreached = true; - floorFailure( - `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + - 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', - ); - } - for (const name of declaredBatteries) { - const count = seen.get(name) ?? 0; - if (count >= SELF_TEST_BATTERIES[name]) continue; - floorBreached = true; - floorFailure( - count === 0 - ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + - 'The verdict below would have claimed those cases hold.' - : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + - `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, - ); - } - if (floorBreached) { - floorFailure( - 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + - 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + - 'skips) and restore it.', - ); - } - if (failures.length) { - console.error('check:sdui-lockstep --self-test FAILED'); - for (const failure of failures) console.error(` - ${failure}`); - process.exit(1); - } - console.log( - 'check:sdui-lockstep --self-test passed (the constant-vs-literal decomposition and its unresolvable ' - + 'direction, the region reader in both drift directions, all four refusal classes, the generator\'s ' - + 'HEAD-vs-pin refusal in both directions, the CI wiring, the containment predicate in both ' - + 'directions, and the in-tree oracle with no pin in both directions)', - ); - - return SELF_TEST_VERDICT; -} - -function main() { - if (process.argv.includes('--self-test')) { - if (selfTest() !== SELF_TEST_VERDICT) { - console.error( - '\n✗ check-sdui-lockstep self-test: selfTest() returned without reaching its verdict,\n' - + 'so no success line was printed. Exiting 0 here would report a self-test\n' - + 'that never finished as a self-test that passed.\n', - ); - process.exit(1); - } - return; - } - if (process.argv.includes('--update')) { - update(); - return; - } - if (!existsSync(join(ROOT, PIN_FILE))) { - mainWithoutPin(); - return; - } - - let record = null; - try { - record = JSON.parse(readFileSync(join(ROOT, RECORD_FILE), 'utf8')); - } catch (error) { - console.error( - `\ncheck-sdui-lockstep: REFUSED — ${RECORD_FILE} could not be read or parsed (${error.message}).\n` - + ' Nothing was compared. Regenerate it from an objectui checkout: pnpm gen:sdui-lockstep\n', - ); - process.exit(1); - } - - let livePin = null; - try { - livePin = readFileSync(join(ROOT, PIN_FILE), 'utf8').trim(); - } catch (error) { - console.error(`\ncheck-sdui-lockstep: REFUSED — ${PIN_FILE} could not be read (${error.message}).\n`); - process.exit(1); - } - - const ours = fingerprintOf(ROOT); - const findings = judge({ record, ours, livePin }); - - if (findings.length === 0) { - console.log( - `check:sdui-lockstep: OK — this copy is byte-identical to objectui@${record.objectui.rev.slice(0, 12)} ` - + `(${record.objectui.revDate}) over ${ours.region.lines} grammar line(s) ` - + `[blob ${ours.region.blob.slice(0, 12)}], agrees on all ${ours.codes.length} diagnostic code(s) ` - + `and on the \`${CONTAINMENT_CODE}\` predicate [blob ${ours.containment.predicate.blob.slice(0, 12)}], ` - + `across ${ours.files.length} non-test source(s).`, - ); - console.log( - ` Recorded against pin ${record.recordedAgainstPin.slice(0, 12)}; the live pin is ${livePin.slice(0, 12)}. ` - + 'This gate does NOT see objectui moving after that revision — only a pin bump brings that question back.', - ); - return; - } - - console.error(`\ncheck:sdui-lockstep: ${findings.length} finding(s) — the two sdui-parser copies have drifted\n`); - for (const finding of findings) console.error(` ${finding}\n`); - console.error( - ' Why this matters: objectui\'s copy runs in the RENDERER and this one runs in the SAVE GATE. When they\n' - + ' disagree, a page saves clean and renders inert (#12719) — a surface-dependent failure that reaches an\n' - + ' author as intermittent, so nothing pushes it back up.\n', - ); - process.exit(1); -} - -/** `main()` for a tree with no pin: the in-tree oracle (see the header). */ -function mainWithoutPin() { - const { refusal, ours, theirs, findings } = checkWithoutPin(ROOT); - if (refusal) { - console.error(`\ncheck-sdui-lockstep: REFUSED — ${refusal}\n`); - process.exit(1); - } - - if (findings.length === 0) { - console.log( - `check:sdui-lockstep: OK (in-tree) — this copy is byte-identical to ${UI_PARSER_SRC} over ${ours.region.lines} ` - + `grammar line(s) [blob ${ours.region.blob.slice(0, 12)}], agrees on all ${ours.codes.length} diagnostic code(s) ` - + `and on the \`${CONTAINMENT_CODE}\` predicate [blob ${ours.containment.predicate.blob.slice(0, 12)}], ` - + `across ${ours.files.length} and ${theirs.files.length} non-test source(s).`, - ); - console.log( - ` No ${PIN_FILE} in this tree, so the renderer's copy is the in-tree one and ${RECORD_FILE} was not ` - + 'consulted. Both copies were read now, so either one moving shows here.', - ); - return; - } - - console.error(`\ncheck:sdui-lockstep: ${findings.length} finding(s) — the two sdui-parser copies have drifted\n`); - for (const finding of findings) console.error(` ${finding}\n`); - console.error( - ` Why this matters: the copy under ${UI_TREE}/ runs in the RENDERER and this one runs in the SAVE GATE. When\n` - + ' they disagree, a page saves clean and renders inert (#12719). Port the change to the other copy in the\n' - + ' same PR.\n', - ); - process.exit(1); -} - -// Exports bindings, so an import for those exports alone must run nothing (#10667). -if (isEntrypoint(import.meta.url)) { - main(); -} diff --git a/scripts/check-sdui-manifest.mjs b/scripts/check-sdui-manifest.mjs index b66bf52e59f..bd852cef52a 100644 --- a/scripts/check-sdui-manifest.mjs +++ b/scripts/check-sdui-manifest.mjs @@ -40,9 +40,8 @@ * 3. STALENESS — the record's `objectuiSha` equals the live `.objectui-sha`. * A pin bump changes which registry the shipped console runs, so the bump * PR goes red HERE until the manifest is regenerated against the new pin - * — the same moment `check-sdui-lockstep` already forces a parser parity - * re-verification, and the moment an objectui checkout is guaranteed to - * exist (the bump required one). + * — the moment an objectui checkout is guaranteed to exist (the bump + * required one). * 4. VERSION ↔ PIN CORRESPONDENCE — the record's `objectuiWorkspaceVersion` * is the version `packages/core/package.json` DECLARES at the pinned * commit. Checks 1-3 reach both fields and relate neither: the version is @@ -100,12 +99,11 @@ * ## What this deliberately does NOT do, and where that risk is held * * No per-PR regeneration: that would put an objectui clone and build (network, - * minutes) inside a required lint job — the shape `check-sdui-lockstep`'s - * header declines, for the same reasons. Under an unchanged pin the pinned + * minutes) inside a required lint job, a network dependency this repo + * declines in a lint job. Under an unchanged pin the pinned * commit's registry is immutable, so content drift per-PR is not a live axis. * The residual axis — this repo's OWN adapter (`manifestFromConfigs`) changing - * while the pin stands still — is held by `packages/sdui-parser`'s lockstep - * gate and unit suite, and by regeneration being byte-deterministic (two runs + * while the pin stands still — is held by `packages/sdui-parser`'s unit suite, and by regeneration being byte-deterministic (two runs * over one built tree, identical bytes), so the remedy this gate prints always * converges. * diff --git a/scripts/gen-sdui-manifest-node.mjs b/scripts/gen-sdui-manifest-node.mjs index 49330951cc4..6b523119638 100644 --- a/scripts/gen-sdui-manifest-node.mjs +++ b/scripts/gen-sdui-manifest-node.mjs @@ -24,9 +24,9 @@ * wire it). The enumeration mirrors objectui's own generator page * (`apps/console/dev/manifest-dump.tsx`) exactly: eager-import the 16 registry * modules IN ITS ORDER, read `ComponentRegistry.getPublicConfigs()`, and - * serialize through `manifestFromConfigs` from `@objectstack/sdui-parser` — - * the hoisted, lockstep-pinned copy of objectui's adapter - * (`pnpm check:sdui-lockstep` holds the two copies byte-equal). + * serialize through `manifestFromConfigs` from `@objectstack/sdui-parser`, + * the one parser the save gate runs (#25712 item 3 retires the renderer's + * second copy under `ui/packages/sdui-parser`). * * `apps/console/node_modules/@object-ui/` in the built tree carries every * registry package as a workspace symlink whose `exports["."].import` is diff --git a/scripts/pm/measurement-claim-triage.mjs b/scripts/pm/measurement-claim-triage.mjs index 1514945822c..1feb2d44fb3 100644 --- a/scripts/pm/measurement-claim-triage.mjs +++ b/scripts/pm/measurement-claim-triage.mjs @@ -414,8 +414,6 @@ const TRIAGE = [ verdict: 'REPAIRED', why: 'kind 3 — same shape over the whole publishable workspace, which moves on every package add or removal' }, { file: 'scripts/check-release-section-coverage.mjs', match: 'assertion 1 fires on 24 minors', verdict: 'ILLUSTRATION', why: 'the figures justify shipping WITH a baseline, and the ruling that fixes them is dated in the sentence before' }, - { file: 'scripts/check-sdui-lockstep.mjs', match: 'measured on this tree at 96 hint literals', - verdict: 'ILLUSTRATION', why: '"at 96 hint literals" is the CONDITION of the reading, not its subject: the claim is about which leading-dot literals the allowlist admits' }, { file: 'scripts/check-skill-compatibility-version.mjs', match: 'both were measured on this tree', verdict: 'REPAIRED', why: 'kind 3 — four precision ratios deciding which roots are declared, all moving with the tree and none reprinted' }, { file: 'scripts/check-slot-lookup-ratchet.mjs', match: 'Measured on this tree: 46s', diff --git a/scripts/pm/os-regen-merge.sh b/scripts/pm/os-regen-merge.sh index e58b172a519..efb9d50ce6e 100644 --- a/scripts/pm/os-regen-merge.sh +++ b/scripts/pm/os-regen-merge.sh @@ -389,9 +389,8 @@ # baseline.json` and their neighbours are SHRINK-ONLY RATCHETS whose own # entries say a merge must NEVER recompute them: a mid-merge regeneration # records whatever the half-merged tree happens to compile to and enters it as -# merge noise instead of as red. `objectui-lockstep.json` cannot be -# regenerated here at all (it needs a sibling checkout), and the scaffold -# templates' generator refuses a file it did not already stamp. Every one of +# merge noise instead of as red, and the scaffold templates' generator +# refuses a file it did not already stamp. Every one of # these is resolved BY HAND and regenerated later, or not at all. # # So class 3 prints one of two per-path readings, and the discriminator is ⛔ NOT diff --git a/scripts/regen-artifacts.mjs b/scripts/regen-artifacts.mjs index d1869d5fec0..2eca3b1cab4 100644 --- a/scripts/regen-artifacts.mjs +++ b/scripts/regen-artifacts.mjs @@ -734,22 +734,6 @@ export const NOT_DRIVER_MANAGED = Object.freeze([ + 'report those 25 as repaired — a config-tier flip that looks exactly like somebody typing 25 ' + 'narrowings — and would rewrite the per-signature keys underneath with it.', }, - { - path: 'packages/sdui-parser/objectui-lockstep.json', - gen: 'gen:sdui-lockstep', - owner: ROOT_OWNER, - why: - 'a VENDORED RECORD OF ANOTHER REPOSITORY, and the only entry here that a merge could not ' - + "regenerate even if regenerating were right. `--update` re-records objectui's side of the " - + 'sdui-parser lockstep and needs an objectui CHECKOUT to do it; a merge driver has no ' - + 'network, no build and no sibling checkout, so "recompute from the merged sources" names ' - + 'sources that are not in this tree at all. And the file is a human decision twice over — ' - + 'the pinned `.objectui-sha` records WHICH objectui revision someone ported to, which is an ' - + 'act of porting and not a projection of this repo. Regenerating it during a merge would ' - + 'either fail or silently re-point the lockstep at whatever checkout happened to be on disk, ' - + 'and a wrong answer here means the save gate and the renderer accept different grammars ' - + 'while every gate stays green.', - }, { path: 'skills/README.md', gen: 'gen:skill-docs',