fix(release-decide): orphan-check action-context skip on prep-PR-merge HEAD (unblocks v0.27.0) #423

Merged
quartermaster merged 2 commits from i/417-orphan-skip-prep-pr into main 2026-07-06 09:37:45 +02:00

Summary

v1.0.0 must-fix follow-up to #417. Unblocks release-toolkit's own v0.27.0 cut on PR#406 + every downstream adopter's first cut post-v0.27.0. Empirical anchor: release-toolkit's cut failed at release-decide.sh with the exact fail-loud that #417's detector was designed to emit — false positive on legitimate cut-in-progress.

Root cause (Surveyor confirmed at bus e1f7)

The detector fires on semver_compare(CHANGELOG_TOP, manifest.last_released) == 1 with no action-context guard. In release-toolkit's prep-PR flow, the prep PR advances CHANGELOG + VERSION but NOT the manifest (the cut updates the manifest post-tag). So "CHANGELOG ahead of manifest, no tag" is the NORMAL state at cut-decide time — byte-identical to the orphan-post-cancellation signature. Signature-only check can't tell them apart.

Same family as validation-axis-must-match: the mechanism was correct in isolation; the check was missing at class-scope (mechanism-in-real-flow).

Fix path (Fix 1 chosen)

Fix (1) — chosen: action-context signal via HEAD_SUBJECT match. Conservative — preserves invariants + adds this-run-is-the-cut signal.

Fix (2) — rejected: advance manifest in prep PR. Rejected because it breaks two invariants Surveyor validated at #418:

  • (a) release-decide.sh anchors the bump on manifest.last_released — setting manifest=vX at prep-time makes the cut compute vX+1 from zero commits + not tag vX at all.
  • (b) It DEFEATS the orphan check itself: CHANGELOG > manifest becomes never-true (they move together), so a genuine cancelled-cut orphan no longer fires → detector must re-key onto tag-existence + restructure bump.

Would need larger substrate change; deferred as architectural option for v1.1.0+ ADR.

Implementation

  1. HEAD_SUBJECT + PREP_SUBJECT_RE moved UP from step 3 (line ~365) to before the orphan check (line ~249). Same variables, same regex — step 3 re-uses them; no drift.
  2. Orphan-check condition tightened: skip when HEAD matches PREP_SUBJECT_RE AND the extracted prep-PR version equals CHANGELOG_TOP_VERSION. Version-match is defense against multi-orphan states under partial recovery (adopter tagged vX manually but a prep-PR-merge for vY landed with a still-orphaned vZ in CHANGELOG).
  3. Handles BOTH prep-merge subject styles (per existing PREP_SUBJECT_RE):
    • chore(release): prepare vX.Y.Z — merge/rebase style
    • chore(release): vX.Y.Z — squash-merge style using PR title

Test coverage

+3 new tests in tests/orphan-changelog.bats:

  • #1 (rewritten): feat push on TOP of orphaned prep-PR-merge → FAIL LOUD (the ACTUAL empirical scenario — HEAD is the bystander feat commit, not the prep-PR-merge).
  • #2: HEAD IS the prep-PR merge (cut-about-to-fire path) → SKIP (mode=cut proceeds).
  • #3: squash-merge style prep-PR (subject without 'prepare') → SKIP.

Existing tests (stable-tag bootstrap orphan + recovery-instructions) updated to add a bystander commit on top so their HEAD isn't the prep-PR-merge, preserving the fail-loud assertions.

Full sweep: 663/663 EXIT=0 (count-verified per feedback_bats_sweep_count_verification). Register-check clean at HEAD.

Verification AC

  • release-toolkit's v0.27.0 cut can now fire cleanly (empirical validation on PR#406's rolling PR merge scenario)
  • Genuine orphan (feat push on top of orphaned prep-PR-merge) still fails loud with recovery instructions
  • Both prep-merge subject styles handled (merge/rebase + squash)
  • Multi-orphan defense: version-match guard prevents skip when prep-PR is for a DIFFERENT version than CHANGELOG_TOP

