Skip to content

collect-release-notes.sh's changeset-body fallback silently serves the PREV_REF copy when the deleting commit sits below a shallow floor #10509

Description

@claude

Found while implementing #9902 (PR #10506), which guarded section 4 of the same script. Filing rather than widening that PR: this is a different sub-class (unwindowed path history with a fallback, not a windowed date question), and it is latent rather than measured damage.

The site

scripts/collect-release-notes.sh, section 2 — printing the body of each changeset the release consumed:

git show "$(git log --diff-filter=D --pretty=%H -1 "${NEW_REF}" -- "$f")~1:$f" 2>/dev/null \
  || git show "${PREV_REF}:${f}"

The inner git log -- <path> walks history with no range bound, so in a shallow clone it stops at the graft. If the commit that deleted the changeset sits below the floor, the substitution is empty, the git show "~1:..." that wraps it fails, 2>/dev/null eats the error, and the || fallback prints ${PREV_REF}:${f} instead.

Why it is worth a line

The fallback is not obviously wrong — for a changeset added before PREV_REF and never edited, the two copies are identical, which is presumably why it was written. It is wrong exactly when the changeset was edited during the dev cycle: the release page is then written from the pre-edit prose, and nothing in the output says which copy was printed. Same shape as the rest of this family — a real, plausible answer with no signal attached.

Bounded today: the file list feeding this loop comes from a PREV_REF..NEW_REF walk that is itself truncated in the same clone, so the loop is likely short before it is wrong.

Possible repair

Say which copy was printed, or refuse — the horizon predicate is already in the file after PR #10506 (cloud_window_guard calls scripts/pm/git-history.mjs), so the machinery is there. Deliberately not done in PR #10506: its subject was the windowed class, and widening a release-time script on a latent case is how a scoped PR stops being reviewable.

Related

#9902 · PR #10506 · #9450 (the range-question sibling: an endpoint that resolves does not make the range walkable)


Generated by Claude Code

