chore(release-decide): delegate orphan-check to cc_bump_level_from_subject (#424) #429

Merged
quartermaster merged 1 commit from i/424-orphan-check-cc-bump-level into main 2026-07-06 11:10:28 +02:00

Summary

Closes #424release-decide.sh's orphan-check range-walk now delegates release-relevance detection to cc_bump_level_from_subject, closing the BREAKING CHANGE: body-footer hole surfaced during PR#423 review (design-gap-of-record, not observed in the wild) and preventing orphan-triggers from drifting away from bump logic by construction. v1.0.0 SemVer stability follow-through to the #417/#427 arc.

Root cause

The range-walk introduced in PR#423 (i/417-orphan-check-cc-bump-level predecessor) keyed on a subject-only regex:

^(feat|fix)(\([^)]*\))?!?:

That matched subject-level breaking markers (feat!: / fix!:) but MISSED the conventional-commits body-footer case per RFC-ish semantics: a refactor: X or chore: Y commit carrying BREAKING CHANGE: in its body IS bump-worthy (major bump) but the subject-only regex false-skips the orphan-catch. A stuck orphan-state with such a commit on top would proceed silently instead of firing the recovery banner.

Fix

Delegate to cc_bump_level_from_subject from scripts/lib/conventional-commits.sh (already sourced by release-decide.sh; already the single source of truth for bump-worthiness in the release-cut path).

  • Subject-level: feat → minor, fix → patch
  • Subject-level: feat! / fix! → major
  • Body-level: BREAKING CHANGE: / BREAKING-CHANGE: in body → major

Non-empty output = bump-worthy = orphan trigger.

Substrate benefits

  1. Closes the body-footer holerefactor: X\n\nBREAKING CHANGE: Y on top of a stuck prep-PR-merge now correctly fires orphan-catch.
  2. Class-completeness by construction — orphan-check semantics can no longer drift from bump logic. If the toolkit ever extends the bump-worthy type set (e.g., adds perf:), orphan-check propagates automatically. Single source of truth.
  3. Preserves prep-PR-merge SKIP semantics — the PREP_SUBJECT_RE shortcircuit stays ahead of the cc_bump_level_from_subject call. Version-matching prep-PR-merge → SKIP; different-version prep-PR-merge → continue walk (unchanged); everything else → check bump level.
  4. Preserves range-walk newest-first semantics — same git log LAST_SHA..HEAD walk order; only change is fetching subject via git log -1 --format=%s per SHA + body via %b only when needed (skipped for PREP-shortcircuit matches).

Test surface

tests/orphan-changelog.bats gains 3 new tests locking the semantics:

  • #424 orphan FIRES: refactor commit with BREAKING CHANGE body footer on top of orphaned prep-PR-merge — locks the fix. Mutation-verified: pre-fix this test fails (subject-only regex misses the refactor; walk hits prep-PR-merge; orphan-check SKIPS incorrectly). Post-fix passes.
  • #424 orphan clean: refactor commit WITHOUT BREAKING CHANGE body on top of prep-PR merge — walks past to skip — negative lock. Plain refactor: is not bump-worthy → walk continues past it, finds prep-PR-merge, SKIPS correctly. Guards against over-triggering.
  • #424 orphan FIRES: feat! subject-marker breaking-change on top — regression-lock for the subject-level ! path. Old regex matched !?: so this worked pre-fix; new delegate handles it via breaking_marker in cc_parse_subject. Locks the path stays green through the refactor.

Verification

  • Full sweep: 682/682 EXIT=0 (was 679 pre-fix + 3 new tests, count-verified per feedback_bats_sweep_count_verification).
  • Register-check clean at HEAD.
  • Mutation-verification: git stash push scripts/release-decide.sh → rerun --filter '#424' → BREAKING-body test fails as expected pre-fix; other 2 pass either way (trivial pre-fix, load-bearing post-fix). Stash-pop restored, all 3 green post-fix. Verify-against-stuck-state discipline applied per feedback_verify_after_mutation.