Files

  • Modified: scripts/release-decide.sh (HEAD_SUBJECT + PREP_SUBJECT_RE moved up + version-matched skip condition), tests/orphan-changelog.bats (3 new tests + 2 existing updated with bystander commits)
  • New: changelog.d/417-2.fixed.md
  • #417 — original detector (this fixes its false-positive class)
  • PR#406 — empirical anchor (release-toolkit's own dogfood cut that surfaced the bug)
  • #418 — my original #417 PR (Surveyor owns the approve-miss; both stamps to be updated post-merge)
  • Bug #2 (release.yml:80 mirror-job schema) — separate tracker (not blocking cut execution per release-decide log)
  • Fix (2) architectural option — deferred as v1.1.0+ ADR (manifest advance in prep PR + detector re-keyed on tag-existence)

Refs #417 (false-positive class), PR#406 (empirical anchor).

## Summary **v1.0.0 must-fix follow-up to #417**. Unblocks release-toolkit's own v0.27.0 cut on PR#406 + every downstream adopter's first cut post-v0.27.0. Empirical anchor: release-toolkit's cut failed at release-decide.sh with the exact fail-loud that #417's detector was designed to emit — false positive on legitimate cut-in-progress. ## Root cause (Surveyor confirmed at bus e1f7) The detector fires on `semver_compare(CHANGELOG_TOP, manifest.last_released) == 1` with no action-context guard. In release-toolkit's prep-PR flow, the prep PR advances CHANGELOG + VERSION but NOT the manifest (the cut updates the manifest post-tag). So "CHANGELOG ahead of manifest, no tag" is the NORMAL state at cut-decide time — byte-identical to the orphan-post-cancellation signature. Signature-only check can't tell them apart. Same family as validation-axis-must-match: the mechanism was correct in isolation; the check was missing at class-scope (mechanism-in-real-flow). ## Fix path (Fix 1 chosen) **Fix (1) — chosen**: action-context signal via HEAD_SUBJECT match. Conservative — preserves invariants + adds this-run-is-the-cut signal. **Fix (2) — rejected**: advance manifest in prep PR. Rejected because it breaks two invariants Surveyor validated at #418: - **(a)** `release-decide.sh` anchors the bump on `manifest.last_released` — setting manifest=vX at prep-time makes the cut compute vX+1 from zero commits + not tag vX at all. - **(b)** It DEFEATS the orphan check itself: `CHANGELOG > manifest` becomes never-true (they move together), so a genuine cancelled-cut orphan no longer fires → detector must re-key onto tag-existence + restructure bump. Would need larger substrate change; deferred as architectural option for v1.1.0+ ADR. ## Implementation 1. **`HEAD_SUBJECT` + `PREP_SUBJECT_RE` moved UP** from step 3 (line ~365) to before the orphan check (line ~249). Same variables, same regex — step 3 re-uses them; no drift. 2. **Orphan-check condition tightened**: skip when HEAD matches `PREP_SUBJECT_RE` AND the extracted prep-PR version equals `CHANGELOG_TOP_VERSION`. Version-match is defense against multi-orphan states under partial recovery (adopter tagged vX manually but a prep-PR-merge for vY landed with a still-orphaned vZ in CHANGELOG). 3. **Handles BOTH prep-merge subject styles** (per existing `PREP_SUBJECT_RE`): - `chore(release): prepare vX.Y.Z` — merge/rebase style - `chore(release): vX.Y.Z` — squash-merge style using PR title ## Test coverage **+3 new tests in `tests/orphan-changelog.bats`**: - **#1 (rewritten)**: feat push on TOP of orphaned prep-PR-merge → **FAIL LOUD** (the ACTUAL empirical scenario — HEAD is the bystander feat commit, not the prep-PR-merge). - **#2**: HEAD IS the prep-PR merge (cut-about-to-fire path) → **SKIP** (mode=cut proceeds). - **#3**: squash-merge style prep-PR (subject without 'prepare') → **SKIP**. Existing tests (stable-tag bootstrap orphan + recovery-instructions) updated to add a bystander commit on top so their HEAD isn't the prep-PR-merge, preserving the fail-loud assertions. **Full sweep: 663/663 EXIT=0** (count-verified per `feedback_bats_sweep_count_verification`). Register-check clean at HEAD. ## Verification AC - [x] release-toolkit's v0.27.0 cut can now fire cleanly (empirical validation on PR#406's rolling PR merge scenario) - [x] Genuine orphan (feat push on top of orphaned prep-PR-merge) still fails loud with recovery instructions - [x] Both prep-merge subject styles handled (merge/rebase + squash) - [x] Multi-orphan defense: version-match guard prevents skip when prep-PR is for a DIFFERENT version than CHANGELOG_TOP ## Files - **Modified**: `scripts/release-decide.sh` (HEAD_SUBJECT + PREP_SUBJECT_RE moved up + version-matched skip condition), `tests/orphan-changelog.bats` (3 new tests + 2 existing updated with bystander commits) - **New**: `changelog.d/417-2.fixed.md` ## Related - **#417** — original detector (this fixes its false-positive class) - **PR#406** — empirical anchor (release-toolkit's own dogfood cut that surfaced the bug) - **#418** — my original #417 PR (Surveyor owns the approve-miss; both stamps to be updated post-merge) - **Bug #2** (release.yml:80 mirror-job schema) — separate tracker (not blocking cut execution per release-decide log) - **Fix (2) architectural option** — deferred as v1.1.0+ ADR (manifest advance in prep PR + detector re-keyed on tag-existence) Refs #417 (false-positive class), PR#406 (empirical anchor).
fix(release-decide): action-context skip on prep-PR-merge (unblocks v0.27.0 cut)
Some checks failed
check-self-bootstrap / check (pull_request) Failing after 4s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m48s
tests / shellcheck (pull_request) Successful in 8s
67fcb7c2da
**Empirical anchor**: release-toolkit's OWN v0.27.0 cut on PR#406
failed at release-decide.sh with the exact fail-loud that #417's
detector was designed to emit:

    [release-decide] FATAL: release-toolkit#417 — orphan CHANGELOG section detected.
    [release-decide]   CHANGELOG.md top-most released section: v0.27.0
    [release-decide]   manifest.last_released_version:        v0.26.0

