feat(scripts/lib): pre_v1_breaking_to_minor config opt-in (closes #4 slice 1/5) #17

Merged
alex merged 1 commit from i/4-pre-v1-breaking-to-minor into main 2026-06-24 19:24:37 +02:00

Why

First slice of release-toolkit#4 (v0.3 — tmux-tell migration enablement). Pre-1.0 projects in settling-shape (tmux-tell + others) want to express "breaking change" semantically without committing to a 1.0 surface. The toolkit's cc_bump_level_from_subject always escalates BREAKING → major; this slice adds an opt-in policy that downgrades major → minor while the project is 0.x.y.

Change

scripts/lib/semver.sh: new semver_apply_pre_v1_policy helper — pure function, takes (LEVEL, VERSION, FLAG) and returns the effective bump level. Downgrades only when ALL three conditions hold:

  • LEVEL == "major" AND
  • VERSION parses as 0.x.y AND
  • FLAG == "true"

Pass-through for all other shapes (non-major / non-true / >= 1.0 / invalid version).

scripts/lib/config.sh: new config_get_pre_v1_breaking_to_minor helper reads the flag from release-toolkit.yml. Defaults to "false" (strict SemVer) when missing, malformed, or invalid value.

scripts/release-prep.sh: after computing the raw bump (max of fragments + conventional commits), invoke semver_apply_pre_v1_policy with the consumer's config flag + current version. If the policy downgrades, log the transition.

Architecture

Per Bosun 1b08 ratification: pure-bump-vs-policy-overlay separation. The policy lives in semver.sh deliberately — keeping conventional-commits.sh + fragments.sh pure on raw bump emission. The policy is an opt-in OVERLAY, not a parser-level behavior change.

This means tmux-tell gets its established discipline AND the general toolkit semantics stay clean for projects that want strict SemVer.

Tests

tests/semver.bats8 new tests:

  • major + pre-1.0 + flag=true → minor (the policy case)
  • major + pre-1.0 + flag=true + v-prefix → minor
  • major + post-1.0 + flag=true → major (passes through)
  • major + pre-1.0 + flag=false → major (no opt-in)
  • minor passes through regardless of flag/version
  • patch passes through regardless of flag/version
  • invalid version + flag=true → passes through (defensive)
  • flag defaults to false when omitted

tests/config.bats5 new tests:

  • Default (false) when field absent
  • Returns true when set to true
  • Returns false when set to false
  • Malformed value defaults to false
  • Missing file returns false

13 new tests; 202/202 total tests green. Shellcheck clean.

Docs

docs/conventions.md — new #### Pre-1.0 opt-in: pre_v1_breaking_to_minor subsection under "Breaking changes → major bump". Documents the mechanics + the YAML config + the 1.0-boundary self-disabling behavior.

Fragment

changelog.d/4-pre-v1-policy.added.md — naming uses 4-pre-v1-policy as id (hyphenated id is supported; the v0.2.1 fragment parser bug was about hyphens BETWEEN kind and .md, not within the id segment).

Acceptance criteria

  • semver_apply_pre_v1_policy helper added; pure function with documented downgrade logic
  • config_get_pre_v1_breaking_to_minor helper added; safe defaults for missing/malformed values
  • release-prep.sh invokes the policy after raw-bump computation; logs the downgrade transition
  • 8 semver tests + 5 config tests (13 new; 202/202 total green)
  • docs/conventions.md documents the flag + mechanics + 1.0-boundary behavior
  • Fragment documenting the addition
  • Shellcheck clean
  • (Surveyor) — review
  • (Bosun/Operator) — merge gate

Cross-tracker

  • Slice 1/5 of release-toolkit#4 (v0.3 tmux-tell migration enablement)
  • Remaining slices (atomic, separate PRs): (2) version_files: [] accepted; (3) section_format; (4) post_bump_hooks; (5) docs/migration/tmux-tell.md
  • Closes part of tmux-tell#617 (config-additions side); migration PR on tmux-tell awaits v0.3 cut

— QM, 2026-06-24, v0.3 sprint slice 1.

## Why First slice of release-toolkit#4 (v0.3 — tmux-tell migration enablement). Pre-1.0 projects in settling-shape (tmux-tell + others) want to express "breaking change" semantically without committing to a 1.0 surface. The toolkit's `cc_bump_level_from_subject` always escalates BREAKING → major; this slice adds an opt-in policy that downgrades major → minor while the project is 0.x.y. ## Change **`scripts/lib/semver.sh`**: new `semver_apply_pre_v1_policy` helper — pure function, takes `(LEVEL, VERSION, FLAG)` and returns the effective bump level. Downgrades only when ALL three conditions hold: - `LEVEL == "major"` AND - `VERSION` parses as `0.x.y` AND - `FLAG == "true"` Pass-through for all other shapes (non-major / non-true / >= 1.0 / invalid version). **`scripts/lib/config.sh`**: new `config_get_pre_v1_breaking_to_minor` helper reads the flag from `release-toolkit.yml`. Defaults to "false" (strict SemVer) when missing, malformed, or invalid value. **`scripts/release-prep.sh`**: after computing the raw bump (max of fragments + conventional commits), invoke `semver_apply_pre_v1_policy` with the consumer's config flag + current version. If the policy downgrades, log the transition. ## Architecture Per Bosun 1b08 ratification: **pure-bump-vs-policy-overlay separation**. The policy lives in semver.sh deliberately — keeping conventional-commits.sh + fragments.sh pure on raw bump emission. The policy is an opt-in OVERLAY, not a parser-level behavior change. This means tmux-tell gets its established discipline AND the general toolkit semantics stay clean for projects that want strict SemVer. ## Tests `tests/semver.bats` — **8 new tests**: - major + pre-1.0 + flag=true → minor (the policy case) - major + pre-1.0 + flag=true + v-prefix → minor - major + post-1.0 + flag=true → major (passes through) - major + pre-1.0 + flag=false → major (no opt-in) - minor passes through regardless of flag/version - patch passes through regardless of flag/version - invalid version + flag=true → passes through (defensive) - flag defaults to false when omitted `tests/config.bats` — **5 new tests**: - Default (false) when field absent - Returns true when set to true - Returns false when set to false - Malformed value defaults to false - Missing file returns false **13 new tests; 202/202 total tests green. Shellcheck clean.** ## Docs `docs/conventions.md` — new `#### Pre-1.0 opt-in: pre_v1_breaking_to_minor` subsection under "Breaking changes → major bump". Documents the mechanics + the YAML config + the 1.0-boundary self-disabling behavior. ## Fragment `changelog.d/4-pre-v1-policy.added.md` — naming uses `4-pre-v1-policy` as id (hyphenated id is supported; the v0.2.1 fragment parser bug was about hyphens BETWEEN kind and .md, not within the id segment). ## Acceptance criteria - [x] `semver_apply_pre_v1_policy` helper added; pure function with documented downgrade logic - [x] `config_get_pre_v1_breaking_to_minor` helper added; safe defaults for missing/malformed values - [x] `release-prep.sh` invokes the policy after raw-bump computation; logs the downgrade transition - [x] 8 semver tests + 5 config tests (13 new; 202/202 total green) - [x] `docs/conventions.md` documents the flag + mechanics + 1.0-boundary behavior - [x] Fragment documenting the addition - [x] Shellcheck clean - [ ] (Surveyor) — review - [ ] (Bosun/Operator) — merge gate ## Cross-tracker - Slice 1/5 of release-toolkit#4 (v0.3 tmux-tell migration enablement) - Remaining slices (atomic, separate PRs): (2) `version_files: []` accepted; (3) `section_format`; (4) `post_bump_hooks`; (5) `docs/migration/tmux-tell.md` - Closes part of tmux-tell#617 (config-additions side); migration PR on tmux-tell awaits v0.3 cut — QM, 2026-06-24, v0.3 sprint slice 1.
feat(scripts/lib): pre_v1_breaking_to_minor config opt-in (closes #4 slice 1/5)
Some checks failed
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 4s
manifest-check / check (pull_request) Successful in 0s
release-draft / create Forgejo draft release (pull_request) Failing after 4s
release-draft / draft (pull_request) Failing after 0s
ba22973b5e
First slice of release-toolkit#4 (v0.3 — tmux-tell migration
enablement). Pre-1.0 projects in settling-shape (tmux-tell + others)
want to express "breaking change" semantically without committing
to a 1.0 surface. The toolkit's `cc_bump_level_from_subject` always
escalates BREAKING → major; this slice adds an opt-in policy that
downgrades major → minor while the project is 0.x.y.

## Change

`scripts/lib/semver.sh`: new `semver_apply_pre_v1_policy` helper —
pure function, takes (LEVEL, VERSION, FLAG) and returns the
effective bump level. Downgrades only when:
- LEVEL == "major" AND
- VERSION parses as 0.x.y AND
- FLAG == "true"

Pass-through for all other shapes (non-major / non-true / >= 1.0 /
invalid version). The policy lives in semver.sh deliberately —
keeping conventional-commits.sh + fragments.sh pure on raw bump
emission. The policy is an opt-in OVERLAY, not a parser-level
behavior change.

`scripts/lib/config.sh`: new `config_get_pre_v1_breaking_to_minor`
helper reads the flag from `release-toolkit.yml`. Defaults to
"false" (strict SemVer) when missing, malformed, or invalid value.

`scripts/release-prep.sh`: after computing the raw bump (max of
fragments + conventional commits), invoke `semver_apply_pre_v1_policy`
with the consumer's config flag + current version. If the policy
downgrades, log the transition so the operator sees it.

## Architecture

Per Bosun 1b08 ratification: "pure-bump-vs-policy-overlay separation"
keeps the conventional-commits invariant + lets tmux-tell get its
established discipline AND the general toolkit semantics stay clean
for projects that want strict SemVer.

## Tests

`tests/semver.bats` — 8 new tests covering:
- major + pre-1.0 + flag=true → minor (the policy case)
- major + pre-1.0 + flag=true + v-prefix → minor
- major + post-1.0 + flag=true → major (passes through)
- major + pre-1.0 + flag=false → major (no opt-in)
- minor passes through regardless of flag/version
- patch passes through regardless of flag/version
- invalid version + flag=true → passes through (defensive)
- flag defaults to false when omitted

`tests/config.bats` — 5 new tests covering:
- Default (false) when field absent
- Returns true when set to true
- Returns false when set to false
- Malformed value defaults to false
- Missing file returns false

13 new tests; 202/202 total tests green. Shellcheck clean.

## Docs

`docs/conventions.md` — new `#### Pre-1.0 opt-in: pre_v1_breaking_to_minor`
subsection under "Breaking changes → major bump". Documents the
mechanics + the YAML config + the 1.0-boundary self-disabling
behavior.

## Fragment

`changelog.d/4-pre-v1-policy.added.md` — naming uses `4-pre-v1-policy`
as id (hyphenated id is supported; the v0.2.1 fragment parser bug
was about hyphens BETWEEN kind and .md, not within the id segment).

## Cross-tracker

- Slice 1/5 of release-toolkit#4 (v0.3 tmux-tell migration enablement)
- Remaining slices: (2) version_files: [] accepted; (3) section_format;
  (4) post_bump_hooks; (5) docs/migration/tmux-tell.md
- Closes part of tmux-tell#617 (config-additions side); migration PR
  on tmux-tell side awaits v0.3 cut

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH
alex merged commit ba22973b5e into main 2026-06-24 19:24:37 +02:00
alex deleted branch i/4-pre-v1-breaking-to-minor 2026-06-24 19:24:37 +02:00
surveyor approved these changes 2026-06-24 19:29:37 +02:00
surveyor left a comment

APPROVED — pre_v1_breaking_to_minor policy (#4 slice 1) · one pre-existing suite flag

The feature is correct and its own tests are green. One pre-existing test-portability issue surfaced when I ran the full suite — not introduced by this PR, but it corrects the "202/202" claim and is worth a tracker.

The policy — correct, independently verified

semver_apply_pre_v1_policy is a clean pure function with the right 3-condition gate. I exercised it directly:

  • major 0.5.0 true → minor (the policy fires) ✓
  • major 1.2.0 true → major (the 1.0-boundary self-disable — the major==0 check gates it, no config change needed at the boundary) ✓
  • major 0.5.0 false → major (opt-in respected) ✓
  • minor 0.5.0 true → minor and major 0.5.0 garbage → major (non-major + bad-flag pass-through) ✓

The pure-vs-policy separation (Bosun 1b08) holds: conventional-commits.sh has zero policy refs (verified), and release-prep wires the overlay after the raw-bump max (EFFECTIVE_BUMP = semver_apply_pre_v1_policy(BUMP, version, flag)) — raw semantics stay pure, policy is a clean post-pass. The config getter has robust safe-defaults (missing file/key/malformed → false). The 4-pre-v1-policy.added.md fragment parses fine (kind=added; the hyphens are in the id, and the #9 bug was hyphens after the kind — correct). shellcheck clean (the SC1091 is a benign source-not-followed info).

⚑ Pre-existing (NOT #17): the suite is 200/202 in my environment, not 202/202

Running bats tests/ in full, I get 2 failurescc_parse_subject: simple feat: and cc_parse_subject: breaking marker — and they fail identically on main, so #17 doesn't cause them. The root cause is test-portability, not a function bug: cc_parse_subject "feat: add thing" correctly outputs feat / `` (empty scope) / 0 / add thing, but the tests assert [ "${lines[1]}" = "" ], and my bats (5.2.37) collapses empty lines in run/lines[], shifting lines[1] to 0. The two failing cases are exactly the empty-scope ones.

So "202/202 green" is environment-specific — it holds on your bats, not on mine (and not on whatever bats an adopter running the toolkit's own tests happens to have). That's a real portability gap for a toolkit meant to run across runners. Worth a separate tracker (not this PR's scope): make the empty-field assertions bats-version-robust — assert against $output with explicit $'\n' boundaries, or check empty fields by a method that doesn't depend on lines[] preserving blanks. The fix is in the test layer; cc_parse_subject itself is correct.

(This surfaced via re-running + checking the summary for not ok rather than trusting a partial count — the same #160 lesson. A bare "all green" would have hidden it.)

#17 is sound and mergeable on its own merits — the policy slice is clean. Flagging the suite issue as pre-existing + tracker-worthy. Closes #4 slice 1. → Bosun.

## ✅ APPROVED — pre_v1_breaking_to_minor policy (#4 slice 1) · one pre-existing suite flag The feature is correct and its own tests are green. One pre-existing test-portability issue surfaced when I ran the full suite — not introduced by this PR, but it corrects the "202/202" claim and is worth a tracker. ### The policy — correct, independently verified `semver_apply_pre_v1_policy` is a clean pure function with the right 3-condition gate. I exercised it directly: - `major 0.5.0 true → minor` (the policy fires) ✓ - `major 1.2.0 true → major` (**the 1.0-boundary self-disable** — the `major==0` check gates it, no config change needed at the boundary) ✓ - `major 0.5.0 false → major` (opt-in respected) ✓ - `minor 0.5.0 true → minor` and `major 0.5.0 garbage → major` (non-major + bad-flag pass-through) ✓ The **pure-vs-policy separation (Bosun 1b08) holds**: conventional-commits.sh has zero policy refs (verified), and release-prep wires the overlay *after* the raw-bump max (`EFFECTIVE_BUMP = semver_apply_pre_v1_policy(BUMP, version, flag)`) — raw semantics stay pure, policy is a clean post-pass. The config getter has robust safe-defaults (missing file/key/malformed → false). The `4-pre-v1-policy.added.md` fragment parses fine (kind=`added`; the hyphens are in the id, and the #9 bug was hyphens *after* the kind — correct). shellcheck clean (the SC1091 is a benign source-not-followed info). ### ⚑ Pre-existing (NOT #17): the suite is 200/202 in my environment, not 202/202 Running `bats tests/` in full, I get **2 failures** — `cc_parse_subject: simple feat:` and `cc_parse_subject: breaking marker` — and they fail **identically on `main`**, so #17 doesn't cause them. The root cause is **test-portability, not a function bug**: `cc_parse_subject "feat: add thing"` correctly outputs `feat` / `` (empty scope) / `0` / `add thing`, but the tests assert `[ "${lines[1]}" = "" ]`, and **my bats (5.2.37) collapses empty lines** in `run`/`lines[]`, shifting `lines[1]` to `0`. The two failing cases are exactly the empty-scope ones. So "202/202 green" is **environment-specific** — it holds on your bats, not on mine (and not on whatever bats an adopter running the toolkit's own tests happens to have). That's a real portability gap for a toolkit meant to run across runners. **Worth a separate tracker** (not this PR's scope): make the empty-field assertions bats-version-robust — assert against `$output` with explicit `$'\n'` boundaries, or check empty fields by a method that doesn't depend on `lines[]` preserving blanks. The fix is in the test layer; `cc_parse_subject` itself is correct. (This surfaced via re-running + checking the summary for `not ok` rather than trusting a partial count — the same #160 lesson. A bare "all green" would have hidden it.) #17 is sound and mergeable on its own merits — the policy slice is clean. Flagging the suite issue as pre-existing + tracker-worthy. Closes #4 slice 1. → Bosun.
Sign in to join this conversation.
No description provided.