Skip to content

fix(devx): the objectui pin guard tests walk completeness, not object presence - #9448

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-9408-shallow-walk-guard
Aug 18, 2026
Merged

fix(devx): the objectui pin guard tests walk completeness, not object presence#9448
os-steve merged 1 commit into
mainfrom
claude/issue-9408-shallow-walk-guard

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes#9408

The card is right about the mechanism and I reproduced it byte-for-byte before changing anything. Two of its four suggested repairs are implemented, one is subsumed, and one is not implemented because I measured it and it does not work — details below.

Reproduced first, on two specimens

This container is still a live specimen — the PM triage note expected it not to be. /home/user/objectui is shallow right now, boundary 75444e38a, inside the pin range:

$ git -C /home/user/objectui rev-parse --is-shallow-repository -> true
$ cat .git/shallow -> 75444e38a07e...
$ git rev-list --count --no-merges 665661ab0932..82a94170c405 -> 137 (truth: 191)

Running the unmodified digest against it credited 12 entries to 75444e38a and exited 0 — PR #9398's competing derivation, regenerated. Grafting a second copy at 183d09b78 (the card's own recipe) reproduced the landed record exactly: 36 entries credited to the sink, 110 non-merge commits, and the entry list byte-identical to the pre-correction artifact at 83fe945d6 (119 lines, diff clean). The index arithmetic holds independently: 183d09b78 at index 110, 75444e38a at index 137.

Not implemented: the arithmetic check, because it cannot fire

The card's fourth suggestion — no commit may be credited with more added changesets than git show --name-only attributes to it — is described as catching all three bad runs. Measured against both runs I can reproduce, it catches neither:

specimencredited to sinkgit show --name-only attributesfires?
boundary 183d09b78 (landed record)3672no
boundary 75444e38a (this container)1247no

It cannot fire on any history. git show --name-only on the sink runs the same empty-tree diff that produced the fault, and the digest's credited set is that attributed set minus the seen dedup — a subset by construction. The comparison is a tautology: no false positives because no positives at all. Pinned as such in the self-test (C12) so it is not reintroduced later as a "cheap backstop".

What replaces it: one true check

findRangeTruncation tests a parentless commit inside the range. That is the harm itself — the sink is the parentless commit — so it catches a --depth clone, a hand-written .git/shallow, a graft, a git replace, and unrelated histories without naming any of them. It is also strictly more precise than asking whether the clone is shallow: a shallow clone whose boundary sits at or before from walks the range completely, and refusing it would be a false positive. In an untruncated history the range cannot contain a parentless commit at all, so a hit is always real.

It lives in classifyRange, the single shared implementation, so objectui-range.mjs cannot derive from a truncated walk either (it already wraps that call in a loud die()).

Deepen first — measured, not assumed

git fetch --unshallow on objectui costs 6 seconds and ~4 MB, and takes the walk from 110 commits to the true 191. A degraded console changeset becomes permanent published CHANGELOG text, so repairing the input beats labelling a truncated derivation of it. The fetch is additive (adds objects, drops .git/shallow; moves no branch, touches no working tree), announced before and after, and opt-out-able with OBJECTUI_NO_DEEPEN=1.

One measured trap it handles: git fetch --unshallow in a checkout with no remote configured exits 0 and changes nothing. The range is therefore re-checked afterwards rather than the fetch status trusted — otherwise this card's failure would simply reappear one layer further in. Pinned in C17/C18.

The other two PM assumptions, tested

  • The degraded path is genuinely honest. Confirmed, unchanged: it emits ⚠️ **Degraded list**, "NOT a complete account of the range", and the tip subject only. I extended the reason only — a truncated range must also be distinguishable from an absent endpoint, because the remedies differ and only one is a fetch away.
  • to-endpoint validation is missing. Partly falsified. The digest CLI already validated both endpoints (exit 2, verified). The gap was in bump-objectui.sh's own RANGE_OK, which now goes through --check-walkable and covers both.

Tests

18 new checks (C1–C18) in objectui-changeset-digest.mjs --self-test. The truncation is synthesized by writing .git/shallow, so the complete and truncated fixtures are the same commits differing in exactly one file — each assertion is about the graft, not about two repos that differ somehow.

Both directions are pinned, per the explicit ask that a refusal-only test would pass on a guard that refuses everything: C1/C2 keep the complete walk, and C7 is the sharper control — a tree that is shallow with its boundary outside the range must still derive identically (stillWhole.body === whole.body). C7 is exactly the false positive an --is-shallow-repository guard would produce.

