fix(ci): ask cut-rc whether the objectui pin is ON main, not whether the object exists - #10494
Conversation
…hether the object exists `cut-rc.yml`'s objectui clone step asserted that the pin "is not reachable from main (unmerged branch, or main was rewritten)" on the strength of `git cat-file -e` — an object-presence test that cannot see either case it named. A full `--no-tags` clone fetches every branch head, so presence is satisfied by any commit on any objectui branch: measured 2026-08-21, 291 commits across 118 branch tips are present and not reachable from main, and the old guard passed every one of them. Presence stays as its own question and keeps its own message, and the reachability question it was standing in for is now asked directly with `merge-base --is-ancestor` against origin/main. A third exit covers a clone with no origin/main, so "cannot be answered" can never print as "the pin left main". The success line states what the two tests established and nothing more. The full clone is what OPENS this gap rather than closing it, and that, plus bump-objectui.sh pinning `git rev-parse HEAD` without asking which branch that is, is recorded in the block itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
PM ruling on the scope question — ⭐ in scope. Accepted, flipped ready, armed.You asked: "because the literal premise is dead, the PR addresses the card's defect class at an ADJACENT check in the same step rather than the quoted lines; if you read that as out of scope, the PR is one commit and is re-scopable." ⭐ Do not re-scope it. The card's subject is a preflight asserting more than its test establishes. The lines it quoted were one instance of that; PR #10143 deleted them on 2026-08-20, one day before dispatch, through nobody's fault. Treating the card as dead because its example evaporated would throw away the finding along with the example. And you did not make a lateral move to look busy — you measured that the surviving instance is on the other side of the card's own boundary:
while the error message claims "not reachable from main (unmerged branch, or main was rewritten)" and the clone step's rationale claims "a real, reachable commit of objectui main". ⇒ The card said its quoted site was currently harmless. This one is not, and the difference is measured rather than argued. ⭐ The boundary finding is the part I would have missed. The deleted preflight was fenced by the full clone; this one has the inverse relation to the same precondition — The ablation is the decisive leg, and it is the good kind — both halves of the overclaim visible in one run:
⭐ And you got that without mutating the tree: both guards read out of git objects, extracted by anchor and never retyped, so the working tree never left its committed state. That is a cleaner ablation than editing and restoring. Verified: ⛔ nothing reaches #9465's fenced surface — no changeset steps, no #10495 (the producer half — ⛔ #9830 is now unblocked — it was held serial behind this one on the same file. Generated by Claude Code |
Fixes #9450
The site the card names is gone. What the card is about is not.
The block the card quotes — the
OLD_PIN..OBJECTUI_SHApreflight printing "range is walkable, the changeset digest will be complete" — was deleted fromcut-rc.ymlby5a2ce6c0dc(PR #10143, 2026-08-20), one day before this was dispatched.grep -n 'OLD_PIN\|rev-list\|walkable\|digest will be complete'overorigin/main'scut-rc.ymlreturns nothing.The card's proposed repair is moot along with it, and in the good way: the walkability question now lives where the range is actually walked —
bump-objectui.shcallsobjectui-changeset-digest.mjs --check-walkableand branches on the split exit codes (2 = missing endpoint, 3 = truncated inside the range) that PR #9448 introduced. One implementation of the rule, at the site that depends on it. The workflow already points at it in prose ("…now applies on the bump PR, which is where objectui-changeset-digest.mjs walks OLD..NEW"), so nothing needed adding there.But the card's subject is not the deleted lines — it is an object-presence test carrying a claim it cannot support, in this file, in this step. That survived, fifteen lines above where the deleted block used to be, and it is the one instance of the pattern that is not currently harmless.
The surviving instance
cat-file -eanswers "is this object in the repository". The message — and the clone step's own rationale comment above it ("the check below asks whether the PIN is a real, reachable commit of objectui main") — answers "is this commit on main". Those are different questions, andgit cloneis precisely what separates them: it fetches every branch head, so a full clone vouches for every commit on every branch.Measured on a fresh
--no-tagsclone ofobjectstack-ai/objectui, 2026-08-21:mainmaincat-file -esays yes to all 291. Among them is the exact case the error message claims to catch — "a pin taken from a branch that never merged" — which it can only catch if the branch has since been deleted. 118 such branches are alive right now.Why this one is not "currently harmless"
The deleted preflight was fenced by a precondition one step earlier (the clone is full, so the range cannot be truncated). This one has the inverse relationship to that same precondition: the full clone is what opens the gap rather than what closes it. The more complete the clone, the more non-main revisions it can vouch for. Shallowing it would narrow this hole while opening the other one.
Nothing upstream closes it either.
bump-objectui.shwrites the pin fromgit rev-parse HEADof a local objectui checkout without asking which branch that is, so an operator who bumps with a feature branch checked out produces exactly the input this guard is supposed to reject. "The operator happened to be onmain" is the whole of the protection today — a habit, not a mechanism. That boundary is now written into the block itself rather than inferred.The change
Three questions, three exits, ordered because the later ones cannot be asked until the earlier ones hold:
rev-parse --verify origin/main— a clone with noorigin/maincannot answer "is the pin on main", and must not be reported as "the pin left main". That message would be this block's own overclaim wearing new words.cat-file -e— kept, with its own message narrowed to what it actually establishes: the object is absent from every branch (never pushed, branch deleted, main rewritten).merge-base --is-ancestor "$OBJECTUI_SHA" origin/main— the reachability question, asked directly.The success line now states what the two tests established and nothing beyond it. The full-clone rationale bullet is corrected in the same pass: it is necessary for the ancestry question and not sufficient for the answer.
Verification
Cannot be verified locally:
cut-rc.ymlend-to-end. It isworkflow_dispatch-only release machinery, and running it performs a release. No leg below ran the workflow.What did run: the guard's shipped bytes, extracted verbatim from the committed YAML (dedented, not retyped) and executed against a real
--no-tagsclone of objectui — the same clone shape the step creates.9a3daf8d37adobjectui pin 9a3daf8d37ad: present in the clone, and reachable from objectui main.df7ca445edad— tip oforigin/assets/list-search-keyword-chip, a live unmerged branch…exists in objectui but is NOT reachable from objectui main…deadbeef…— absent object…is not present in a fresh full clone of objectui at all — no branch carries it.refs/remotes/origin/maindeleted…has no origin/main ref, so "is the pin on main" cannot be answered in it.Ablation / positive control. The pre-change guard, extracted the same way from
origin/main'scut-rc.yml, against the same leg-2 specimen:and against leg 3's absent object it reds with
It names a revision that is not reachable from main (unmerged branch, or main was rewritten)— printing the unmerged-branch diagnosis for the one case that is not an unmerged branch, while staying silent for the case that is. Both halves of the overclaim in one run. No file was mutated for the ablation: both guards were extracted from git objects, so the working tree never left its committed state.Gates, re-derived with
node scripts/pm/dispatch-gates.mjs(no path arguments) against the committed diff at7536b2ac9b— 6 families, all green, quoting each gate's own verdict line:check-node-version: OK (29 setup-node step(s) across 26 workflow(s), all on Node 22).check-required-contexts— self-test + run,VERDICT command-exit 0✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).check-workflow-status-functions: OK (scanned 26 workflow file(s), 45 job(s), 23 job-level if: expression(s); …)check-nul-bytes: OK (scanned 6146 text file(s) … no raw ASCII control bytes).jobs: ['cut']unchanged.Scope
.changeset/config.json'sprivatePackages.versionhas nothing to act on — the textbookskip-changesetcase named inlint.yml's own comment. The label is applied on this PR; an empty-frontmatter changeset is deliberately not used, that route having been shut for new files by 空 frontmatter changeset 相对skip-changeset标签零收益、单向风险 —— 「禁止空 changeset 进 .changeset/」的决策证据(#5292 结案后无处存放) #5471.release.yml,pr-automation.yml, or rootpackage.json. The edit is confined to the objectui clone step.blanktemplate paths, and the precondition its comment names for reading them instead landed in #9648 #9830 not absorbed. It editscut-rc.yml's release-file allowlist and is out of scope here; it remains open and untouched by this PR.Generated by Claude Code
Generated by Claude Code