Repository navigation
fix(scripts): refuse to record the sdui lockstep from a tree the pin does not name - #17268
Merged
Merged
Conversation
…does not name `gen:sdui-lockstep` read the record's content from whatever the objectui checkout had at HEAD while stamping `recordedAgainstPin` from `.objectui-sha`. Nothing joined those two, so a checkout off the pin wrote a record that named the pin while describing a different tree — silently, at exit 0. `judge()`'s `pin-moved` clause cannot reach that: it compares the stamped side against the pin file, two values from the same file that agree by construction. The join has to happen in the generator, the only moment both the tree and the pin are in hand. `judgeRecordability` is that join, as a pure function so `--self-test` can pin both directions: on the pin it records as before, off the pin it refuses and names both revisions plus the two ways to move. `recordedAgainstPin` is now stamped from the value the refusal compared, so what is checked and what is stamped cannot drift apart again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
… own body
The self-test's wiring cases scanned the WHOLE FILE for
`judgeRecordability({ head:`, which matched the battery's own test code. An
ablation proved the consequence: deleting the generator's call to the judge left
the self-test GREEN at exit 0 — the assertion was pinning itself, and the
battery it claimed to protect was a phantom check.
Scoped to a slice of `update()`'s own body, with the slice bounds asserted so a
rename cannot make the case vacuously green instead. The stamp case is scoped
the same way, for the same reason.
Also stops the off-pin case from crashing when the guard is ablated: reading
`[0].includes(...)` off an empty array threw a TypeError where a named failure
belongs, so the self-test redded for the right reason with the wrong words.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
baozhoutao
marked this pull request as ready for review
September 10, 2026 02:00
baozhoutao
enabled auto-merge
September 10, 2026 02:00
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #16797
gen:sdui-lockstepread the record's content from whatever the objectui checkout had at HEAD, while stampingrecordedAgainstPinfrom.objectui-sha. 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.judge()'spin-movedclause cannot reach that, which is the whole point: it comparesrecord.recordedAgainstPinagainst the live pin — 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, and the one check comparing anything compared the stamp. The join has to happen in the generator, the only moment both the tree and the pin are in hand.Route taken: 2 (refuse loudly), and why
The card left the direction open between (1) resolving the objectui checkout to the pin before reading and (2) refusing when
HEAD != .objectui-sha. This PR takes route 2. Three reasons, in the order they decided it:5580159689records that a pre-generator equality assertion was already field-proven against this exact seam during chore(console): bump the objectui pin to 53ded82bf7a4 — the shipped console lowers array analytics filters #16788's second regeneration — run by hand, and it is what made a correct record safe rather than lucky. This PR lifts that assertion into the generator so it protects every future bump instead of depending on the operator remembering. That the same operator, holding this defect in mind, still had to catch it a second time within the hour is this card's own strongest argument that a convention is not enough.--updatenow asserts the same way before any record.⛔ The forbidden repair (stamping HEAD into
recordedAgainstPin) is not taken and the field is unchanged: it would make the field honest and useless, and it would makejudge()'s comparison pass forever, destroying a working check along with the record.What changed
One file,
scripts/check-sdui-lockstep.mjs(+192 / -6), across two commits:judgeRecordability({ head, livePin })— a new exported pure function, so--self-testcan pin both directions offline. Empty array means recordable; otherwise[head-off-pin],[pin-unusable]or[head-unusable], injudge()'s finding shape so both are read the same way. Absence and malformation are distinguished from being off the pin: a pin that cannot be compared must not read as a checkout on the wrong commit.update()consults it first, before anything is read or written — ahead of the dirty-tree, delimiter and code-set refusals, because on the wrong revision every later reading is a reading of the wrong tree and a complaint about it is noise. The refusal names both revisions and both remedies (park the checkout on the pin, or move the pin first).--updateused to read.objectui-shainline at the stamp with no handling; an unreadable pin threw a raw stack. It is now read once, with the same refusal shapemain()already uses, andrecordedAgainstPinis stamped from that same value — so what is CHECKED and what is STAMPED cannot drift apart again.Acceptance — both legs, measured at
3d38cc7883Against a real objectui clone. Pin is
53ded82bf7a4; today's objectui tip is4fa0eb9c4ad2.⭐ Leg 2, the REVERSE control — objectui parked ON the pin. Still produces output, and the output is byte-identical:
Leg 1, the firing leg — objectui parked on a non-pin commit. Refuses, and writes nothing:
Positive control for leg 1 — the same corpus, the UNFIXED script.
origin/main's version of the file, run against the same non-pin checkout, so the zero above is a reading and not a silence:That is the defect reproduced a third time, on a third independent objectui revision (the card measured
d65b2baa, #16788's sync measuredf76f4362, this is4fa0eb9c), and it is the plain default state of a freshgit cloneinto the sibling pathresolveObjectuilooks at. Restored afterwards bygit checkout HEAD -- ..., verified by state: blob back tobc425df3,git diff HEADempty.The
parse.tstrap, re-measured on today's pair — so the legs above are not the leg the card warned against:The grammar-region blob is
0131f27cf86din both the good and the bad record, which is exactly why a leg comparingparse.tsor the region alone would have seen nothing. The 25-vs-24 difference lives in the tree, and the legs above compare the whole generated record.Ablation — the new cases can actually fail
Each mutation was proved on disk (anchor grep count before/after, blob hash vs the HEAD blob) and restored under a
trap, verified bygit diff HEADbeing empty and the blob returning to06cc0483:head !== livePinbranch disableda checkout one commit off the pin is refused, not recordedupdate()stops calling the judgethe generator CALLS the recordability judge, so that battery is not phantomit stamps recordedAgainstPin from the value it comparedjudgeRecordability({ head:— which matched its own battery's test code, so deleting the generator's call left the self-test green at exit 0. The assertion was pinning itself. It is now scoped toupdate()'s own body slice, with the slice bounds asserted so a rename cannot make it vacuous, and the comment at the site names the measurement. Every "is it wired" assertion in a self-testing script has this shape; only the ablation shows it.Gates
node scripts/pm/dispatch-gates.mjs --commandsderived 32 families from the actual change set; all 32 run, all exit 0. Reconciled:dispatch-gates --ranreports32 derived, 32 run, 0 NOT-MEASURED, 0 UNRUN.pnpm check:sdui-lockstep(self-test plus gate) exits 0 at the final commit.--self-testin the fresh worktree returned exit 3 —ts-parse: PREREQUISITE NOT MET,typescriptnot installed. It says nothing about the tree. Re-run afterpnpm installand it is a real exit 0.Lint — a declared narrowing, with its three readings.
pnpm lintis a repo-wide scan CI owns; run here narrowed to the one changed file, with the narrowing proven rather than assumed:eslint --print-config, not guessed): exactly 2 rules are enabled for this path —no-restricted-importsandcomment-swallow/no-code-inside-block-comment.--format json: 1 file linted, 0 errors, 0 warnings, exit 0.eslint.config.mjsand it never enables type-aware linting for any file (noparserOptions.project, no typed rules) — stated and independently measured ateslint.config.mjs:327. A one-file diff therefore cannot move the verdict on any file it does not touch.Positive control for that zero, same corpus and same invocation: a restricted import planted in a sibling
scripts/*.mjsfile gives exit 1, 1 error, ruleno-restricted-imports. Removed; tree clean.Changeset: graded
skip-changeset, measuredNothing publishable moves. Measured rather than asserted: of 24 workspace packages, 0 have a
files[]that would ship the repo'sscripts/directory, and no package reaches it throughbinorexports; the root package@objectstack/spec-monorepoisprivate: true. Positive control on the same scan: 23 of those 24 packages do ship adistpath, so the scan readsfiles[]and a zero from it is a reading.验收备注
noted, not filed— withjudgeRecordabilityin place,record.objectui.revandrecord.recordedAgainstPinare now equal by construction at record time, sojudge()could cheaply re-assert that equality offline and catch a self-certifying record produced by the old generator. Not filed and not done here: no such record exists (leg 2 proves the shipped record regenerates byte-for-byte at the pin), so there is no defect to reproduce — it is defence in depth, not class (a), (b) or (c). It also adds verification surface, which is what this card's reverse leg warns against. Whoever next bumps the pin is the party who touches this file; they hit nothing, because the generator now refuses first.resolveObjectuifalls back to (../objectui) puts a fresh clone at objectui tip, i.e. exactly the trap condition, with no step in between. That is now caught rather than recorded; noted because it explains why this fired twice in one hour rather than being exotic.⛔ #16715 is not addressed here — adjacent gate, different failure.
A note on this branch's history
The second commit exists because the wiring fix the ablation forced was made after the first commit was already pushed. It was stacked rather than amended in: a force-push is not available to this seat, and rewriting a pushed branch is not worth hiding the fact that the ablation caught something. Every measurement in this body was re-run at the pushed head
3d38cc7883, whose tree is byte-identical to the tree those measurements were first taken against (git diffbetween them is empty):check:sdui-lockstepexit 0,dispatch-gates --ranstill32 derived, 32 run, 0 NOT-MEASURED, 0 UNRUN, leg 2 byte-identical, leg 1 exit 1.🤖 Generated with Claude Code
https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
Generated by Claude Code