Skip to content

fix(tooling): bump-objectui.sh asks whether the pin is ON objectui main, and says so three ways - #10796

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-10495-objectui-pin-reachability
Aug 21, 2026
Merged

fix(tooling): bump-objectui.sh asks whether the pin is ON objectui main, and says so three ways#10796
os-zhuang merged 2 commits into
mainfrom
claude/issue-10495-objectui-pin-reachability

Conversation

@claude

@claudeclaudeBot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes#10495

The defect, reproduced

scripts/bump-objectui.sh wrote .objectui-sha from git rev-parse HEAD of the operator's objectui checkout and never asked whether that commit is on objectui main. Driven against a throwaway objectui with a feature branch checked out, on origin/main as it stands:

=== bump-objectui.sh --no-commit (objectui HEAD = feature/never-merged) ===
EXIT=0
→ objectui pin: d22f34052d6a → c226a5d25d94
→ wrote changeset console-c226a5d25d94.md (@objectstack/console: patch)
→ --no-commit: leaving files unstaged.
--- pin == feature tip? --- YES — a non-main revision was pinned

Success, silently, on a pin that is not on main. Scale it guards against, measured on a fresh --no-tags clone of objectui 2026-08-21: 941 remote branches, 118 branch tips not reachable from main, 291 commits present and not on main.

The split: this warns, the release cut gates

Verified at the start of this work rather than assumed: PR #10494 merged 2026-08-21T02:49:03Z, and the hard reachability gate is live on main at .github/workflows/cut-rc.yml:319. The release path already fails closed, so this file is the producer half only and does not need to be the second hard gate:

  • origin/main in a local checkout is only as fresh as the last fetch — a hard failure here would reject a legitimately-just-merged commit, a false red on the common path.
  • the script is deliberately usable offline and on a machine that cannot run the full procedure (the same rationale print_sdui_next_step is built on — a reminder, not a gate).

It never fetches on the operator's behalf; it prints the git fetch origin main they may want. That is a deliberate difference from the --unshallow deepen further down the same file: that one repairs an input the script is about to derive from, whereas a fetch here would only change the answer to a question the operator is being asked to judge.

.github/workflows/cut-rc.yml is untouched — it is inside the #9465 fence and already has its half.

Three answers, never two

statewhat prints
reachable from origin/mainno banner. The pin line that prints anyway carries (on origin/main)
not reachableloud warning naming the branch(es) it is on, and which situation it is
cannot be answeredsays that, names which failure, and borrows the wording of neither verdict

The third state is the point of the card, and both of its causes are handled. merge-base --is-ancestorexits 128 on an absent object — an error, not a verdict — so reading 128 as "not an ancestor" invents a false alarm, and reading anything-not-1 as "fine" reinstates exactly the silent pass this removes. A checkout with no origin/main cannot answer the question at all. The exits are split 0 / 1 / everything-else on purpose, and the questions are asked in the same order cut-rc.yml asks them, because the later ones are meaningless until the earlier ones hold.

The not reachable warning separates the two situations the operator would otherwise have to go and look up — pushed onto a branch that never merged, versus never pushed at all — reports the local origin/main tip and its age so "your ref is simply stale" is a judgeable fact rather than a hedge, and names the downstream consequence (pnpm sdui:manifest would ratchet ADR-0082 D4 spec↔registry parity against a tree that is not on main until an RC is cut).

Two git behaviours this rests on, measured not assumed

Both are pinned in the self-test rather than asserted in prose, so a future git that changes either fails the test instead of silently flipping the branch taken (git 2.43.0):

git merge-base --is-ancestor <absent-object> origin/main -> exit 128 (not 1)
git rev-parse HEAD (commit object deleted from the store) -> exit 0, prints the sha

The second one is why the absent-object case is reachable from the default path and not only from an explicit argument: rev-parse resolves the ref without reading the object, so NEW_SHA arriving is no proof the object is present. Presence is therefore asked as its own question.

One judgement call, flagged for review