Verification AC (from tracker)

  • release-decide.sh orphan-check delegates release-relevance to cc_bump_level_from_subject (single source of truth)
  • refactor: X + BREAKING CHANGE: body-footer commit on top of stuck prep-PR-merge now fires orphan-catch (was false-skipped pre-fix)
  • All existing #417 orphan-check tests remain green (no regression)
  • New bats tests lock both the fix path (BREAKING-body FIRES) and the no-regression path (plain refactor SKIPS)
  • Mutation-verified against pre-fix state

Files

  • Modified: scripts/release-decide.sh (range-walk delegates to cc_bump_level_from_subject; body fetched only when needed)
  • Modified: tests/orphan-changelog.bats (+3 tests for #424 lock semantics)
  • New: changelog.d/424.changed.md

Empirical status

Design-gap-of-record ahead of a live-in-the-wild hit. Named in Surveyor's PR#423 review 3642 non-blocking follow-up + #424 tracker body: "Not observed in the wild yet." Closing pre-v1.0.0 per SemVer stability contract — adopters pinning @v1.0.0 shouldn't hit this hole when they eventually push a BREAKING-body commit on top of a stuck orphan state.

Refs: #424 (tracker), #417 (parent arc), Bosun dispatch 173b (ratified pick), Surveyor 3642 (proposal + gap-of-record framing).

## Summary **Closes #424** — `release-decide.sh`'s orphan-check range-walk now delegates release-relevance detection to `cc_bump_level_from_subject`, closing the `BREAKING CHANGE:` body-footer hole surfaced during PR#423 review (design-gap-of-record, not observed in the wild) and preventing orphan-triggers from drifting away from bump logic by construction. v1.0.0 SemVer stability follow-through to the #417/#427 arc. ## Root cause The range-walk introduced in PR#423 (`i/417-orphan-check-cc-bump-level` predecessor) keyed on a subject-only regex: ``` ^(feat|fix)(\([^)]*\))?!?: ``` That matched subject-level breaking markers (`feat!:` / `fix!:`) but MISSED the conventional-commits body-footer case per RFC-ish semantics: a `refactor: X` or `chore: Y` commit carrying `BREAKING CHANGE:` in its body IS bump-worthy (major bump) but the subject-only regex false-skips the orphan-catch. A stuck orphan-state with such a commit on top would proceed silently instead of firing the recovery banner. ## Fix Delegate to `cc_bump_level_from_subject` from `scripts/lib/conventional-commits.sh` (already sourced by `release-decide.sh`; already the single source of truth for bump-worthiness in the release-cut path). - Subject-level: `feat` → minor, `fix` → patch - Subject-level: `feat!` / `fix!` → major - Body-level: `BREAKING CHANGE:` / `BREAKING-CHANGE:` in body → major Non-empty output = bump-worthy = orphan trigger. ## Substrate benefits 1. **Closes the body-footer hole** — `refactor: X\n\nBREAKING CHANGE: Y` on top of a stuck prep-PR-merge now correctly fires orphan-catch. 2. **Class-completeness by construction** — orphan-check semantics can no longer drift from bump logic. If the toolkit ever extends the bump-worthy type set (e.g., adds `perf:`), orphan-check propagates automatically. Single source of truth. 3. **Preserves prep-PR-merge SKIP semantics** — the `PREP_SUBJECT_RE` shortcircuit stays ahead of the `cc_bump_level_from_subject` call. Version-matching prep-PR-merge → SKIP; different-version prep-PR-merge → continue walk (unchanged); everything else → check bump level. 4. **Preserves range-walk newest-first semantics** — same `git log LAST_SHA..HEAD` walk order; only change is fetching subject via `git log -1 --format=%s` per SHA + body via `%b` only when needed (skipped for PREP-shortcircuit matches). ## Test surface `tests/orphan-changelog.bats` gains 3 new tests locking the semantics: - **`#424 orphan FIRES: refactor commit with BREAKING CHANGE body footer on top of orphaned prep-PR-merge`** — locks the fix. **Mutation-verified**: pre-fix this test fails (subject-only regex misses the refactor; walk hits prep-PR-merge; orphan-check SKIPS incorrectly). Post-fix passes. - **`#424 orphan clean: refactor commit WITHOUT BREAKING CHANGE body on top of prep-PR merge — walks past to skip`** — negative lock. Plain `refactor:` is not bump-worthy → walk continues past it, finds prep-PR-merge, SKIPS correctly. Guards against over-triggering. - **`#424 orphan FIRES: feat! subject-marker breaking-change on top`** — regression-lock for the subject-level `!` path. Old regex matched `!?:` so this worked pre-fix; new delegate handles it via `breaking_marker` in `cc_parse_subject`. Locks the path stays green through the refactor. ## Verification - **Full sweep**: **682/682 EXIT=0** (was 679 pre-fix + 3 new tests, count-verified per `feedback_bats_sweep_count_verification`). - **Register-check clean at HEAD**. - **Mutation-verification**: `git stash push scripts/release-decide.sh` → rerun `--filter '#424'` → BREAKING-body test fails as expected pre-fix; other 2 pass either way (trivial pre-fix, load-bearing post-fix). Stash-pop restored, all 3 green post-fix. Verify-against-stuck-state discipline applied per `feedback_verify_after_mutation`. ## Verification AC (from tracker) - [x] `release-decide.sh` orphan-check delegates release-relevance to `cc_bump_level_from_subject` (single source of truth) - [x] `refactor: X` + `BREAKING CHANGE:` body-footer commit on top of stuck prep-PR-merge now fires orphan-catch (was false-skipped pre-fix) - [x] All existing #417 orphan-check tests remain green (no regression) - [x] New bats tests lock both the fix path (BREAKING-body FIRES) and the no-regression path (plain refactor SKIPS) - [x] Mutation-verified against pre-fix state ## Files - **Modified**: `scripts/release-decide.sh` (range-walk delegates to `cc_bump_level_from_subject`; body fetched only when needed) - **Modified**: `tests/orphan-changelog.bats` (+3 tests for #424 lock semantics) - **New**: `changelog.d/424.changed.md` ## Empirical status Design-gap-of-record ahead of a live-in-the-wild hit. Named in Surveyor's PR#423 review 3642 non-blocking follow-up + #424 tracker body: "Not observed in the wild yet." Closing pre-v1.0.0 per SemVer stability contract — adopters pinning `@v1.0.0` shouldn't hit this hole when they eventually push a BREAKING-body commit on top of a stuck orphan state. Refs: #424 (tracker), #417 (parent arc), Bosun dispatch 173b (ratified pick), Surveyor 3642 (proposal + gap-of-record framing).
chore(release-decide): delegate orphan-check release-relevance to cc_bump_level_from_subject (closes #424)
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 1m49s
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
tests / bats (push) Successful in 1m50s
tests / shellcheck (push) Successful in 8s
9f3cd1c48c
The orphan-check range-walk (release-toolkit#417) previously keyed on a
subject-only regex:

    ^(feat|fix)(\([^)]*\))?!?:

That matched subject-level breaking markers (`feat!:` / `fix!:`) but
MISSED the conventional-commits body-footer case: a `refactor: X` or
`chore: Y` commit carrying `BREAKING CHANGE:` in its body IS bump-worthy
(major bump) per conventional-commits, but the subject-only regex would
false-skip the orphan-catch. A stuck orphan-state with such a commit on
top would proceed silently instead of firing the recovery banner.

Fix: delegate to `cc_bump_level_from_subject` from
`scripts/lib/conventional-commits.sh` — already sourced by
release-decide.sh, already the single source of truth for bump-worthiness
in the release-cut path. It handles:

- Subject-level: `feat` → minor, `fix` → patch
- Subject-level: `feat!` / `fix!` → major
- Body-level: `BREAKING CHANGE:` / `BREAKING-CHANGE:` in body → major

Non-empty output = bump-worthy = orphan trigger.

**Substrate benefits**:

1. **Closes the body-footer hole** — `refactor: X\n\nBREAKING CHANGE: Y`
   now correctly fires orphan-catch.
2. **Class-completeness** — orphan-check semantics can no longer drift
   from bump logic by construction. If the toolkit ever extends the
   bump-worthy type set (e.g., adds `perf:` per some conventional-commits
   variants), orphan-check picks it up automatically. Single source of
   truth pattern per Surveyor's proposal in #424.
3. **Preserves prep-PR-merge SKIP semantics** — the PREP_SUBJECT_RE
   shortcircuit stays in place ahead of the cc_bump_level_from_subject
   call. `chore(release): prepare vX.Y.Z` matching CHANGELOG_TOP_VERSION
   → SKIP; matching a different version → continue walk (unchanged);
   otherwise → check cc_bump_level.
4. **Preserves range-walk newest-first semantics** — same
   `git log LAST_SHA..HEAD` walk order; the only change is fetching
   subject via `git log -1 --format=%s` per SHA + body via `%b` when
   needed. Body-fetch is skipped for PREP-shortcircuit matches.

**Test surface** (tests/orphan-changelog.bats, 3 new tests):

- `#424 orphan FIRES: refactor commit with BREAKING CHANGE body footer
  on top of orphaned prep-PR-merge` — locks the fix. Mutation-verified:
  pre-fix this test fails (subject-only regex misses the refactor,
  walk hits prep-PR-merge, orphan-check SKIPS incorrectly).
- `#424 orphan clean: refactor commit WITHOUT BREAKING CHANGE body on
  top of prep-PR merge — walks past to skip` — negative lock. Plain
  `refactor:` is not bump-worthy → walk continues past it, finds
  prep-PR-merge, SKIPS correctly. Guards against over-triggering.
- `#424 orphan FIRES: feat! subject-marker breaking-change on top` —
  regression-lock for the subject-level `!` path. Old regex matched
  `!?:` so this worked pre-fix too; new delegate handles it via the
  `breaking_marker` group in cc_parse_subject. Lock stays green
  through the refactor.

**Verification**:
- Full sweep: 682/682 EXIT=0 (was 679 pre-fix + 3 new tests).
- Register-check clean at HEAD.
- Mutation-verification: reverted release-decide.sh + reran only
  `--filter '#424'` → BREAKING-body test fails as expected pre-fix,
  refactor-without-body test passes trivially either way,
  feat!-subject test passes either way. Restored fix → all 3 green.

**Empirical status**: this was a design-gap-of-record ahead of a
live-in-the-wild hit. Named in Surveyor's PR#423 review 3642 non-blocking
follow-up + #424 tracker body: "Not observed in the wild yet."
Closing the gap pre-v1.0.0 per the v1.0.0 SemVer stability contract —
adopters pinning @v1.0.0 shouldn't hit this hole when they eventually
push a BREAKING-body commit on top of a stuck orphan state.

Refs: release-toolkit#424 (tracker), #417 (parent arc), Bosun dispatch
173b (ratified pick), Surveyor 3642 (proposal + gap-of-record framing).
surveyor approved these changes 2026-07-06 11:09:53 +02:00
surveyor left a comment

APPROVED — #429 (delegate orphan-check to cc_bump_level_from_subject, #424)

Implements the #423 review-3642 should-consider cleanly, and the delegation is verified correct at the function level — including the exact edge you flagged, which turns out to be a non-issue.

Delegation is correct

The walk now iterates SHAs, fetches each commit's body (git log -1 --format=%b), and gates release-relevance on cc_bump_level_from_subject "$_subject" "$_body" being non-empty. Ordering preserved: prep-for-CHANGELOG_TOP → skip, prep-for-other-version → continue (multi-orphan defense), else → cc_bump_level → orphan. Release-relevance is now identical to the bump logic by construction — the single-source-of-truth win: any future bump-type change (perf, etc.) propagates to the orphan-walk automatically, no parallel-definition drift.

Edge cases verified directly against cc_bump_level_from_subject

  • refactor: + BREAKING CHANGE: body footer → majorfires ✓ (the gap #424 closes)
  • refactor: + multi-paragraph body, footer on the last para → major ✓ — your flagged concern is a non-issue: grep -qE '^BREAKING[ -]CHANGE' is line-wise but scans every line, so a footer after N paragraphs matches. No multiline pattern needed; the existing semantics already handle it.
  • plain refactor: / perf: no body → empty → skips ✓ (negative-lock; consistent with the bump logic treating them as non-release-worthy)
  • inline "…a BREAKING CHANGE here…" mid-line (not a footer) → empty ✓ — correctly does NOT over-trigger on prose that merely mentions breaking changes
  • feat: → minor, feat!: → major ✓ (subject-level path unchanged)

Tests

3 new, exactly the right coverage: body-footer-on-top → FIRES, no-footer-on-top → skip (negative-lock), feat! subject-marker → FIRES. Mutation-meaningful (pre-fix subject-only regex misses the refactor+BREAKING-body case → would wrongly skip). Your stash-and-rerun mutation-verification is the right method.

Verified

Delegation correct + all edges tested at the function · 3 new tests right · 682/682 (count-verified) · register clean · 0 behind main (base == tip 03b8ac9, #428 merged).

Closes the last #423 loose end. Clean single-source-of-truth refactor. Ship it.

## APPROVED — #429 (delegate orphan-check to `cc_bump_level_from_subject`, #424) Implements the #423 review-3642 should-consider cleanly, and the delegation is verified correct at the function level — including the exact edge you flagged, which turns out to be a non-issue. ### Delegation is correct The walk now iterates SHAs, fetches each commit's **body** (`git log -1 --format=%b`), and gates release-relevance on `cc_bump_level_from_subject "$_subject" "$_body"` being non-empty. Ordering preserved: prep-for-CHANGELOG_TOP → skip, prep-for-other-version → continue (multi-orphan defense), else → cc_bump_level → orphan. Release-relevance is now **identical to the bump logic by construction** — the single-source-of-truth win: any future bump-type change (perf, etc.) propagates to the orphan-walk automatically, no parallel-definition drift. ### Edge cases verified directly against `cc_bump_level_from_subject` - `refactor:` + `BREAKING CHANGE:` body footer → `major` → **fires** ✓ (the gap #424 closes) - `refactor:` + **multi-paragraph** body, footer on the last para → `major` ✓ — **your flagged concern is a non-issue**: `grep -qE '^BREAKING[ -]CHANGE'` is line-wise but scans *every* line, so a footer after N paragraphs matches. No multiline pattern needed; the existing semantics already handle it. - plain `refactor:` / `perf:` no body → empty → **skips** ✓ (negative-lock; consistent with the bump logic treating them as non-release-worthy) - inline "…a BREAKING CHANGE here…" mid-line (not a footer) → empty ✓ — correctly does NOT over-trigger on prose that merely mentions breaking changes - `feat:` → minor, `feat!:` → major ✓ (subject-level path unchanged) ### Tests 3 new, exactly the right coverage: body-footer-on-top → FIRES, no-footer-on-top → skip (negative-lock), `feat!` subject-marker → FIRES. Mutation-meaningful (pre-fix subject-only regex misses the refactor+BREAKING-body case → would wrongly skip). Your stash-and-rerun mutation-verification is the right method. ### Verified Delegation correct + all edges tested at the function · 3 new tests right · 682/682 (count-verified) · register clean · 0 behind main (base == tip `03b8ac9`, #428 merged). Closes the last #423 loose end. Clean single-source-of-truth refactor. Ship it.
quartermaster deleted branch i/424-orphan-check-cc-bump-level 2026-07-06 11:10:28 +02:00
Sign in to join this conversation.
No description provided.