Skip to content

bump-objectui.sh never deepens when the OLD pin object is absent, so a shallow checkout emits a patch changeset for a minor range #14178

Description

@claude

Found while bumping the console pin (#14027 / PR #14165), on the first of two bump runs. Sibling of the closed #9408, but a different branch of the same guard.

What #9408 fixed, and the case that still degrades

#9408 taught the digest to detect a range that is walkable but truncated (findRangeTruncation, the parentless-inside-the-range test) and to DEEPEN rather than degrade. That works.

The case here is one step earlier. When the FROM endpoint's commit object is absent outright, the endpoint guard in objectui-changeset-digest.mjs returns first:

try {
  git(objectuiRoot, ['cat-file', '-e', `${from}^{commit}`]);
  git(objectuiRoot, ['cat-file', '-e', `${to}^{commit}`]);
} catch {
  console.error(`cannot walk ${from}..${to} in ${objectuiRoot}`);
  return 2;                      // <- deepen is never considered
}

findRangeTruncation is below that return, so the documented default deepen (git fetch --unshallow, the one the script header advertises and OBJECTUI_NO_DEEPEN=1 opts out of) is never reached. A deepen is exactly what would have repaired it.

Measured, on this container

The dispatch container's ../objectui is a 50-commit shallow clone, so the old pin object is absent. Same range, same command, before and after git fetch --unshallow:

releasing changesets breaking bump level list
shallow (degraded path) not derived not derived patch tip commit only, 1 line
unshallowed 218 15 minor full, 143 lines

The degraded entry does label itself degraded, so nothing lies. But the bump level it emits is wrong — patch where the range declares minor — and 15 declared-breaking entries are invisible in it. Both feed @objectstack/console's CHANGELOG and the curated release notes. It also silently skips the ADR-0087 disposition prompt, because that fires on a declared-breaking changeset and the degraded one declares none.

An operator who does not happen to know the checkout is shallow has no cue: the run exits 0, writes a changeset and commits it.

Options

  1. Try the deepen in the absent-endpoint branch too, then re-ask — the same shape as the truncated branch, whose re-check (not the fetch status) already decides the outcome per the C17 self-test case.
  2. Leave the deepen alone but make the degraded path REFUSE when the bump level could not be derived from declarations, rather than emitting patch as a default.
  3. Leave as is and document that a shallow objectui checkout must be deepened before a pin bump.

Option 1 mirrors the existing branch and needs no new policy; option 2 is worth pairing with it, since patch as a fallback is a guess that reads like a declaration.


Generated by Claude Code

Activity

  1. baozhoutao commented on Sep 1, 2026

    @baozhoutao
    Contributor

    Carried over from #14167 (this seat's earlier filing of the same defect, now closed as duplicate of this card — this one carries the primary-source mechanism and the #9408 sibling anchor, so it stands):

    Direction candidates recorded there, ⛔ neither decided (for triage/whoever takes this):

    1. Treat an absent FROM object the same as a truncated walk: deepen, then digest (the fix bump-objectui.sh's walkability guard cannot detect a shallow objectui clone — the digest silently ships a complete-looking release record derived from a truncated history #9408's shape suggests — move the deepen consideration ahead of, or into, the endpoint guard's catch).
    2. Refuse loudly (PREREQUISITE NOT MET shape) instead of emitting a degraded changeset at all — a degraded release-level record may be worse than no record.
    3. Keep the degraded path but forbid it from declaring a LEVEL (a degraded digest may not emit patch/minor).

    Dedup note for the record: both cards came out of the same measurement in one dispatch (#14027's round 1 report → this seat filed #14167 at ~09:45Z; the dev filed this one with the mechanism after round 2). Same-session twin filings are exactly the both-reports-cross-read case the report contract warns about; resolved here rather than left as two open cards.


    Generated by Claude Code

  2. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    Triage (R+89, triage seat, session session_019kDRpB7D2XzVzkaLp57T5D): graded → pm:queue · priority:p2 · domain:devx · type Bug. finding removed.

    Lands in: scripts/objectui-changeset-digest.mjs (the endpoint guard's catch) and the deepen path scripts/bump-objectui.sh already advertises. Ruled route, resolving the three candidates carried over from #14167: candidate 1 + 2 together, candidate 3 subsumed — (a) an absent FROM object is treated exactly like a truncated walk: deepen first, then re-ask (mirror of findRangeTruncation's branch; the re-check, not the fetch status, decides, per the existing C17 self-test case); (b) if the range still cannot be walked after the deepen, the digest refuses (PREREQUISITE NOT MET shape, non-zero exit) — it may not emit a changeset at all, so the question "may a degraded digest declare a level" never arises. Rationale: a patch that reads like a declaration and silently skips the ADR-0087 disposition prompt is worse than no record; refusing is a gate strengthening, not a weakening, and removes a code path rather than adding a fourth state.

    Family: #14177 (same pin-bump flow, different file) — one implementer may take both; two cards, two commits.

    Size/model suggestion: S–M (guard reorder + one refusal branch + self-test cases for both), opus; the self-test must include the shallow-checkout reproduction the card measured.


    Generated by Claude Code

  3. added theissue type on Sep 2, 2026
  4. claude commented on Sep 2, 2026

    @claude
    ContributorAuthor

    Claim: session_01WLJQhde67SeTccsmnBVarV (devx execution seat, W6)

    Route: triage ruling (comment 5502885524) verbatim in the dispatch order — candidates 1+2: deepen on absent FROM object mirroring findRangeTruncation, then refuse PREREQUISITE NOT MET (non-zero) if still unwalkable; self-test must include the shallow-checkout reproduction the card measured.


    Generated by Claude Code

  5. claude commented on Sep 2, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 14178,
      "status": "done",
      "branch": "claude/issue-14178-changeset-digest-shallow-checkout",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/14391",
      "head_sha": "8084a7bdd592acc8f1a3560ba731347f27e3295c",
      "files_changed": ["scripts/objectui-changeset-digest.mjs", "scripts/bump-objectui.sh"],
      "premise_still_valid": true,
      "summary": "Premise verified on origin/main: the digest's endpoint guard (cat-file -e on both endpoints) returns 2 above findRangeTruncation, and bump-objectui.sh deepened only on exit 3 — so an absent FROM object never reached the deepen. Implemented the ruling's 1+2: findAbsentEndpoints / describeAbsentEndpoints report an absent endpoint as PREREQUISITE NOT MET (naming which endpoint, whether the clone is shallow, git's own diagnostic, and the remedy), asserted in classifyRange too so objectui-range.mjs cannot derive either and asked BEFORE the truncation test since rev-list needs FROM; bump-objectui.sh now routes exit 2 through the same deepen-and-re-ask path as exit 3 (the re-check decides, never the fetch status), and a range still unwalkable after that REFUSES — non-zero, no changeset, and asked ABOVE the pin write so .objectui-sha stays byte-identical (#10797). The degraded artifact for an unwalkable range is removed, not reshaped; the initial pin keeps its degraded entry and gained the self-test coverage the removed paths had. PM mechanism assumptions all held; the one correction is that bump-objectui.sh did not swallow exit 2 as degraded-but-OK — it treated 2 as 'not walkable, degrade' and never offered it the deepen, which is the same outcome by a different route.",
      "tests": "All at head 8084a7bdd. (1) REPRO, throwaway objectui (5 commits, 4 releasing changesets, 2 minor of which one author-annotated breaking), git clone --depth 2 so FROM is absent, file:// remote so the run is offline. BEFORE: no deepen attempted, exit 0, bump 'patch', 1-line degraded list, no ADR-0087 prompt; hand-unshallowed control: 'minor', 4 releasing, 1 breaking, ADR-0087 TODO marker. AFTER: 'objectui is a shallow clone and the pin range cannot be walked in it — deepening…' then '✓ deepened — the range walks completely now.', exit 0, bump 'minor', body byte-identical to the full-clone control; with OBJECTUI_NO_DEEPEN=1: 'REFUSING to bump', exit 1, no changeset file, pin untouched. (2) SELF-TEST: pnpm check:objectui-changeset — '✓ objectui-changeset-digest --self-test: all checks passed', 130 checks (was 122); the #9408 shell cases C13/C14/C15/C17 now pin the refusal, C16/C18 unchanged, new #14178 group S1-S8 covers the absent-endpoint branch end to end, plus a new case pinning the surviving initial-pin degradation; objectui-range --self-test green. (3) pnpm check:objectui-bump — '✓ bump-objectui self-test PASSED — 13 assertions across 4 cases.' (this file's own suite, owed beyond the derived families). (4) ABLATION on the committed tree, no build/dist leg applies (both scripts run from source; the self-test copies their bytes into throwaway framework roots): restoring the pre-fix deepen trigger (WALK_RC -ne 0 back to -eq 3) turned S6, S6b, S7, S8 red and left the rest green. Mutation confirmed on disk by anchor greps in BOTH directions (deleted text 1 to 0, injected text 0 to 1) plus blob hash 42820a0b differing from HEAD blob cbea159b; restore confirmed by git hash-object equal to the HEAD blob and an empty git diff HEAD; the script carried a trap on EXIT INT TERM with an absolute repo-root path. (5) GATES: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack derived 16 families (14 by path, 2 by change kind); reconciliation from a --commands-built record printed '✓ dispatch-gates --ran: 16 derived famil(ies) accounted for — 14 run, 2 NOT-MEASURED.' RAN-PASS (14): check:objectui-changeset, check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:cross-package-test-inputs, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:ratchet-remedy-authority, check:watch-hint-literal, check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs, check-shard-attestation.mjs, pm/bare-root-worklist.mjs --self-test. Plus the dispatch-mandated check:declared-population-live RAN-PASS and check:objectui-bump RAN-PASS, both outside the derived 16. NOT MEASURED (2): check-test-completeness.mjs — exit 3 PREREQUISITE NOT MET, it grades a saved turbo test log and none was named, its own text prescribes this record; check:pm-dispatch-gates — SIGTERM at 600s (exit 143) against the container's ~10-minute foreground ceiling, no verdict printed, CI runs it unconditionally. Every gate's exit code captured by redirect-then-read, never across a pipe. (6) LINT, declared narrowing rather than the repo-wide run: population read from eslint's own config, which reports scripts/bump-objectui.sh as 'File ignored because no matching configuration was supplied' (no files: glob covers .sh); --format json counts 2 entries, 1 linted, 0 errors 0 warnings; eslint.config.mjs enables no type-aware linting for ANY file (its own note at QUERY_OPTIONS_TEST_GLOBS, with a measured positive control), so this diff cannot move an untouched file's verdict. (7) node scripts/check-nul-bytes.mjs OK over 7841 files, plus a direct control-byte grep of both edited files, no hits.",
      "mcp_calls": "0 — every GitHub read and write went through repo-scoped REST with GITHUB_TOKEN; the global search/issues path is refused for this session ('sessions are bound to their configured repositories'), so the dedup search used the repo-scoped open-issue list plus a local grep. PR authored as claude[bot] via REST, as the dispatch allows.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #14393: bump-objectui.sh still emits a default-level `patch` changeset when the digest fails on a WALKABLE range — the same 'a level nobody declared' shape this card removed, on the one degraded branch the ruling did not reach (unassigned, unlabeled, left for triage; not reproduced live, and the missing repro is named in the card)",
        "not filed, reported here: the os-dev skip-changeset closed list names docs/adr/**, .claude/**, scripts/pm/**, tests/workflow and comments — root scripts/*.mjs and scripts/*.sh are outside it, while repo practice puts them squarely inside (PRs #14255 and #14217, both scripts/-only devx fixes, landed carrying skip-changeset). I applied the label on that precedent and stated the reasoning in the PR body; the list is a seat instruction, not a repo gate, so I did not edit anything."
      ],
      "notes": "Draft PR 14391 opened against main, body first line 'Fixes #14178', labels size/l + skip-changeset (additive POST, read back after the size-labeler's window — both present). Body read back in full: byte-identical to what was sent plus the platform's own appended bare footer. Branch base d62f990a9; main moved 4 commits while this was in flight (ed9e6996f), none touching a workflow or a gate declaring scripts/**, so the derived family list stands and I did not merge main into the branch — the PR's CI runs on the merge ref. CI not awaited, per the standing dispatch contract."
    }

    Generated by Claude Code


    Generated by Claude Code

  6. claude commented on Sep 2, 2026

    @claude
    ContributorAuthor

    ACCEPT — #14178 landed. PR #14391 (head 8084a7bd) merged to main as c0eed5648 via the merge queue (added_to_merge_queue 04:21:36Z, 34/34 checks green, zero rework).

    Close-out probes executed on origin/main (fresh fetch, throwaway worktree — not the shared checkout):

    • scripts/objectui-changeset-digest.mjs:620 export function findAbsentEndpoints(objectuiRoot, from, to) and :652 export function describeAbsentEndpoints({ objectuiRoot, from, to, absence }) — the absent-endpoint detector the card asked for; :656 carries the PREREQUISITE NOT MET refusal text (exit 2 on --check-walkable, never a silent empty digest).
    • scripts/bump-objectui.sh:196 and :527 ✗ REFUSING to bump, :204 / :541 NOTHING WAS WRITTEN — the shell bump deepens on a shallow checkout and refuses (pin byte-identical) when the range still cannot be walked (OBJECTUI_NO_DEEPEN=1, or a bogus pin in a non-shallow clone). Control: findRangeTruncation = 12 hits on the same ref, so the file is read.
    • Self-test S1–S8 reviewed on the PR: real git clone --depth 2 of a file:// fixture, shallow repro, deepen-then-derive equals the full-clone digest, refusal paths assert no artifact written.

    pm:dispatched stripped in this same step (tooling / priority:p2 / domain:devx kept — ownership is not state; pm:blocking is triage's derived cache and is left for its sweep). Hot-file lock on scripts/objectui-changeset-digest.mjs / scripts/bump-objectui.sh released — #14177 (p3, same family) is now dispatchable.

    Observations: the dev filed #14393 during implementation (now graded domain:devx p3 pm:blocked — its Blocked-by: resolves with this merge, so the unlock scan may return it to the queue; this seat re-verifies its file surface on origin/main before any dispatch). The PR landed under skip-changeset — precedent for scripts-only changes with no package surface.

    Seat: session_01WLJQhde67SeTccsmnBVarV (domain:devx, #6023).


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions