feat(scripts): a gate that fails when prose about the installed @objectstack pin disagrees with the artifact (objectui#8924) - #9094
Conversation
…isagrees with the artifact (objectui#8924) objectui#8897 corrected the six sentences that said the pin is 17.3.0, using a probe that was version-literal on 17.3.0 and therefore blind by construction to the identical sentence about an earlier pin. Correcting a version number with nothing reading it regenerates the card at the next bump, so this adds the reader instead of another sweep. `scripts/check-installed-spec-pin-claims.mjs` resolves the pin from two faces (`pnpm-lock.yaml` and the resolved tree, which must agree or nothing is judged), recognises claims by PREDICATE — a version plus a present-tense installed-ness marker, minus semver ranges, CHANGELOGs and pending changesets — and fails when a claim and the artifact disagree. The distinction that must survive is enforced in two layers: eight documented historical cues decline sentences about WHEN something changed, and the LEDGER classifies the rest as `stale` or `historical`. Both are pinned on real lines of this tree, each refusal paired with a firing control. Measured on 16fc4cf: 68 claim sites in 55 files; 9 name the pin correctly and go red at the next bump, 59 are off it across 16 packages and `scripts/`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
…jectui#8924) `check:entry-guard` fails a hand-typed `process.argv[1]` comparison, and it is right to: node leaves `process.argv[1]` as the caller typed it, so a script reached through a symlink compares two different paths, answers false and does nothing — exit 0, no output, which a wrapper holding `result.status` reads as a pass. `isEntrypoint(import.meta.url)` is the one predicate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
Landing provenance — PR #9094 (card objectui#8924)Head ① contract review — NOT OWED. ② gates —
③ checks. 34 check-runs on charter provenance. Seven governing files byte-identical between ⭐ What this PR deliberately does NOT do, and why that is the right callIt ships the pin, not the sweep. The 47 stale sentences are not restamped, and that is the correct reading of the card rather than an omission: each site states a measured fact as well as a version, so moving the number without re-parsing the fact against the resolved artifact would plant a fresh false premise — the exact hazard the card was filed about. The ledger becomes the machine-checked worklist for that burn-down instead. The ledger ratchets both ways: new drift fails, and an entry whose site was repaired fails just as loudly ("Delete the entry in the same change that repaired the site"), so the list cannot rot into a permanent hole. ⭐ The population was re-derived rather than inherited: 68 recognised sites / 55 files, of which 9 are AT the pin (true today, red the day the pin moves) and 59 off it; 53 ledger entries, 47 stale and 12 historical. The card's ~53/46/13 was taken at a different commit and was ⛔ not carried. ⭐ The "when it changed" refusal has two layers and both are demonstrated on REAL lines located at run time, ⛔ not on copied-in fixtures — with a firing control for each: the same line with
Landing by squash through the merge queue. Generated by Claude Code |
Part of #8924
A pin over the claim, not a sweep of corrected sentences. objectui#8897 corrected six statements that said the installed
@objectstack/specpin is 17.3.0, with a probe that was version-literal on17.3.0— blind by construction to the identical sentence written about an earlier pin. A corrected number with nothing reading it regenerates the card at the next bump, so this adds the reader.The pin, from two faces
pnpm-lock.yaml@objectstack/spec17.4.0, on 2 snapshot keysgrep -n '@objectstack/spec@' pnpm-lock.yamlnode -e "console.log(require('./node_modules/@objectstack/spec/package.json').version)"They agree.
@objectstack/clientalso resolves 17.4.0 on both faces;@objectstack/coreis in the lockfile but linked into no workspace package, so the gate reports it as transitive and judges no claim about it rather than judging against a number only one face can see.The re-derived population — measured on this branch point, NOT carried from the card
The card's
~53 sites / 46 files / 13 packageswas taken at4fa0eb9c4on 2026-09-10. Re-measured by predicate on16fc4cf20:scripts/stale(real drift, awaiting re-measurement)historical(about WHEN something changed — never restamp)It differs from the card's figure and is expected to: a day of commits, and a predicate that is not the card's prose but an executable one. The full listing is
node scripts/check-installed-spec-pin-claims.mjs --explain.The predicate, verbatim from the gate: a version token (three parts, prerelease tail kept whole) that is not preceded by a range operator, on a line carrying one of the five installed-ness markers objectui#8924 named —
installed,this repo's pin,the pin is,pinned,at that pin— each word-bounded, withCHANGELOG,.changeset/**, the lockfile and the gate's own two files excluded by path.The "when it changed" refusal, with firing controls
⭐ A refusal shown only where nothing could have fired is not evidence of a distinction. Every refusal below is demonstrated on a real line of this tree, located at run time by a stable substring, and paired with a control that makes the same line fire.
1. Refused at the marker, by a word boundary.
apps/console/src/__tests__/registry-inputs-spec-parity.test.tscarries'EMPTY SPEC SHAPE. New in @objectstack/spec 17.3.0, … Registered propless by @object-ui/app-shell (src/console/marketplace/InstalledListWidget.tsx)'. It is a textbook "when it changed" sentence naming an in-range version, and the lettersinstalledappear in it only inside the identifierInstalledListWidget. The obvious unbounded marker puts it in the population, and restamping it would delete a true fact about 17.3.0.recogniseLine(line).verdict === 'none'New inreplaced bythe pin isreturnsclaimnaming17.3.0— so the version token is reachable on that exact line, and only the missing marker declined it.2. Refused at a cue, by tense. The same file's
// them was the installed contract, not a judgement — @objectstack/spec 17.0.0.refused,refusedBy: 'was-the-installed'— and the test also asserts the marker and the version were both found, so nothing is declining on a technicality; the sentence's tense is.was the installed→is the installedreturnsclaimnaming17.0.0.All eight historical cues carry the real line they were taken from, and a test asserts each cue still matches its own citation, so the set cannot rot one entry at a time.
What fails when a claim drifts — ablation on the real tree, both legs
Mutation proven on disk by blob hash, restored by
git checkout HEAD -- PATH(never the bare form, which restores from the index), restoration proven by hash equality and an emptygit diff HEAD.So the ledger ratchets in both directions: new drift fails, and an entry naming a claim that is no longer there fails just as loudly. A partial repair fails on the count. Nine
judge()unit tests pin the same behaviour on synthetic inputs, including that a claim at the pin needs no entry and turns red the moment the pin moves.What this deliberately does NOT do
⛔ It does not restamp the 47
stalesentences, and the ledger is not a licence to. Each site states a measured FACT as well as a version — "installed 17.2.0:SelectOptionSchemais.strict()" is two claims — and the fact has to be re-parsed against the resolved artifact before the number moves, or the repair plants a fresh false premise, which is strictly worse than the stale one. objectui#8897 did that re-parse for each of its six. The unit of the remaining work is one measurement per site, and the ledger is now the machine-checked worklist for it: burn an entry down and the gate makes you delete it in the same change.Two limits are written into the gate header rather than left to be discovered:
packages/i18n/src/utils/spec-formatters.tsends a line with "The pinned" and opens the next with "17.0.0-rc.6 removed all four". That claim is as stale as any in the ledger and this instrument cannot see it.packages/plugin-form/src/sectionFields.spec-parity.test.tssays "this repo is pinned to^17.0.0-rc.6", which is false and is a range, so it is recorded here and not silently covered.Where it runs
The vitest suite is the blocking copy —
scripts/__tests__/check-installed-spec-pin-claims.test.tsruns the gate over the real tree, the same placementscripts/__tests__/doc-version-claims.test.tshas. This reads text and a lockfile, needs no build and no network, so a dedicated workflow would be a second thing to keep wired and nothing else.pnpm check:installed-pin-claimsis the human entry point. ⛔ No workflow file is touched.Verification
All runs below are at the final commit⚠️ The first push was RED on one step and the fix is the second commit:
1987db23e, clean tree.check:entry-guardrefuses a hand-typedprocess.argv[1]entry guard, because node leavesprocess.argv[1]as the caller typed it — a script reached through a symlink compares two different paths, answers false and does nothing, with exit 0 and no output, which a wrapper holdingresult.statusreads as a pass. The gate now usesisEntrypoint(import.meta.url). That gate is inlint.ymland not in the local list this PR's author derived; it is in the table below now.pnpm exec vitest run scripts/__tests__/check-installed-spec-pin-claims.test.tspnpm exec vitest run scripts/(the whole gate-test tree, for siblings)node scripts/check-entry-guard.mjsscripts/files, 0 hand-typed guardsnode scripts/check-lint-coverage.mjs·check:upstream-port-parity·check:bash32-floor·check:vi-mock-override-shapenode scripts/check-installed-spec-pin-claims.mjspnpm type-check:scriptspnpm check:control-bytespnpm check:new-line-citationspnpm check:unreferenced-sources·check:esm-specifiers·check:self-import·check:phantom-deps·check:shell-escape-residuenode scripts/check-changeset-presence.mjsnode scripts/check-changeset-no-major.mjsnode scripts/check-governed-queue-guard.mjs --teston all four pathsAGENTS.mdexits 3, GOVERNEDESLint, narrowed — and the narrowing is measured, not assumed. ① The population is
eslint .under this repo's own config, whoseignorescover only build output (**/dist,**/.next,**/node_modules,**/public,**/.source);scripts/is in scope. ②pnpm exec eslint --no-inline-config --format jsonon the two new files reports 2 files linted, 0 errors, 0 warnings — and two files is the entire lintable half of this diff (the other two changed paths are.jsonand.md). ③ Invariance:eslint.config.jscontains 0 occurrences each ofprojectService,parserOptionsandproject:, so type-aware linting is not enabled and this diff cannot move the verdict on any file it does not contain.Manual control-byte sweep beyond the gate, on the four changed paths:
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'exits 1 with 0 matches.验收备注
Observations recorded, not filed — each would be carried by whoever burns the ledger down, so none needs its own card today:
packages/plugin-form/src/sectionFields.spec-parity.test.tsstates a false range ("pinned to^17.0.0-rc.6"). Ranges are outside objectui#8924's predicate by construction, so the gate records the line's historical half and says nothing about the range.packages/i18n/src/utils/spec-formatters.tscarries a stale claim split across two lines, invisible to a per-line predicate; the ledger entry beside it names the hole.scripts/check-spec-symbol-derivation.mjscarries "Re-measured at spec 17.2.0" — a stamp shape the five markers objectui#8924 named do not cover, so it is out of the population rather than in it.packages/layout/src/ResponsiveGrid.tsx,packages/types/src/mobile.ts) say they expire on a pin bump that has now happened twice. Their substance rests on an ALLOW entry inscripts/check-spec-symbol-derivation.mjswhose ratchet 3 forces the deletion, so re-measuring them is a real piece of work rather than a restamp.🤖 Generated with Claude Code
https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
Generated by Claude Code
Generated by Claude Code