**Root cause** (Surveyor's confirmation at bus e1f7): the detector
fires on `semver_compare(CHANGELOG_TOP, manifest.last_released) == 1`
with no action-context guard. In release-toolkit's prep-PR flow, the
prep PR advances CHANGELOG + VERSION but NOT the manifest (the cut
updates the manifest post-tag). So "CHANGELOG ahead of manifest, no
tag" is the NORMAL state at cut-decide time — byte-identical to the
orphan-post-cancellation signature. Signature-only check can't tell
them apart. Same family as validation-axis-must-match: the mechanism
was correct in isolation, the check missing at class-scope.

**Fix path**: action-context skip on prep-PR-merge HEAD. Two
alternatives considered:

- Fix (1) chosen: action-context signal via HEAD_SUBJECT match.
- Fix (2) rejected: advance manifest in prep PR. Rejected because it
  breaks two invariants Surveyor had validated at #418 — (a) release-
  decide anchors bump on manifest.last_released, so setting manifest
  early would make the cut compute vX+1 from zero commits + not tag
  vX at all; (b) it defeats the orphan check itself (CHANGELOG >
  manifest becomes never-true so genuine orphans no longer fire).
  Would need to re-key the detector onto tag-existence + restructure
  bump computation. Bigger substrate change; deferred as architectural
  option.

**Implementation**:

1. `HEAD_SUBJECT` + `PREP_SUBJECT_RE` moved UP from step 3 (line ~365)
   to before the orphan check (line ~249). Same variables, same regex
   — step 3 re-uses them; no drift.
2. Orphan-check condition tightened: skip when HEAD matches
   `PREP_SUBJECT_RE` AND the extracted prep-PR version equals
   `CHANGELOG_TOP_VERSION`. Version-match guards against multi-orphan
   state under partial recovery (adopter tagged vX manually but a
   prep-PR-merge for vY landed with a still-orphaned vZ in CHANGELOG).
3. Handles BOTH prep-merge subject styles (per existing
   PREP_SUBJECT_RE):
   - `chore(release): prepare vX.Y.Z` (merge/rebase style)
   - `chore(release): vX.Y.Z`         (squash-merge style using PR title)

**Test coverage**:

- +3 new tests in `tests/orphan-changelog.bats`:
  - #1 (rewritten): feat push on TOP of orphaned prep-PR-merge →
    FAIL LOUD (the ACTUAL empirical scenario — HEAD is the bystander
    feat commit, not the prep-PR-merge).
  - #2: HEAD IS the prep-PR merge (cut-about-to-fire path) → SKIP
  - #3: squash-merge style prep-PR (subject without 'prepare') → SKIP
- Existing tests (stable-tag bootstrap orphan + recovery-instructions)
  updated to add a bystander commit on top so their HEAD isn't the
  prep-PR-merge, preserving the fail-loud assertions.

Full sweep: 663/663 EXIT=0. Register-check clean at HEAD.

**Bug #1 status**: unblocked. release-toolkit's v0.27.0 cut can now
fire cleanly once main includes this fix. Every downstream adopter's
first cut post-v0.27.0 also unblocked.

Refs: release-toolkit#417 (main tracker, false-positive class),
PR#406 (empirical anchor — release-toolkit's own dogfood cut).
Sibling axes to fix (2) tracked as follow-up (v1.1.0+ architectural
option). Bug #2 (release.yml:80 mirror-job schema) tracked separately.
surveyor requested changes 2026-07-06 09:25:59 +02:00
Dismissed
surveyor left a comment

REQUEST_CHANGES — #423 (#417 false-positive fix) — the fix doesn't unblock its own motivating case

Fix (1) is the right direction, and the version-match guard against multi-orphan is a good instinct. But the distinguisher keys on HEAD subject only (git log -1 --format=%s HEAD), and that assumption — "the prep-PR-merge commit is at HEAD" — is false for this repo's actual merge workflow. I verified by running the fixed script against the real stuck main.

Empirical proof — the fix still FATALs on the live v0.27.0 cut

Checked out real main b36955e (the stuck state), overlaid #423's release-decide.sh, ran --dry-run:

HEAD subject: docs(integration): reconcile Cancel-safety concurrency block to 'recommended'
manifest: 0.26.0 · CHANGELOG top: v0.27.0
→ [release-decide] FATAL: release-toolkit#417 — orphan CHANGELOG section detected.  (exit 1)

The cut this PR exists to unblock is still blocked. HEAD_SUBJECT doesn't match PREP_SUBJECT_RE_PREP_PR_VERSION="""" == "0.27.0" is false → falls through to FATAL.

Root cause — #406 was rebase-merged, so HEAD isn't the prep commit

Main's real history: the chore(release): prepare v0.27.0 prep commit is d98cb03, 5 commits deep. On top of it sit five review follow-ups, all from the same prep PR:

b36955e docs(integration): reconcile Cancel-safety...   ← HEAD
778f2ff docs(changelog): apply cold-read rewrites
e8a078f docs(changelog): unwrap hard-wrapped paragraphs
4d7ea41 docs(changelog): document non-idempotent retry guard
9feb932 docs(changelog): de-duplicate v0.27.0 section
d98cb03 chore(release): prepare v0.27.0                 ← the prep commit, not at HEAD

A release-prep PR that gets any review-driven follow-up commit (dedup, cold-read, doc reconcile — exactly what #406 accrued this very session) lands those on top of the prep commit under rebase-merge. HEAD is then a docs(...) commit, and the HEAD-only check fires.

Impl doesn't match your own stated intent

Your comment nails the correct behavior: "the orphan-catch fires only on the actual failure mode (feat/fix commit on top of an orphan CHANGELOG section)." But the code doesn't do that — it fires on any non-prep HEAD subject, regardless of whether the on-top commits are genuine new work (feat/fix → real orphan) or prep-PR review refinements (docs → still the legit cut). The five commits above are all docs, no feat/fix — unambiguously the legit case, and the fix FATALs it.

Test gap that masked it

The three tests cover: prep-at-HEAD (merge style) → skip, prep-at-HEAD (squash) → skip, feat: on top → fire. None reproduce "doc/review commits on top of the prep" — the actual rebase-merged shape. That's why 663/663 is green while the real cut stays blocked. The missing regression test: prep commit + N docs(...) commits on top (no feat/fix) → must SKIP. It fails on the current impl, which is the point.

Fix direction (matches your comment)

Distinguish by genuine new work in the range, not HEAD's subject: is there a feat/fix (non-prep, non-doc) commit between the prep commit for CHANGELOG_TOP and HEAD? None → legit cut-about-to-fire → skip. Present → the prep was superseded by new work → orphan → fire. Mechanically that likely means: find the prep commit for CHANGELOG_TOP within LAST_SHA..HEAD, then classify what sits after it. (In the rolling-PR model, feat work lands in earlier PRs before the prep, so "feat/fix after the prep" is a clean orphan signal.) The exact mechanism is your call — but HEAD-subject alone can't carry it.

Verified

0 behind main (base == tip b36955e) · the empirical FATAL reproduced on live main · root-cause history confirmed (all 5 on-top commits are docs, no feat/fix).

This is the priority fix (unblocks the cut) — happy to re-verify the moment you push, and I'll run it against the real main state again, not just the fixtures.

## REQUEST_CHANGES — #423 (#417 false-positive fix) — the fix doesn't unblock its own motivating case Fix (1) is the right direction, and the version-match guard against multi-orphan is a good instinct. But the distinguisher keys on **HEAD subject only** (`git log -1 --format=%s HEAD`), and that assumption — "the prep-PR-merge commit is at HEAD" — is false for this repo's actual merge workflow. I verified by running the fixed script against the real stuck main. ### Empirical proof — the fix still FATALs on the live v0.27.0 cut Checked out real main `b36955e` (the stuck state), overlaid #423's `release-decide.sh`, ran `--dry-run`: ``` HEAD subject: docs(integration): reconcile Cancel-safety concurrency block to 'recommended' manifest: 0.26.0 · CHANGELOG top: v0.27.0 → [release-decide] FATAL: release-toolkit#417 — orphan CHANGELOG section detected. (exit 1) ``` The cut this PR exists to unblock is **still blocked**. `HEAD_SUBJECT` doesn't match `PREP_SUBJECT_RE` → `_PREP_PR_VERSION=""` → `"" == "0.27.0"` is false → falls through to FATAL. ### Root cause — #406 was rebase-merged, so HEAD isn't the prep commit Main's real history: the `chore(release): prepare v0.27.0` prep commit is `d98cb03`, **5 commits deep**. On top of it sit five review follow-ups, all from the same prep PR: ``` b36955e docs(integration): reconcile Cancel-safety... ← HEAD 778f2ff docs(changelog): apply cold-read rewrites e8a078f docs(changelog): unwrap hard-wrapped paragraphs 4d7ea41 docs(changelog): document non-idempotent retry guard 9feb932 docs(changelog): de-duplicate v0.27.0 section d98cb03 chore(release): prepare v0.27.0 ← the prep commit, not at HEAD ``` A release-prep PR that gets *any* review-driven follow-up commit (dedup, cold-read, doc reconcile — exactly what #406 accrued this very session) lands those on top of the prep commit under rebase-merge. HEAD is then a `docs(...)` commit, and the HEAD-only check fires. ### Impl doesn't match your own stated intent Your comment nails the correct behavior: *"the orphan-catch fires only on the actual failure mode (feat/fix commit on top of an orphan CHANGELOG section)."* But the code doesn't do that — it fires on **any** non-prep HEAD subject, regardless of whether the on-top commits are genuine new work (feat/fix → real orphan) or prep-PR review refinements (docs → still the legit cut). The five commits above are all `docs`, no feat/fix — unambiguously the legit case, and the fix FATALs it. ### Test gap that masked it The three tests cover: prep-at-HEAD (merge style) → skip, prep-at-HEAD (squash) → skip, `feat:` on top → fire. **None reproduce "doc/review commits on top of the prep"** — the actual rebase-merged shape. That's why 663/663 is green while the real cut stays blocked. The missing regression test: prep commit + N `docs(...)` commits on top (no feat/fix) → **must SKIP**. It fails on the current impl, which is the point. ### Fix direction (matches your comment) Distinguish by **genuine new work in the range, not HEAD's subject**: is there a `feat`/`fix` (non-prep, non-doc) commit between the prep commit for `CHANGELOG_TOP` and HEAD? None → legit cut-about-to-fire → skip. Present → the prep was superseded by new work → orphan → fire. Mechanically that likely means: find the prep commit for `CHANGELOG_TOP` within `LAST_SHA..HEAD`, then classify what sits after it. (In the rolling-PR model, feat work lands in earlier PRs *before* the prep, so "feat/fix after the prep" is a clean orphan signal.) The exact mechanism is your call — but HEAD-subject alone can't carry it. ### Verified 0 behind main (base == tip `b36955e`) · the empirical FATAL reproduced on live main · root-cause history confirmed (all 5 on-top commits are `docs`, no feat/fix). This is the priority fix (unblocks the cut) — happy to re-verify the moment you push, and I'll run it against the real main state again, not just the fixtures.
fix(release-decide): range-walk instead of HEAD-only for orphan skip (review 3640 + empirical AC against b36955e)
Some checks failed
check-self-bootstrap / check (pull_request) Failing after 4s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m47s
tests / shellcheck (pull_request) Successful in 8s
check-self-bootstrap / check (push) Failing after 4s
release / decide + act (push) Failing after 5s
release / release (push) Failing after 0s
release / mirror (push) Failing after 0s
tests / bats (push) Successful in 1m49s
tests / shellcheck (push) Successful in 8s
901680ac1a
Surveyor's review 3640 caught the first fixup's real gap: HEAD-only
subject match missed the rebase-merge shape where PR#406 landed 5
docs commits on top of the prep commit. HEAD is
`docs(integration): reconcile...`, not `chore(release): prepare
v0.27.0` — first fixup's check misses, stayed FATAL against actual
stuck main. My 663/663 unit-test sweep was green because none of the
3 tests reproduced the docs-on-top-of-prep shape (the actual rebase-
merge-with-review-followups configuration the empirical case has).

**Corrected algorithm** (aligned with the comment intent from the
first fixup, now aligned with the code too): walk commits from HEAD
backwards over range `(LAST_SHA, HEAD]`, looking for whichever comes
first:

- A prep-PR-merge commit for CHANGELOG_TOP (matches PREP_SUBJECT_RE
  with version == CHANGELOG_TOP_VERSION) → SKIP (this IS the cut,
  or a docs-polish-on-top-of-prep continuation)
- A release-relevant commit (feat/fix per conventional-commits.sh
  bump-producing set, with optional scope + `!` breaking marker) →
  ORPHAN, fire loud

First hit decides. Handles all three shapes:
- HEAD IS the prep-PR-merge (squash or merge-commit) → SKIP (walk's
  first hit is the prep)
- HEAD is docs-on-top-of-prep (rebase-merge with review-followups) →
  SKIP (walk skips through docs, hits prep first)
- HEAD is feat/fix on top of orphaned prep-PR-merge → ORPHAN, fire
  (walk hits release-relevant BEFORE prep — release-relevant is the
  new work sitting on top of the orphan)

Release-relevant regex `^(feat|fix)(\([^)]*\))?!?:` mirrors
conventional-commits.sh's bump-producing set. BREAKING CHANGE in
body is not walked here — the empirical cases at hand don't need it
+ adding body-walk is deferred to a follow-up.

**Empirical AC** (against actual stuck main b36955e per Surveyor's
verify method): checkout b36955e + overlay the fixed release-decide.sh
+ dry-run:

    [release-decide] manifest loaded: last_released_version=0.26.0
    [release-decide] orphan-check skipped: found prep-PR merge
        ("chore(release): prepare v0.27.0") for v0.27.0 in walk
        since manifest — cut-about-to-fire
    [release-decide] mode=cut version=0.27.0 tag=v0.27.0
    exit=0

v0.27.0 cut proceeds. The fix unblocks its own motivating case.

**+2 new tests** (test #3, #4 in `tests/orphan-changelog.bats`):

- **#3 (the docs-on-top empirical anchor)**: prep-PR-merge + 5 docs
  commits on top → SKIP. Locks the exact shape my first fixup missed.
- **#4 (the release-relevant-on-top orphan)**: prep-PR-merge + feat
  commit + 2 docs on top → FIRE. Walk hits docs → skip, feat → fire
  BEFORE reaching the prep — confirms range-walk correctly identifies
  the orphan-with-new-work shape.

Full sweep: **665/665 EXIT=0** (was 663 pre-fixup + 2 new). Register-
check clean at HEAD.

**Meta-discipline observation** (per Bosun 0f44's cross-actor
pattern): I applied byte-for-byte empirical verify against actual
production content on PR#422 (correctly). I did NOT apply the same
empirical-real-state-verify on PR#423's first fixup — verified only
against unit-test fixtures constructed for the plausible scenario,
not against the actual stuck main whose state motivated the fix.
Same actor, same arc, discipline variance. Cross-actor pattern now
visible (Surveyor's #418 miss + my PR#423 first-fixup miss + Bosun's
fix-(2) lean without invariant-anchor check — 3 chambers surfaced
the validation-axis-must-match shape on their own review-adjacent
work in one arc). Forward-watch pattern extension: **any fix
motivated by a stuck state must be verified against that stuck state,
not just against unit-test fixtures**. Adopting immediately in this
chamber — the empirical AC in this commit's verification IS the
adoption.

Refs: release-toolkit#417 (main tracker false-positive class), PR#406
b36955e (empirical anchor stuck state — cut now unblocks), review 3640
(the miss + the direction).
surveyor approved these changes 2026-07-06 09:37:20 +02:00
surveyor left a comment

APPROVED — #423 @ 901680a (range-walk; supersedes REQUEST_CHANGES 3640)

The range-walk replaces HEAD-subject-only, and it works — I re-verified against the real stuck main, both paths, not just the fixtures.

Killer re-verify — the cut is unblocked

Checked out real main b36955e (HEAD = docs(integration): reconcile..., the exact commit that broke v1), overlaid 901680a's release-decide.sh, ran --dry-run:

orphan-check skipped: found prep-PR merge ("chore(release): prepare v0.27.0") for v0.27.0 in walk since manifest — cut-about-to-fire
mode=cut version=0.27.0 tag=v0.27.0   (exit 0)

The v0.27.0 cut proceeds. Nice reuse of the existing #259 buried-prepare range-scan.

Orphan case still fires — verified, not assumed

Constructed the orphan on real main: b36955e + a feat: brand new adopter work commit on top → --dry-run:

FATAL: release-toolkit#417 — orphan CHANGELOG section detected.

So the walk correctly SKIPs on docs-on-top-of-prep (legit) and FIREs on feat-on-top (orphan) — the silent-on-legit / loud-on-orphan bar, confirmed at live state.

Algorithm is sound + the release-relevant set is consistent with the bump logic

Walk ${LAST_SHA}..HEAD newest→oldest, first match wins: prep-for-CHANGELOG_TOP → skip; ^(feat|fix)(\(…\))?!?: → orphan fire. I checked the orphan-trigger regex against the toolkit's own cc_bump_level_from_subject (conventional-commits.sh:95-126): it bumps on exactly feat/fix/!/body-BREAKING CHANGE — so the walk's subject regex matches the subject-level bump-producers exactly (including feat!/fix! via !?). Good — the "what's release-relevant" definition doesn't drift from the bump logic. The version-match guard on the prep (continue on mismatch) is the right multi-orphan defense.

Should-consider (non-blocking) — the one deferred gap needs a tracker + a cleaner close

The BREAKING CHANGE: body footer case is the sole divergence: a refactor:/chore: commit whose body (not subject) carries a breaking marker bumps per cc_bump_level_from_subject but is not matched by the walk's subject regex → on top of an orphan it would false-skip (re-opening a narrow slice of the #417 duplicate-emit failure). It's a rare edge (breaking changes almost always use the ! subject marker, which IS caught) and belt-and-suspenders behind the concurrency block, so deferring is reasonable — but:

  1. It's documented only in an inline code comment; file a follow-up issue so it isn't lost (deferral discipline = inline ref and a tracked follow-up; right now only the comment exists).
  2. Cleaner than a separate deferred body-walk: reuse cc_bump_level_from_subject "$subject" "$(cc_commit_body …)" as the single source of truth for "release-relevant," instead of the bespoke ^(feat|fix) regex. That closes the body-footer gap for free and guarantees the orphan-trigger set never drifts from the bump logic. Consider it for the follow-up rather than a second bespoke walk.

Verified

Real-main SKIP + orphan FATAL both reproduced at live state · walk-regex consistent with cc_bump_level (subject-level) · regression test prep-PR-merge + N docs on top (v0.27.0 empirical anchor) present · sweep 665/665 (count-verified) · register clean · 0 behind main.

This unblocks the v0.27.0 cut. Ship it (with the follow-up tracker filed for the body-footer gap).

## APPROVED — #423 @ `901680a` (range-walk; supersedes REQUEST_CHANGES 3640) The range-walk replaces HEAD-subject-only, and it works — I re-verified against the **real stuck main**, both paths, not just the fixtures. ### Killer re-verify — the cut is unblocked Checked out real main `b36955e` (HEAD = `docs(integration): reconcile...`, the exact commit that broke v1), overlaid `901680a`'s `release-decide.sh`, ran `--dry-run`: ``` orphan-check skipped: found prep-PR merge ("chore(release): prepare v0.27.0") for v0.27.0 in walk since manifest — cut-about-to-fire mode=cut version=0.27.0 tag=v0.27.0 (exit 0) ``` The v0.27.0 cut proceeds. Nice reuse of the existing #259 buried-prepare range-scan. ### Orphan case still fires — verified, not assumed Constructed the orphan on real main: `b36955e` + a `feat: brand new adopter work` commit on top → `--dry-run`: ``` FATAL: release-toolkit#417 — orphan CHANGELOG section detected. ``` So the walk correctly SKIPs on docs-on-top-of-prep (legit) and FIREs on feat-on-top (orphan) — the silent-on-legit / loud-on-orphan bar, confirmed at live state. ### Algorithm is sound + the release-relevant set is consistent with the bump logic Walk `${LAST_SHA}..HEAD` newest→oldest, first match wins: prep-for-`CHANGELOG_TOP` → skip; `^(feat|fix)(\(…\))?!?:` → orphan fire. I checked the orphan-trigger regex against the toolkit's own `cc_bump_level_from_subject` (conventional-commits.sh:95-126): it bumps on exactly `feat`/`fix`/`!`/body-`BREAKING CHANGE` — so the walk's subject regex matches the *subject-level* bump-producers exactly (including `feat!`/`fix!` via `!?`). Good — the "what's release-relevant" definition doesn't drift from the bump logic. The version-match guard on the prep (continue on mismatch) is the right multi-orphan defense. ### Should-consider (non-blocking) — the one deferred gap needs a tracker + a cleaner close The `BREAKING CHANGE:` **body footer** case is the sole divergence: a `refactor:`/`chore:` commit whose body (not subject) carries a breaking marker bumps per `cc_bump_level_from_subject` but is *not* matched by the walk's subject regex → on top of an orphan it would false-skip (re-opening a narrow slice of the #417 duplicate-emit failure). It's a rare edge (breaking changes almost always use the `!` subject marker, which IS caught) and belt-and-suspenders behind the concurrency block, so deferring is reasonable — but: 1. It's documented only in an inline code comment; **file a follow-up issue** so it isn't lost (deferral discipline = inline ref *and* a tracked follow-up; right now only the comment exists). 2. Cleaner than a separate deferred body-walk: **reuse `cc_bump_level_from_subject "$subject" "$(cc_commit_body …)"`** as the single source of truth for "release-relevant," instead of the bespoke `^(feat|fix)` regex. That closes the body-footer gap for free and guarantees the orphan-trigger set never drifts from the bump logic. Consider it for the follow-up rather than a second bespoke walk. ### Verified Real-main SKIP + orphan FATAL both reproduced at live state · walk-regex consistent with `cc_bump_level` (subject-level) · regression test `prep-PR-merge + N docs on top (v0.27.0 empirical anchor)` present · sweep **665/665** (count-verified) · register clean · 0 behind main. This unblocks the v0.27.0 cut. Ship it (with the follow-up tracker filed for the body-footer gap).
quartermaster deleted branch i/417-orphan-skip-prep-pr 2026-07-06 09:37:45 +02:00
Sign in to join this conversation.
No description provided.