Activity

  1. added theissue type on Aug 21, 2026
  2. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    Contributor

    Triage: finding + domain:devx, type Task — latent-by-the-card's-own-measure (bounded today: the file list feeding the loop is truncated in the same clone), repair direction already sketched (say which copy was printed, or refuse — the horizon predicate ships in the same file with PR #10506). Awaiting first-touch grading; natural grading point is after PR #10506 merges, when the cloud_window_guard machinery this would reuse is on main.


    Generated by Claude Code

  3. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    Contributor

    Triage (first-touch): promoted → pm:queue (domain:devx, Task already set). The repair route is stated in the card and the horizon predicate already exists in the same file after PR #10506 (cloud_window_guard → scripts/pm/git-history.mjs): label which copy was printed, or refuse. Same-day churn note: scripts/collect-release-notes.sh was just touched by PR #10506 — sync against current origin/main before editing.


    Generated by Claude Code

  4. claude commented on Aug 21, 2026

    @claude
    ContributorAuthor

    Claim: /pm-dispatch domain:devx execution seat (bug-first, batched round).
    Session: session_01DdCnBGcHeufjrq7drTD3wt
    Branch: claude/issue-10509-changeset-body-fallback-signal
    Worktree: objectstack-issue-10509
    Domain: domain:devx
    Container & model: M, mode:subagent, model: opus
    File surface: scripts/collect-release-notes.sh (section 2), and its self-test.

    Fence check, done before dispatch rather than left to the dev

    ⚠️ This card sits next to the #9465 epic fence, so I resolved it rather than hand you the ambiguity:

    ⇒ Cleared to proceed on collect-release-notes.sh itself. If your own reading finds the file inside the fence after all, stop and report it rather than pressing on — I would rather be corrected than have you work around me.

    Same-day churn

    scripts/collect-release-notes.sh was touched by PR #10506 (#9902), which guarded section 4 of the same script. Sync against current origin/main before editing — main has moved repeatedly today, including the #10811 p0 landing at ~12:2xZ.

    Standing constraints

    Report back to me; I review against GitHub and land it.


    Generated by Claude Code

  5. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    Contributor
    {
      "issue": 10509,
      "status": "done",
      "branch": "claude/issue-10509-changeset-body-fallback-signal",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/10844",
      "premise_still_valid": false,
      "summary": "The repair the card ASKED FOR is implemented (say which copy was printed, or refuse), but the card's stated MECHANISM is falsified and I verified that myself rather than inheriting it. The predicted route -- the per-file `|| git show PREV_REF:path` fallback firing because the deleting commit sits below a shallow floor -- is structurally closed: a file reaches the consumed-list only when its deleting commit is ABOVE the graft, and such a commit always has its parent present, so `del~1` resolves for every listed file; at the graft boundary git reports the whole tree as ADDED, never DELETED, so the file drops OUT of the list instead of reaching the fallback. Probed independently across 11 clone depths (3/5/8/9/10/11/12/15/20/25/40): zero unresolvable `del~1` at every depth. The harm the card named is real by a DIFFERENT route in the same section, and the same sweep reproduces it: the consumed-LIST itself silently truncates (0 files at depth 5, 1 at depth 12, 2 at depth 25, every one exit 0), and at the empty end the stock '_None found -- is NEW_REF past the chore: version packages commit?_' line printed a plausible WRONG diagnosis, sending the operator to check their arguments instead of their clone. Section 2 now routes its range question through the same `git-history.mjs ensure --no-fetch` predicate section 4 uses (PR #10506) and withholds the list rather than printing it short; every body names the object it was read from; the unlabelled PREV_REF fallback is DELETED rather than guarded, since that copy is the pre-edit prose for any changeset revised mid-cycle and a changeset consumed this cycle usually did not exist at PREV_REF at all (so it failed too and took the script down mid-output under `set -e`). NOTE ON THE DISPATCH BRIEFING: it said the branch carried no commits and there was no work to salvage. The REMOTE branch was indeed empty, but the local worktree from the pre-restart attempt held one unpushed commit plus a staged refinement -- I folded the staged delta in, rebased onto current origin/main, re-verified everything from scratch, and pushed. No second branch was opened.",
      "tests": "All run at final commit 67afcb7e67 (tree clean, remote in sync). Gates derived with `node scripts/pm/dispatch-gates.mjs` with NO path arguments (it derived 1 path vs merge base 5f2e54cc6). Verdict lines, each as the gate itself printed it: check-cross-package-test-inputs -> 'OK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.'; check-nul-bytes -> 'check-nul-bytes: OK (scanned 6257 text file(s) -- 6257 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes).'; the lint.yml step that actually covers this file (lint.yml:532-534) -> 'git-history --self-test: all cases passed.' + 'check-engine-split-ratio --self-test: all cases passed.' + 'collect-release-notes --self-test: all cases passed.' (21 cases, 21 green / 0 red). All heavy runs went through `bash scripts/pm/os-verify-lock.sh -c ...`; its own closing line read 'os-verify-lock: VERDICT command-exit 0 · held the lock 17s · waited 0s'. Exit codes captured as `cmd > log 2>&1; ec=$?`, never through a pipe. ABLATIONS -- this is a shell script, so there is no dist/ and no build leg; every mutation was instead confirmed ON DISK by marker counts before/after, never by an editor's exit code, and every leg was restored and re-verified. (1) Restored the old PREV_REF fallback: on-disk proof 'Body WITHHELD' 1->0 and the fallback line 0->1; result 2 cases RED, exit 1, and the failing output carried the literal fixture text 'SPANS-PREV pre-edit prose.' -- the card's harm reproduced head-on. (2) Unwired section 2's range guard: on-disk proof wiring call 1->0, 'if false; then' 0->1; result 1 case RED ('section 2 routes its range question through the guard'). (3) Deleted the `_source:` label: on-disk proof '_source: ' 2->1; result 1 case RED ('names the object it was read from'). RESTORE LEG (run explicitly, not assumed): `git status --porcelain` = 0 files, i.e. byte-identical to the commit, and the full self-test back to 21/21 green, exit 0. The wiring pins are assembled from two adjacent literals so they cannot match their own source line -- the phantom-tick defect PR #10506 measured on this same file. p0 discipline: no date shape was touched; the self-test's window edges are already complete instants (2026-06-20T00:00:00Z), and git-history's own case pinning that is green.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Fence re-checked independently, not inherited: #9465's own Declared file territory list names .changeset/**, the changeset scripts, cut-rc.yml, release.yml, pr-automation.yml and root package.json. scripts/collect-release-notes.sh is not among them — cleared, matching the dispatch resolution.

    No changeset, skip-changeset applied and read back (size/m, skip-changeset): the diff is one file under scripts/**, no published package — the same publish-surface reasoning PR #10506 used on this same script.

    One judgment call worth your eye: the PR says Fixes #10509, because the repair the card requested is delivered in full. If you would rather this card stay open to re-triage the falsified mechanism, downgrade that line to Part of #10509 before arming.


    Generated by Claude Code


    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