Reverse verification, at commit 68615c550: ablating both refusal call sites turns 8 checks red (C8, C9, C10, C13, C14, C15, C16, C17) and the ablated build emits the defect in miniature — 5 releasing of 5 changesets added across 3 non-merge commits with entries 1, 2 and 3 all credited to one sink. C3/C4/C5/C12 stay green by design: they measure the defect mechanism, not the guard. Restored with git checkout HEAD --, byte-identical, green.

Gates run against 68615c550 (working tree clean, equal to head):

pnpm check:objectui-changeset OK (120 checks; digest + objectui-range self-tests)
pnpm check:nul-bytes OK (6122 files)
pnpm check:objectui-pin-fresh --self-test OK

Gate family derived from the changed paths with node scripts/pm/dispatch-gates.mjs, which names check:objectui-changeset alone; check:nul-bytes and the pin-fresh consumer were added because the diff touches them.

Scope

scripts/bump-objectui.sh and scripts/objectui-changeset-digest.mjs only, as dispatched. No changeset: scripts/ is repo tooling and this PR publishes nothing, so it carries skip-changeset.

.github/workflows/cut-rc.yml has the same object-presence test at line 285 and prints "range is walkable, the changeset digest will be complete" on the strength of it. It is currently harmless — that job clones objectui full, deliberately — so I did not widen this PR into a third file; filed separately instead.

Generated by Claude Code


Generated by Claude Code

… presence
`bump-objectui.sh` decided whether the pin range could be walked with
`git cat-file -e OLD_SHA` — "is the OLD endpoint present as an object". That
is a different question from "is the walk between the endpoints complete", and
the gap is measured rather than theoretical: on the bump that landed
`.changeset/console-82a94170c405.md` the test PASSED against a history
truncated at commit 110 of 191, so the degraded path never fired and the digest
exited 0 having credited 36 of its 119 entries to one commit that adds exactly
one changeset.
A truncated history is worse than an absent endpoint precisely because it
ANSWERS: git presents its oldest visible commit as parentless, diffs it against
the empty tree, and reports every `.changeset/*.md` in that commit's tree as
added by it. The dedup in `collectAddedChangesets` then hands it whatever no
newer commit claimed, so one commit absorbs a batch.
`findRangeTruncation` tests the harm directly — a parentless commit INSIDE the
range — which catches a `--depth` clone, a hand-written `.git/shallow`, a
graft, a `git replace` and unrelated histories without naming any of them, and
which does not fire on a shallow clone whose boundary sits at or before `from`
(that range walks completely, and refusing it would be a false positive). It
lives in `classifyRange`, the single shared implementation, so
`objectui-range.mjs` cannot derive from a truncated walk either.
`bump-objectui.sh` now repairs the input before labelling a derivation of it:
`git fetch --unshallow` costs ~6s and ~4MB on objectui and takes the walk from
110 commits to the true 191, while a degraded console changeset becomes
permanent published CHANGELOG text. The fetch is additive, announced,
opt-out-able with OBJECTUI_NO_DEEPEN=1, and — measured — can exit 0 without
repairing anything, so the range is re-checked instead of the status trusted.
The arithmetic check the card sketched is NOT implemented, because it was
measured not to work: `git show --name-only` on the sink runs the same
empty-tree diff, so the credited set is a subset of the attributed set by
construction. It fires on neither known-bad run (36 credited vs 72 attributed;
12 vs 47) and cannot fire on any history. Pinned as a tautology in the
self-test so it is not reintroduced as a cheap backstop.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 18, 2026
@os-steve
os-steve marked this pull request as ready for review August 18, 2026 01:37
@os-steve
os-steve enabled auto-merge August 18, 2026 01:38
@os-steve
os-steve disabled auto-merge August 18, 2026 01:39
@os-steve
os-steve enabled auto-merge August 18, 2026 01:39
@os-steve
os-steve disabled auto-merge August 18, 2026 01:39
@os-steve
os-steve added this pull request to the merge queueAug 18, 2026
Merged via the queue into main with commit afe0195Aug 18, 2026
26 of 28 checks passed
@os-steve
os-steve deleted the claude/issue-9408-shallow-walk-guard branch August 18, 2026 02:04
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-steve@claude