Skip to content

fix(devx): refuse instead of degrading when a walkable range's changeset blob is unreadable - #14440

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-14393-bump-walkable-digest-refuse
Sep 2, 2026
Merged

fix(devx): refuse instead of degrading when a walkable range's changeset blob is unreadable#14440
baozhoutao merged 4 commits into
mainfrom
claude/issue-14393-bump-walkable-digest-refuse

Conversation

@baozhoutao

@baozhoutao baozhoutao commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #14393

What changed

bump-objectui.sh reached its degraded emitter (BUMP="${CONSOLE_BUMP:-patch}")
in two states after #14178: the initial pin (no previous SHA — stays permitted,
unchanged here) and a range that walks completely but whose changeset
derivation (objectui-changeset-digest.mjs) throws anyway — the reachable
trigger is a changeset blob unreadable at both to and the commit that added
it. State 2 declared a bump level (patch) nobody actually declared, published
into @objectstack/console's CHANGELOG and the curated release notes, exactly
the shape the #14178 triage ruling forbade ("a patch that reads like a
declaration and silently skips the ADR-0087 disposition prompt is worse than no
record"). This PR makes state 2 refuse instead, matching the #14178
disposition for the unwalkable-range case — RANGE_OK is the discriminator: a
walkable range means a level was derivable in principle, so a failed
derivation there is an error to surface, not a guess to publish.

Placement, per the card's zone 2 item 2

The changeset derivation (node .../objectui-changeset-digest.mjs --out "$CS_FILE") used to run after the .objectui-sha pin write, so a refusal
there alone would have left the pin moved with no record — the exact
half-applied state #10797 exists to prevent, one input further in. This PR
moves the derivation call above the pin write and refuses there on failure.

It writes straight into CS_FILE rather than a temp path moved into place
afterward: reading objectui-changeset-digest.mjs's main(), the --out file
is only written (writeFileSync(out, file)) once the whole digest has built
successfully — a readAt failure throws inside buildDigest, before that
write is ever reached, so a failing derivation leaves CS_FILE unwritten on
its own. There is no partial-write case left for a temp file to guard against
(confirmed by inspection of objectui-changeset-digest.mjs:763-788 and
:1184-1245 — read-only per the card's file-surface constraint, not modified).

Reproduction (the deliverable per zone 1 / zone 3)

Built a throwaway objectui repo: commit A (previous pin), commit B adding
a .changeset/widget-refresh.md file declaring "@object-ui/core": minor. The range A..B
walks completely (--check-walkable exits 0). Deleted only the changeset
blob object added in B (not the commit) and re-confirmed the walk still
answers 0 — the walk is over commits/trees, --check-walkable never touches
blobs.

Before (unmodified scripts/bump-objectui.sh at origin/main 96b627d13),
bump-objectui.sh --no-commit (with commit B as the argument):

✓ objectui-changeset-digest: 51903a2f9b7f..35367ff71983 walks completely in .../objectui.
→ objectui pin: 51903a2f9b7f → 35367ff71983 (on origin/main)
✗ objectui-changeset-digest: cannot read .changeset/widget-refresh.md — neither at `to` nor at the commit that added it.
  at `to` (35367ff71983...):
    fatal: bad object 35367ff71983...:.changeset/widget-refresh.md
  at the commit that added it (35367ff71983...):
    fatal: bad object 35367ff71983...:.changeset/widget-refresh.md
[node stack trace / Error: Command failed — exit 128]
→ wrote changeset console-35367ff71983.md (@objectstack/console: patch)
→ --no-commit: leaving files unstaged.

Exit 0. .objectui-sha moved. A changeset landed declaring patch with a
"Degraded list" body — exactly the silent-patch defect this card describes.

After (this PR's scripts/bump-objectui.sh), same fixture, same command:

✓ objectui-changeset-digest: 71d868c3b3e9..c3c844b4a73d walks completely in .../objectui.
✗ objectui-changeset-digest: cannot read .changeset/widget-refresh.md — neither at `to` nor at the commit that added it.
  [same two git diagnostics]
✗ REFUSING to bump: the objectui range 71d868c3b3e9...c3c844b4a73d walks
  completely in .../objectui, but deriving the @objectstack/console changeset
  from it failed — its diagnostic is in this run's output, above.
  A level was derivable in principle here; the derivation just failed reading a
  changeset blob. This bump used to emit a degraded entry carrying the default
  level instead (`patch`) as if it were a declaration nobody made, publishing
  into @objectstack/console's CHANGELOG and the curated release notes. No record
  beats a wrong one (objectstack#14178 triage ruling), so this refuses instead.
  NOTHING WAS WRITTEN — .objectui-sha is untouched and still holds the old pin.
  Repair the objectui object store and re-run this bump, or move the pin without
  a release record at all: scripts/bump-objectui.sh --no-changeset
  (the pin moves, nothing is derived, and nothing is claimed about the range).

Exit 1. .objectui-sha byte-identical to before the run. No changeset
file written.

This exact fixture is case_5 in scripts/bump-objectui.selftest.sh (new),
mirroring the same ok/bad idiom as cases 1-4. It also asserts the fixture
is real — the blob is confirmed gone (git cat-file -e fails) and
--check-walkable is re-asserted green on the broken tree before the bump is
even run, so the case cannot pass over a fixture that never reached this state.

Ablation

Reverted only the refusal commit (git checkout pointed at the fixture-only
commit, for the path scripts/bump-objectui.sh, restoring the pre-fix bytes
while the fixture commit stayed), reran bash scripts/bump-objectui.selftest.sh:

✗ expected a non-zero exit, got 0 — the bump did not refuse
✗ .objectui-sha CHANGED — half-applied state: (the new sha)
✗ no 'REFUSING to bump' in the output; got: → wrote changeset console-....md (@objectstack/console: patch)|...
✓ the refusal says the range WALKS (the discriminator from the unwalkable-range refusal)
✗ the refusal does not tell the operator the tree is clean
✗ a changeset was emitted for a range whose derivation failed

✗ bump-objectui self-test FAILED — 5 assertion(s) failed, 15 passed.

Restored (git checkout HEAD -- scripts/bump-objectui.sh, safe — the fix was
already committed at that point) → bash scripts/bump-objectui.selftest.sh
✓ bump-objectui self-test PASSED — 20 assertions across 5 cases.

check:declared-population-live

Adding case_5 initially introduced ONE bare quoted path literal
(.changeset/widget-refresh.md inside the break_changeset_blob call), which
check:declared-population-live correctly read as a declared population for
the check:objectui-bump family that reaches nothing in this tree (it names a
path inside a disposable mktemp -d checkout, never a tracked file). Fixed by
building the path from a CHANGESET_NAME variable via interpolation
everywhere it's used instead of spelling it as one bare token, plus a
dispatch-gates: no-path-population marker documenting why. Verified with a
direct extractWatchHints call — zero hints now, and check:declared-population-live
is green (157 of 201 families declare a population, all reaching the tree).

Header / self-test-pointer comment sync

Per zone 2 item 5: the header's refusal-states description (bump-objectui.sh:17-52)
and the self-test pointer comment (:90-105) are updated to describe the new
refusal and that case 5 needs node (unlike cases 1-4, which stay offline/no-node).

Changeset

None added. scripts/bump-objectui.sh and scripts/bump-objectui.selftest.sh
publish nothing from any package. Following this repo's own convention for
scripts/**-only changes: PR #14391 (c0eed5648, the direct #14178 precedent —
same two-file surface, a considerably larger change) shipped with no
.changeset/*.md file. This PR's file surface is outside the closed list
os-dev.md lets a dev self-apply skip-changeset for (scripts/pm/**, not
scripts/bump-objectui*), so the label — needed for Check Changeset to pass
— is the PM's call per the card and os-dev.md.

Gates

All commands node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands scripts/bump-objectui.sh scripts/bump-objectui.selftest.sh derived
(16 total), run under scripts/pm/os-verify-lock.sh, at final head 85fd2d22e:

command result
node scripts/check-ci-filter-parity.mjs RAN-PASS
node scripts/check-cross-package-test-inputs.mjs RAN-PASS
node scripts/check-shard-attestation.mjs RAN-PASS
node scripts/check-test-completeness.mjs NOT MEASURED — PREREQUISITE NOT MET (grades a saved turbo run test log; CI tees it, this branch is unreachable there)
node scripts/pm/bare-root-worklist.mjs --self-test RAN-PASS
pnpm check:agent-test-spelling RAN-PASS
pnpm check:bash32-floor RAN-PASS (153 self-test cases; 26 tracked shell files, 0 findings)
pnpm check:cli-command-ids RAN-PASS
pnpm check:cross-package-test-inputs RAN-PASS
pnpm check:entry-guard RAN-PASS
pnpm check:objectui-bump RAN-PASS — 20 assertions across 5 cases
pnpm check:objectui-changeset RAN-PASS — digest + range self-tests green (READ-ONLY file, unmodified)
pnpm check:parse-guard RAN-PASS
pnpm check:pm-dispatch-gates RAN-PASS — dispatch-gates self-test: 1232 cases pass (held the verify-lock 780s/13m under shared-box contention)
pnpm check:pnpm-filter-targets RAN-PASS
pnpm check:watch-hint-literal RAN-PASS

Also run per zone 3 (not in the derived 16, explicitly named in the dispatch):
pnpm check:ratchet-remedy-authority — RAN-PASS; pnpm check:declared-population-live
— red once (see above), fixed, RAN-PASS after fix. shellcheck on both bash
files: not run — the binary is absent from this container and no workflow
under .github/ runs shellcheck.

All 16 derived local gates are green (check:test-completeness NOT
MEASURED — prerequisite is a CI-only turbo test log). node scripts/check-nul-bytes.mjs
also green.

Generated by Claude Code

…ob is unreadable

Builds a throwaway objectui repo with a real changeset commit, deletes only
the changeset's blob object (not the commit), and asserts --check-walkable
still exits 0 on the broken tree before driving bump-objectui.sh through it.
Red against the unmodified script: it still emits a degraded patch changeset
and exits 0. Adds the objectui-changeset-digest.mjs + invoked-as.mjs copies
the case needs, mirroring the digest script's own self-test fixtures.
…set blob is unreadable

RANGE_OK=1 but objectui-changeset-digest.mjs still fails (a changeset blob
unreadable at both `to` and the commit that added it) used to fall into the
same degraded-patch path as the initial pin, publishing a level nobody
declared into @objectstack/console's CHANGELOG. The range walked, so a level
WAS derivable in principle -- this state now refuses, matching the #14178
disposition for the unwalkable-range case.

Moves the changeset derivation above the pin write so a refused run leaves
.objectui-sha byte-identical and no changeset file, preserving the #10797
write-ordering invariant one input further in: the digest CLI only writes
its --out file once the whole digest has built successfully, so a failing
derivation leaves CS_FILE unwritten on its own -- no temp-file indirection
needed. The initial pin (no previous SHA, no range) is unaffected and keeps
its degraded entry.

Updates the header's refusal-states description and the self-test pointer
comment to describe the new case.
… literal

check:declared-population-live read the quoted '.changeset/widget-refresh.md'
in break_changeset_blob's call as a declared population for check:objectui-bump
-- the whole family's only path-shaped literal, and it reaches no tracked file
since it names a path inside a disposable throwaway checkout. Builds the path
from CHANGESET_NAME by interpolation everywhere it is used instead, and adds
a no-path-population marker documenting why (case 5's fixture paths live only
under mktemp -d).
@github-actions github-actions Bot added the size/m label Sep 2, 2026
The block above the pin write no longer opens with the first working-tree
mutation on the success path -- the digest's --out write (CS_FILE) now lands
first, the pin second, since the changeset derivation moved above the pin
write. Reword the comment to state what actually holds: the digest only
writes CS_FILE once the whole digest has built, so a refusal never leaves it
behind; the invariant #10797 needs is that no read of objectui happens below
this point, not that this line is literally the first write.

No behaviour change.

Copy link
Copy Markdown
Contributor Author

Reworded the pin-write comment per review: it no longer claims to be "the first mutation" (the digest's --out write to CS_FILE is first on the success path now, the pin second — both land in that order or neither does, since the digest only writes once the whole digest has built). States the actual invariant instead: no read of objectui (git or the digest) happens below this point. Comment-only, no behaviour change.

bash scripts/bump-objectui.selftest.sh — exit 0, 20 assertions across 5 cases, PASSED.
pnpm check:declared-population-live — exit 0, green (157/201 families).

New head: d68f71657

Generated by Claude Code


Generated by Claude Code

@baozhoutao baozhoutao added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026 — with Claude
@baozhoutao
baozhoutao marked this pull request as ready for review September 2, 2026 08:12
@baozhoutao
baozhoutao enabled auto-merge September 2, 2026 08:12
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 2, 2026
Merged via the queue into main with commit d99832a Sep 2, 2026
35 of 36 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-14393-bump-walkable-digest-refuse branch September 2, 2026 08:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bump-objectui.sh still emits a default-level patch changeset when the digest fails on a WALKABLE range

2 participants