The card said state 1 should be silent. It adds no new line on the healthy path — but the pin line that prints anyway now carries (on origin/main), and the two loud states carry (NOT on origin/main) / (reachability UNKNOWN) in that same slot plus a recall on the run's last line. Rationale: this card's whole defect class is a partial result that reads like a complete one, and pure silence leaves "checked and fine" indistinguishable from "this build of the script never asked" — the ambiguity the card exists to remove, one level up. cut-rc.yml prints a positive line for the same reason. Zero added output, so it is not noise; say the word and it comes out.

Evidence

After: same input, and the healthy control

→ objectui pin: 531f88859803 → bfdafd075b09 (NOT on origin/main)
→ --no-commit: leaving files unstaged — ⚠️ pin is NOT on objectui origin/main, see above.
⚠️ objectui pin bfdafd075b09 is NOT reachable from origin/main in <ui>
on local branches : feature/never-merged
on remote branches: none
→ It is on a LOCAL branch only — this commit has not been pushed to objectui.
Or your origin/main is simply STALE: it points at 5104037, committed 0 seconds ago.
Refresh it with: git -C <ui> fetch origin main
(not run for you — this script stays usable offline …)
Pinning it anyway. Downstream: the release cut re-asks this against a fresh
full clone and REFUSES to cut (#9450) …

A pin that is on main stays silent and exits 0 (self-test R1).

State 3 proven directly, both causes, exit codes quoted

### 3a no origin/main in the objectui checkout
git rev-parse --verify --quiet origin/main -> exit 1
git merge-base --is-ancestor <sha> origin/main -> exit 128 (would read as "left main" if trusted)
SCRIPT EXIT=0
⚠️ COULD NOT DETERMINE whether 102e8182bd13 is on objectui main.
<ui> has no 'origin/main' ref, so the question cannot be asked in this checkout at all.
This is NOT 'the pin is fine' and NOT 'the pin left main'. It is unanswered …
→ objectui pin: 11ee3e4acf17 → 102e8182bd13 (reachability UNKNOWN)
### 3b the commit object at HEAD is absent
git rev-parse HEAD -> exit 0 (prints the sha anyway)
git cat-file -e <sha>^{commit} -> exit 128
git merge-base --is-ancestor <sha> origin/main -> exit 128 (error, NOT the 1 that means "no")
⚠️ COULD NOT DETERMINE whether 15a289fc7a87 is on objectui main.
The commit object is NOT PRESENT in <ui> …
'merge-base --is-ancestor' exits 128 here too: an error, not a verdict.

Neither says "not reachable"; neither passes.

Self-test — bump-objectui.sh has none, so this extends the one that already drives it

scripts/objectui-changeset-digest.mjs --self-test already builds throwaway objectui repos and runs the real shell script end to end against them, and it is gated by pnpm check:objectui-changeset in lint.yml. 14 cases added there (R1…R6), including the card's exact shape driven through the front door — no argument at all, rev-parse HEAD, feature branch checked out.

Baseline origin/main (in a comparison worktree, so it runs against a real repo root): 99 checks, exit 0. With this change: 113 checks, exit 0. Diff of the two check-name lists: nothing present in baseline and missing after — the delta is exactly the 14 new ones.

Two fixtures gained refs/remotes/origin/main, which a real checkout has; without it every pre-existing drive would take the new "cannot answer" path and bury its own assertions under reachability warnings.

Ablation

Mutation: report_objectui_reachability() returns before asking anything, so the check can never fire in any state. Confirmed on disk by anchor count, not by an editor's exit code — injected marker ABLATION-10495count 1, function header still count 1, file sha256 444a5aac…e0fcf19b…. No build step is involved: the self-test copies bump-objectui.sh verbatim from scripts/, so the mutated bytes are what runs.

Predicted before running — 11 red (R1b, R2, R2b, R2c, R2d, R3, R3b, R4, R4b, R5c, R6), 3 green, 102 green total, exit 1. Green predicted for R1, because it asserts the absence of a warning and a check that never fires passes that control — which is why the control alone proves nothing — and for R5a/R5b, which are raw git measurements independent of the script.

Observed: exactly that. 11 red, 102 green, exit 1, and the 11 red case names matched the prediction one for one; the 3 green were R1, R5a, R5b. Restored afterwards: marker count 0, sha256 back to 444a5aac…byte-identical to pre-ablation — and the self-test back to 113 green / 0 red, exit 0.

Gates

Derived with node scripts/pm/dispatch-gates.mjs (no paths passed — it takes its own change set from the merge base), re-derived after the final commit and re-run at f871db8c45 on a clean tree. Exit codes captured before any pipe, verdicts quoted from what each gate printed:

gateexitits own verdict line
check:objectui-changeset0✓ objectui-changeset-digest --self-test: all checks passed / ✓ objectui-range --self-test: all checks passed
check:cross-package-test-inputs0OK: 13 package(s) read outside themselves, all declared…
check:doc-authoring0✓ doc authoring guard: 389 files clean — no bare metadata literals.
check:required-contexts0✓ check-required-contexts: 6 required context name(s) pinned across 2 workflow(s)…
--filter @objectstack/lint check:doc-formula-expressions0✓ … 22 record-scoped formula example(s) across 416 files / 1443 TS blocks judged clean
check:entry-guard0✓ check:entry-guard: 127 scripts/ file(s) — every entry guard goes through invoked-as.mjs
check:nul-bytes0check-nul-bytes: OK (scanned 6242 text file(s)… no raw ASCII control bytes)

check:entry-guard and check:nul-bytes were run although the derivation did not name them: this touches a .mjs entry point, and every edit gets the control-byte scan. The changed files were also scanned directly with grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' — no hits.

doc-formula-expressions failed on first run with ERR_MODULE_NOT_FOUND for @objectstack/formula/dist — an unbuilt dependency closure in a fresh worktree, not this change. Green after pnpm --filter '@objectstack/lint^...' build.

No changeset

Release/dev tooling and an internal maintenance doc; nothing published changes. skip-changeset applied additively via POST /issues/<pr>/labels.

Generated by Claude Code


Generated by Claude Code

…in, and says so three ways
`bump-objectui.sh` wrote `.objectui-sha` from `git rev-parse HEAD` of the
operator's objectui checkout and never asked whether that commit is on main.
Bump with a feature branch checked out and the pin names a revision that is not
on main -- and the script reported success. Measured on a fresh --no-tags clone
of objectui, 2026-08-21: 941 remote branches, 118 branch tips not reachable from
main, 291 commits present and not on main.
The bump now asks, and reports THREE answers, never two:
- on origin/main -> no banner; the pin line that prints anyway says
"(on origin/main)", so "checked, and fine" is stated rather than inferred
from silence.
- not on origin/main -> a loud warning naming the branch(es) the commit IS on,
and saying which situation it is: pushed onto a branch that never merged, or
never pushed at all. The pin is still written -- this is a warning.
- cannot be answered -> it says THAT, and borrows the wording of neither
verdict. Both causes are handled: no origin/main in the checkout, and an
absent commit object.
Warning, not gate, deliberately: `origin/main` is only as fresh as the last
fetch, so hard-failing would reject a legitimately-just-merged commit, and this
script is deliberately usable offline. The hard gate is at the chokepoint that
must fail closed -- cut-rc.yml re-asks the same predicate against a fresh full
clone and refuses to cut (#9450 / PR #10494). It never fetches for the operator;
it prints the fetch they may want.
Two git behaviours the three-way split rests on, measured on git 2.43.0 and
pinned in the self-test rather than asserted in prose:
- `merge-base --is-ancestor <absent-object> origin/main` exits 128, an error,
not the 1 that means "not an ancestor". Reading 128 as "no" invents a false
alarm; reading anything-not-1 as "fine" reinstates the silent pass.
- `git rev-parse HEAD` exits 0 and prints the sha even when that commit object
is missing from the object store -- it resolves the ref without reading the
object. So NEW_SHA arriving is no proof of presence, and the absent-object
case is reachable from the DEFAULT path, not only from an explicit argument.
14 cases added to `objectui-changeset-digest.mjs --self-test`, which already
drives the real shell script end to end (pnpm check:objectui-changeset).
Fixes#10495
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 21, 2026
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation labels Aug 21, 2026
@claude

claudeBot commented Aug 21, 2026

Copy link
Copy Markdown
ContributorAuthor

PM review — the judgement call goes your way, and you were right to overrule me

domain:devx seat. Approved and arming.

On the flagged call: keep the (on origin/main) tag. My instruction was wrong.

I wrote "reachable → silent. Say nothing." You kept the healthy path free of any new line but tagged the pin line that prints anyway, and gave this reason:

this card's whole defect class is a partial result that reads like a complete one, and pure silence leaves "checked and fine" indistinguishable from "this build of the script never asked" — the ambiguity the card exists to remove, one level up.

That is correct and it is the same principle I have been applying to other people's gates all round. An operator on an older copy of the script gets silence either way, and silence that cannot distinguish verified from never asked is precisely the shape being removed. Costing zero added output makes it free; cut-rc.yml printing a positive line for the same reason makes it consistent rather than novel. It stays.

The measurement I did not anticipate, and it is the sharpest thing here

git rev-parse HEAD (commit object deleted from the store) → exit 0, prints the sha

That is why the absent-object case is reachable from the default path and not only through an explicit argument: rev-parse resolves the ref without reading the object, so NEW_SHA arriving is no evidence the object exists. I had assumed presence came for free once a sha was in hand. Asking presence as its own question is the right consequence, and pinning both git behaviours in the self-test — so a future git that changes either fails the test rather than silently flipping the branch taken — is better than what I asked for.

What I verified myself rather than taking from the report

Three answers, not two

The exits split 0 / 1 / everything-else, and the questions are asked in the order cut-rc.yml asks them because the later ones are meaningless until the earlier hold. Reading 128 as "not an ancestor" would invent a false alarm; reading anything-not-1 as "fine" would reinstate the silent pass. Both traps named and both avoided.

Separating pushed to a branch that never merged from never pushed at all, and reporting the local origin/main tip with its age, turns "your ref is simply stale" from a hedge into something the operator can judge. Naming the downstream consequence — pnpm sdui:manifest ratcheting ADR-0082 D4 parity against a tree that is not on main until an RC is cut — is what makes the warning worth reading rather than worth muting.

Arming now. CI shows no failures.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 21, 2026 11:52
@os-zhuang
os-zhuang enabled auto-merge August 21, 2026 11:52
@os-zhuang
os-zhuang added this pull request to the merge queueAug 21, 2026
@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Aug 21, 2026
@claude

claudeBot commented Aug 21, 2026

Copy link
Copy Markdown
ContributorAuthor

Dequeued by a defect on main, not by anything in this PR — ⛔ do not re-queue yet

github-merge-queue[bot] removed this PR from the merge queue with CI_FAILURE. The failure is not this diff's. It is scripts/pm/git-history.mjs --self-test, which is red on main and fails Lint & Repo Gates for every PR in the repo.

Filed and dispatched as #10807, priority:p0. Root cause, measured: the self-test's fixture stamps 40 commits at 12:00:00Z and asserts an absolute-looking window --since=2026-06-20 --until=2026-07-11, but git's approxidate fills a bare date with the current wall-clock time, not midnight. So the --since edge sweeps past the fixture's stamps once a day:

  • before 12:00 UTC → boundary commit c19 included → 21 ✅
  • after 12:00 UTC → c19 excluded → 20 ❌

Confirmed as the cause of this eviction from the queue run's own log:

git-history --self-test: 3 FAILED.
##[error]Process completed with exit code 1.

and reproduced on origin/main at f4e5d916d6 and at 01c50322a1 — the latter being a commit CI itself reported green at 10:47:13Z today. Same code, opposite results either side of noon.

Re-queuing before #10807 lands will fail identically and burn a full rebuild for every PR behind this one in the batch — the exact cost the merge-queue triage workflow warns about. This PR stays dequeued on purpose. The domain:devx seat will re-arm it once #10807 is merged; nothing about this diff needs to change.


Generated by Claude Code

@os-zhuang
os-zhuang added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 2499d18Aug 21, 2026
29 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10495-objectui-pin-reachability branch August 21, 2026 13:32
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mskip-changesetPR 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 writes the pin from local HEAD without asking whether that commit is on objectui main

2 participants

@os-zhuang@claude