Skip to content

feat(scripts): a gate that fails when prose about the installed @objectstack pin disagrees with the artifact (objectui#8924) - #9094

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-8924-installed-pin-claim-rot
Sep 11, 2026
Merged

feat(scripts): a gate that fails when prose about the installed @objectstack pin disagrees with the artifact (objectui#8924)#9094
os-warren merged 2 commits into
mainfrom
claude/issue-8924-installed-pin-claim-rot

Conversation

@os-warren

@os-warren os-warren commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Part of #8924

A pin over the claim, not a sweep of corrected sentences. objectui#8897 corrected six statements that said the installed @objectstack/spec pin is 17.3.0, with a probe that was version-literal on 17.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

face reading command
pnpm-lock.yaml @objectstack/spec 17.4.0, on 2 snapshot keys grep -n '@objectstack/spec@' pnpm-lock.yaml
resolved tree 17.4.0 at the root and in every workspace copy node -e "console.log(require('./node_modules/@objectstack/spec/package.json').version)"

They agree. @objectstack/client also resolves 17.4.0 on both faces; @objectstack/core is 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 packages was taken at 4fa0eb9c4 on 2026-09-10. Re-measured by predicate on 16fc4cf20:

$ node scripts/check-installed-spec-pin-claims.mjs
  pin   @objectstack/spec = 17.4.0   (pnpm-lock.yaml AND the resolved tree agree)
  pin   @objectstack/client = 17.4.0   (pnpm-lock.yaml AND the resolved tree agree)
  pin   @objectstack/formula = 17.4.0   (pnpm-lock.yaml AND the resolved tree agree)
  (transitive) @objectstack/core = 17.4.0 in the lockfile, linked into no workspace package

  68 recognised claim site(s) — 9 at the pin, 53 ledger entr(ies)
  ledgered sites by class: historical 12, stale 47
  exit 0
reading count
recognised present-tense claim sites 68 in 55 files
of those, naming the pin correctly (the live half — red at the next bump) 9 in 8 files
of those, off the pin 59 in 48 files, across 16 workspace packages and scripts/
ledger entries (file + package + version), each with a class and a reason 53
off-pin sites classed stale (real drift, awaiting re-measurement) 47
off-pin sites classed historical (about WHEN something changed — never restamp) 12
lines declined by a historical cue before ever reaching the ledger 10

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, with CHANGELOG, .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.ts carries '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 letters installed appear in it only inside the identifier InstalledListWidget. The obvious unbounded marker puts it in the population, and restamping it would delete a true fact about 17.3.0.

  • refusal: recogniseLine(line).verdict === 'none'
  • ⭐ firing control: the same line with New in replaced by the pin is returns claim naming 17.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.

  • refusal: verdict 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.
  • ⭐ firing control: was the installedis the installed returns claim naming 17.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 empty git diff HEAD.

HEAD blob packages/types/src/views.ts        = 29824e7eb7f801dcde2297438a4c87ba981fd984
HEAD blob scripts/check-spec-range-floors.mjs = 0de1ebe8701e1b6f36ec150083f1fae9e16c3296

LEG 1 — a live at-pin claim drifts (17.4.0 -> 17.3.0 in views.ts)
  '17.4.0 @objectstack' count BEFORE = 1   AFTER = 0
  '17.3.0 @objectstack' count AFTER  = 1
  mutated blob = b61f0851d6ca2e8e4390ca7ad1916787a1489510   (differs from HEAD blob)
  gate exit = 1
  FAIL packages/types/src/views.ts:91 claims @objectstack/spec 17.3.0, the artifact resolves 17.4.0.
  restored blob = 29824e7eb7f801dcde2297438a4c87ba981fd984   git diff HEAD exit=0 (empty)

LEG 2 — a ledgered site is repaired, ledger not updated (17.2.0 -> 17.4.0 in check-spec-range-floors.mjs)
  'which is 17.2.0' count BEFORE = 1   AFTER = 0
  'which is 17.4.0' count AFTER  = 1
  gate exit = 1
  FAIL LEDGER is stale: scripts/check-spec-range-floors.mjs no longer carries a drifting
       @objectstack/spec 17.2.0 claim. Delete the entry in the same change that repaired the site.
  restored blob = 0de1ebe8701e1b6f36ec150083f1fae9e16c3296   git diff HEAD exit=0 (empty)

BASELINE after both restores: gate exit = 0

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 stale sentences, and the ledger is not a licence to. Each site states a measured FACT as well as a version — "installed 17.2.0: SelectOptionSchema is .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:

  • Claims split across lines are invisible. packages/i18n/src/utils/spec-formatters.ts ends 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.
  • Declared ranges are out of the predicate by construction. packages/plugin-form/src/sectionFields.spec-parity.test.ts says "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.ts runs the gate over the real tree, the same placement scripts/__tests__/doc-version-claims.test.ts has. 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-claims is the human entry point. ⛔ No workflow file is touched.

Verification

All runs below are at the final commit 1987db23e, clean tree. ⚠️ The first push was RED on one step and the fix is the second commit: check:entry-guard refuses a hand-typed process.argv[1] entry guard, because node leaves process.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 holding result.status reads as a pass. The gate now uses isEntrypoint(import.meta.url). That gate is in lint.yml and not in the local list this PR's author derived; it is in the table below now.

run result
pnpm exec vitest run scripts/__tests__/check-installed-spec-pin-claims.test.ts 29 passed (29), exit 0
pnpm exec vitest run scripts/ (the whole gate-test tree, for siblings) 140 files passed, 2 skipped; 4168 tests passed, exit 0
node scripts/check-entry-guard.mjs exit 0 — 86 scripts/ files, 0 hand-typed guards
node scripts/check-lint-coverage.mjs · check:upstream-port-parity · check:bash32-floor · check:vi-mock-override-shape exit 0 each
node scripts/check-installed-spec-pin-claims.mjs exit 0
pnpm type-check:scripts exit 0
pnpm check:control-bytes exit 0 — 7282 tracked text files scanned
pnpm check:new-line-citations exit 0 — 0 new citations
pnpm check:unreferenced-sources · check:esm-specifiers · check:self-import · check:phantom-deps · check:shell-escape-residue exit 0 each
node scripts/check-changeset-presence.mjs exit 0 — "0 of them published source of a package the release covers"
node scripts/check-changeset-no-major.mjs exit 0
node scripts/check-governed-queue-guard.mjs --test on all four paths exit 0, NOT GOVERNED; control on AGENTS.md exits 3, GOVERNED

ESLint, narrowed — and the narrowing is measured, not assumed. ① The population is eslint . under this repo's own config, whose ignores cover only build output (**/dist, **/.next, **/node_modules, **/public, **/.source); scripts/ is in scope. ② pnpm exec eslint --no-inline-config --format json on 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 .json and .md). ③ Invariance: eslint.config.js contains 0 occurrences each of projectService, parserOptions and project:, 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.ts states 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.ts carries 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.mjs carries "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.
  • The two self-expiring notes (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 in scripts/check-spec-symbol-derivation.mjs whose 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

…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

Copy link
Copy Markdown
Collaborator Author

Landing provenance — PR #9094 (card objectui#8924)

Head 1987db23e8341e1ee07bd9f9dcebc34991f7b5bf. Gate checkout: objectstack clone os-fresh2, origin/main = abc4b83ce8b0….

① contract review — NOT OWED. Clause-②: no, both carriers absent and agreeing. ⛔ No PASS cited because none exists and none is owed.

② gatescheck-clause2-carriers --pair 9094 0 ("readable in the fixed spelling and both carriers agree") · check-governed-merges --pr objectstack-ai/objectui#9094 0 (NOT governed) · check-widening-tells --declaration no 0 ⇒ but see below.

⚠️ The widening-tells exit 0 is not a clean reading and the tool says so: "⛔ NOTHING on this diff was examined for widening tells, so this exit 0 is evidence about no surface at all." 4 of 4 paths NOT MEASURED. My firing control fired (exit 4) but on packages/types/src/zod/layout.zod.ts, a path this PR does not touch ⇒ it shows the tool works, ⛔ not that a tell could have fired here. The support for "widens nothing" is the governed-surface predicate and the file list.

③ checks. 34 check-runs on 1987db23e…, all completed, 0 non-green, reported total_count 34 == returned array length 34. ⚠️ Lint FAILED on the previous head 6dbf99620 (step "Verify every scripts/ entry guard goes through one predicate"); 1987db23e is the fix and Lint now reads completed/success. ⛔ Landing waited for that, and for 9 checks that were in_progress at 00:55Z — an in_progress check is not a green one.

charter provenance. Seven governing files byte-identical between ad715aca57… and abc4b83ce8…: 7 / 7 present-and-identical, against an unfiltered control diff of 38 files / +3508 / -148.

⭐ What this PR deliberately does NOT do, and why that is the right call

It 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 New inthe pin is, and with was the installedis the installed, each then recognised as a claim. That is what shows the refusal is the tense declining it and ⛔ not a technicality.

⚠️ Four holes are named rather than hidden: a false range claim outside the predicate by construction; a stale claim split across two lines, invisible to a per-line predicate; a Re-measured at spec 17.2.0 stamp shape none of the five markers cover; and the two self-expiring notes. Each is written into the gate header or its ledger entry.

Landing by squash through the merge queue.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant