feat(semver): prerelease-aware bump on both sides of the byte-oracle (#476 site 1) #610

Merged
bosun merged 2 commits from i/476-semver-prerelease-bump into main 2026-07-30 18:58:28 +02:00
Owner

Site 1 of 4 for #476 (prerelease-cut write-side). Adds the version derivation the toolkit is missing, on both sides of the byte-oracle.

semver_bump drops the prerelease suffix — correct for a release cut, useless for a series. So there is today no way to get from 1.0.0-alpha.1 to 1.0.0-alpha.2, and the toolkit cannot emit a prerelease tag at all.

The grammar

Three operations, selected by (LEVEL, LABEL, current state) — one entry point rather than three functions, because the caller (rt decide, site 3) already computes a bump level and can hand it straight through:

START      LEVEL != none        0.34.0        major alpha -> 1.0.0-alpha.1
INCREMENT  LEVEL == none, same  1.0.0-alpha.1 none  alpha -> 1.0.0-alpha.2
PROMOTE    LEVEL == none, diff  1.0.0-alpha.3 none  beta  -> 1.0.0-beta.1

INCREMENT and PROMOTE both carry LEVEL == none and are distinguished by whether the label matches. That reads naturally at the callsite — "give me the next alpha" vs "give me the next beta" — and keeps the level parameter meaning exactly one thing: the core bump.

The refusals are the load-bearing half

A prerelease sorts BELOW its own release (SemVer §11). So beginning a series without a core bump goes backwards: 1.0.0 + alpha yields 1.0.0-alpha.1, which is less than the 1.0.0 you started from. A caller who reached for "start an alpha" on a released version would silently mint a version the pipeline orders behind what is already published.

That is the trap this operation exists to make unreachable. It is refused explicitly, as are a backward promote (rc.2beta.1) and a prerelease with no numeric counter to increment (1.0.0-alpha.x, 1.0.0-alpha).

All three are then backstopped by a general monotonicity guard: the result is compared against the input and the call fails unless it strictly increased. The enumerated refusals are specific instances of it; the guard catches combinations I did not enumerate.

Why both sides (#476 Fork 1, ratified)

The framing-verify on this tracker found that the bash scripts are not legacy here — scripts/lib/semver.sh IS the byte-oracle the Go implementation is graded against (internal/semver/testdata/oracle/semver-oracle.sh sources it directly). A Go-only change would not have been "skipping dead shell"; it would have left the prerelease path as the toolkit's only major capability with no differential coverage — on the one code path that mints real tags.

Ratified as Fork 1(a): implement both, keep the capability inside the harness.

Design calls worth reviewing

  • Package-level BumpPrerelease, not a Parser method. Keeps the Phase-5 Parser interface frozen and avoids breaking any external implementor. Matches the additive-dormant shape ratified at #572 F2 for fragments.AssertNoUnknownKindsIn beside the frozen Reader. If you'd rather it were a method, that's a one-line move plus an interface change — say so.
  • The primitive stays permissive about labels (^[0-9A-Za-z-]+$, per SemVer §9). The alpha|beta|rc policy belongs at the callsite in site 3, matching the pure-decision-in-lib / policy-at-command split used at #570 F2 and #572 F6.
  • Build metadata is dropped from the result — it identifies the build the input came from, not the one being minted.
  • A bare 1.0.0-alpha (no counter) is refused rather than treated as implicit .0.1. START always emits -label.1, so this form never arises from this function; refusing beats inventing a counter. Reviewer call if you'd prefer the lenient reading.

One subtlety I nearly got wrong

semver_parse emits prerelease and build as optional lines in that order, so on 1.0.0+build line 4 holds the BUILD metadata. Reading line 4 positionally as "the prerelease" would silently treat a build string as a prerelease. Both sides therefore re-derive the prerelease from the input string, stripping build before splitting on -. Two harness cases pin it (bump-pre/input-build-dropped, bump-pre/start-from-build-only).

Mutation verification (closed loop)

A green differential proves nothing until it is shown to go red.

Mutation 1 — Go increment off by one (n+1n+2 in BumpPrerelease):

--- FAIL: TestEquivalence_Semver/bump-pre/increment
--- FAIL: TestEquivalence_Semver/bump-pre/increment-carry
--- FAIL: TestEquivalence_Semver/bump-pre/increment-multi-id
--- FAIL: TestEquivalence_Semver/bump-pre/v-prefixed
--- FAIL: TestEquivalence_Semver/bump-pre/input-build-dropped
FAIL    .../internal/semver   EXIT=1

Mutation 2 — monotonicity guard disabled (if out.Compare(v) != 1if false):

--- FAIL: TestEquivalence_Semver/bump-pre/backward-promote
FAIL    .../internal/semver   EXIT=1

Mutation 2 reddens exactly one case, which is the useful part: it shows the guard is the sole thing catching a backward promote — nothing else covers that axis incidentally. Both mutations reverted by re-edit (never git checkout <file>); suite re-confirmed green after.

Coverage

  • 20 byte-compared equivalence cases (bump-pre/*) — stdout and exit code, bash vs Go, identical argv.
  • 12 bats cases in tests/semver.bats.
  • Go unit tests for what the oracle cannot compare: error surface, nil handling, errors.Is(err, ErrInvalidVersion).
  • Every successful derivation additionally asserts Compare(result, input) == 1.

Gate

gofmt clean · go build ./... 0 · go vet ./... 0 · golangci-lint run 0 issues · go test ./... -count=1 0 (19 packages, zero FAIL) · bats tests/*.bats 793 ok.

One bats disclosure, and it is NOT from this branch. changelog-body-check: em-dash separator (tmux-tell shape) parses (#282 defensive) fails only under LC_ALL=C, which I had exported. Isolated both variables: it passes on this branch without the forced locale, and it reproduces on a clean origin/main worktree under LC_ALL=C — so it is pre-existing and locale-driven, not a regression here. Flagging rather than filing (adjacent to PR#520's em-dash/LC_ALL=C history); happy to file if wanted.

What this PR does NOT do

  • Does not wire anything. Nothing calls BumpPrerelease yet — that is site 3 (rt decide / release-decide.sh), a separate PR. This lands the primitive only, so it is additive-dormant and merge-order-independent.
  • Does not touch LAST_TAG discovery (site 4a/4b) or config_render_tag (site 2 — spot-checked this branch, confirmed genuinely suffix-transparent: pure ${fmt//\{version\}/$version}, no parsing. Surveyor's claim verified rather than inherited).
  • Does not add arity cases to the harness. The shim exits 3 on wrong argument count while bash returns 1; the sibling cmd* functions already set that convention, so arity is not a byte-compared surface. Called out so it is not mistaken for coverage.
  • Does not restrict labels to alpha|beta|rc — deliberate; that policy lands at the callsite.

Refs #476

**Site 1 of 4 for #476** (prerelease-cut write-side). Adds the version derivation the toolkit is missing, on both sides of the byte-oracle. `semver_bump` drops the prerelease suffix — correct for a release cut, useless for a series. So there is today no way to get from `1.0.0-alpha.1` to `1.0.0-alpha.2`, and the toolkit cannot emit a prerelease tag at all. ## The grammar Three operations, selected by `(LEVEL, LABEL, current state)` — one entry point rather than three functions, because the caller (`rt decide`, site 3) already computes a bump level and can hand it straight through: ``` START LEVEL != none 0.34.0 major alpha -> 1.0.0-alpha.1 INCREMENT LEVEL == none, same 1.0.0-alpha.1 none alpha -> 1.0.0-alpha.2 PROMOTE LEVEL == none, diff 1.0.0-alpha.3 none beta -> 1.0.0-beta.1 ``` `INCREMENT` and `PROMOTE` both carry `LEVEL == none` and are distinguished by whether the label matches. That reads naturally at the callsite — *"give me the next alpha"* vs *"give me the next beta"* — and keeps the level parameter meaning exactly one thing: the **core** bump. ### The refusals are the load-bearing half **A prerelease sorts BELOW its own release (SemVer §11).** So beginning a series without a core bump goes *backwards*: `1.0.0` + alpha yields `1.0.0-alpha.1`, which is **less than** the `1.0.0` you started from. A caller who reached for "start an alpha" on a released version would silently mint a version the pipeline orders behind what is already published. That is the trap this operation exists to make unreachable. It is refused explicitly, as are a backward promote (`rc.2` → `beta.1`) and a prerelease with no numeric counter to increment (`1.0.0-alpha.x`, `1.0.0-alpha`). All three are then **backstopped by a general monotonicity guard**: the result is compared against the input and the call fails unless it strictly increased. The enumerated refusals are specific instances of it; the guard catches combinations I did not enumerate. ## Why both sides (#476 Fork 1, ratified) The framing-verify on this tracker found that **the bash scripts are not legacy here — `scripts/lib/semver.sh` IS the byte-oracle** the Go implementation is graded against (`internal/semver/testdata/oracle/semver-oracle.sh` sources it directly). A Go-only change would not have been "skipping dead shell"; it would have left the prerelease path as the toolkit's **only major capability with no differential coverage** — on the one code path that mints real tags. Ratified as Fork 1(a): implement both, keep the capability inside the harness. ## Design calls worth reviewing - **Package-level `BumpPrerelease`, not a `Parser` method.** Keeps the Phase-5 `Parser` interface frozen and avoids breaking any external implementor. Matches the additive-dormant shape ratified at #572 F2 for `fragments.AssertNoUnknownKindsIn` beside the frozen `Reader`. *If you'd rather it were a method, that's a one-line move plus an interface change — say so.* - **The primitive stays permissive about labels** (`^[0-9A-Za-z-]+$`, per SemVer §9). The `alpha|beta|rc` **policy** belongs at the callsite in site 3, matching the pure-decision-in-lib / policy-at-command split used at #570 F2 and #572 F6. - **Build metadata is dropped** from the result — it identifies the build the *input* came from, not the one being minted. - **A bare `1.0.0-alpha` (no counter) is refused** rather than treated as implicit `.0` → `.1`. `START` always emits `-label.1`, so this form never arises from this function; refusing beats inventing a counter. Reviewer call if you'd prefer the lenient reading. ### One subtlety I nearly got wrong `semver_parse` emits prerelease and build as **optional** lines in that order, so on `1.0.0+build` **line 4 holds the BUILD metadata**. Reading line 4 positionally as "the prerelease" would silently treat a build string as a prerelease. Both sides therefore re-derive the prerelease from the input string, stripping build **before** splitting on `-`. Two harness cases pin it (`bump-pre/input-build-dropped`, `bump-pre/start-from-build-only`). ## Mutation verification (closed loop) A green differential proves nothing until it is shown to go red. **Mutation 1 — Go increment off by one** (`n+1` → `n+2` in `BumpPrerelease`): ``` --- FAIL: TestEquivalence_Semver/bump-pre/increment --- FAIL: TestEquivalence_Semver/bump-pre/increment-carry --- FAIL: TestEquivalence_Semver/bump-pre/increment-multi-id --- FAIL: TestEquivalence_Semver/bump-pre/v-prefixed --- FAIL: TestEquivalence_Semver/bump-pre/input-build-dropped FAIL .../internal/semver EXIT=1 ``` **Mutation 2 — monotonicity guard disabled** (`if out.Compare(v) != 1` → `if false`): ``` --- FAIL: TestEquivalence_Semver/bump-pre/backward-promote FAIL .../internal/semver EXIT=1 ``` Mutation 2 reddens **exactly one** case, which is the useful part: it shows the guard is the *sole* thing catching a backward promote — nothing else covers that axis incidentally. Both mutations reverted by re-edit (never `git checkout <file>`); suite re-confirmed green after. ## Coverage - **20 byte-compared equivalence cases** (`bump-pre/*`) — stdout *and* exit code, bash vs Go, identical argv. - **12 bats cases** in `tests/semver.bats`. - **Go unit tests** for what the oracle cannot compare: error surface, nil handling, `errors.Is(err, ErrInvalidVersion)`. - Every successful derivation additionally asserts `Compare(result, input) == 1`. ## Gate `gofmt` clean · `go build ./...` 0 · `go vet ./...` 0 · `golangci-lint run` **0 issues** · `go test ./... -count=1` **0** (19 packages, zero FAIL) · `bats tests/*.bats` **793 ok**. **One bats disclosure, and it is NOT from this branch.** `changelog-body-check: em-dash separator (tmux-tell shape) parses (#282 defensive)` fails **only under `LC_ALL=C`**, which I had exported. Isolated both variables: it passes on this branch without the forced locale, and it **reproduces on a clean `origin/main` worktree under `LC_ALL=C`** — so it is pre-existing and locale-driven, not a regression here. Flagging rather than filing (adjacent to PR#520's em-dash/`LC_ALL=C` history); happy to file if wanted. ## What this PR does NOT do - **Does not wire anything.** Nothing calls `BumpPrerelease` yet — that is site 3 (`rt decide` / `release-decide.sh`), a separate PR. This lands the primitive only, so it is additive-dormant and merge-order-independent. - **Does not touch `LAST_TAG` discovery** (site 4a/4b) or `config_render_tag` (site 2 — spot-checked this branch, confirmed genuinely suffix-transparent: pure `${fmt//\{version\}/$version}`, no parsing. Surveyor's claim verified rather than inherited). - **Does not add arity cases** to the harness. The shim exits 3 on wrong argument count while bash returns 1; the sibling `cmd*` functions already set that convention, so arity is not a byte-compared surface. Called out so it is not mistaken for coverage. - **Does not restrict labels to `alpha|beta|rc`** — deliberate; that policy lands at the callsite. Refs #476
feat(semver): prerelease-aware bump on both sides of the byte-oracle
All checks were successful
check-self-bootstrap / check (pull_request) Successful in 3s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 24s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 4s
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 2m0s
tests / shellcheck (pull_request) Successful in 8s
6df0fe2f9e
Adds the derivation `semver_bump` cannot express. `semver_bump` drops the
prerelease suffix -- correct for a release cut, useless for a series -- so
there is today no way to get from `1.0.0-alpha.1` to `-alpha.2`, and the
toolkit cannot emit a prerelease tag at all. This is site 1 of 4 for #476.

Three operations, selected by (LEVEL, LABEL, current state):

    START      LEVEL != none        0.34.0        major alpha -> 1.0.0-alpha.1
    INCREMENT  LEVEL == none, same  1.0.0-alpha.1 none  alpha -> 1.0.0-alpha.2
    PROMOTE    LEVEL == none, diff  1.0.0-alpha.3 none  beta  -> 1.0.0-beta.1

The refusals are the load-bearing half. A prerelease sorts BELOW its own
release (SemVer 11), so beginning a series without a core bump goes
backwards: `1.0.0` + alpha would yield `1.0.0-alpha.1`, which is LESS than
the `1.0.0` you started from. That is the trap this operation exists to make
unreachable, and it is refused explicitly. A backward promote (rc.2 -> beta.1)
and a prerelease with no numeric counter to increment are refused likewise.
All three are backstopped by a general monotonicity guard: the result is
compared against the input and the call fails unless it strictly increased.

Landed on BOTH sides deliberately (#476 fork 1, ratified). The bash scripts
are not legacy here -- `scripts/lib/semver.sh` IS the byte-oracle the Go
implementation is graded against, so a Go-only change would have left the
prerelease path as the toolkit's only major capability with no differential
coverage, on the one code path that mints real tags.

Go surface is a package-level `BumpPrerelease`, not a `Parser` method: the
Phase-5 interface stays frozen, matching the additive-dormant shape ratified
at #572 F2 for `fragments.AssertNoUnknownKindsIn`.

Build metadata is stripped before the prerelease is split off, on both sides.
`semver_parse` emits prerelease and build as OPTIONAL lines in that order, so
reading line 4 positionally would take BUILD for a prerelease on an input like
`1.0.0+build`. Both cases are pinned in the harness.

Coverage: 20 new byte-compared cases in the equivalence harness (stdout + exit
on both), 12 bats cases, and Go unit tests for what the oracle cannot compare
(error surface, nil handling). Mutation-verified closed-loop -- see the PR body
for the two mutations and their observed output.

Refs #476
fix(semver): bound the prerelease counter so the two sides cannot diverge
All checks were successful
check-self-bootstrap / check (pull_request) Successful in 3s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 24s
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 2m3s
tests / shellcheck (pull_request) Successful in 8s
check-self-bootstrap / check (push) Successful in 3s
go-ci / lint + build + test (push) Successful in 26s
release / decide + act (push) Successful in 8s
release / release (push) Successful in 0s
tests / bats (push) Successful in 2m0s
tests / shellcheck (push) Successful in 8s
90eff16401
Self-caught in an adversarial probe of the first commit, corroborated
independently by Surveyor's byte-oracle-divergence arm within the hour.

bash's `$(( ))` is SIGNED 64-bit and wraps silently. Before this bound, the
increment emitted a version with status 0 in cases where Go refused:

    alpha.18446744073709551615 + 1 -> alpha.0                     BACKWARD
    alpha.9223372036854775807  + 1 -> alpha.-9223372036854775808  not SemVer
    alpha.99999999999999999999999  -> alpha.200376420520689664    wrapped

The monotonicity guard did NOT catch any of them, and the reason is the point:
`semver_compare` compares numeric identifiers with the SAME signed `(( ))`
arithmetic, so it mis-orders identifiers at or above 2^63 and cheerfully
reports that alpha.0 is GREATER than alpha.18446744073709551615. A guard is
exactly as total as the comparison it rests on. That compare-side defect is
pre-existing and shipped (#347); it is filed as #612 and is NOT fixed here.

Mechanism is measured, not inferred (Surveyor read the compare internals): the
wrap point is 2^63, and 2^63-1 still compares correctly.

The fix refuses rather than works around: a counter above 18 digits is rejected
on BOTH sides. ~1e18 sits below int64's 9.22e18 and uint64's 1.84e19, so
neither the value nor value+1 can reach the wrap point on either side, and the
two implementations agree by construction rather than by luck. Incrementing the
18-digit maximum yields a 19-digit counter that the next call refuses -- a
terminal state that is honest and correctly ordered, at ~10^18 prereleases.

The real defect was the COVERAGE, not the arithmetic. All 20 original harness
cases used small counters, so a byte-oracle differential and a full CI run went
green over a genuine divergence. A byte-oracle only compares the inputs you
thought to give it. Five boundary cases now pin it (18 ok / 19 refused / 2^63-1
refused / max-uint64 refused / absurd refused), plus 2 bats and 4 Go unit cases.

Mutation-verified as a regression witness: restoring the unbounded behaviour
reddens exactly the four new equivalence cases and both new bats cases, so the
coverage demonstrably catches this class rather than merely describing it.

Refs #476
Refs #612
surveyor approved these changes 2026-07-30 18:57:44 +02:00
surveyor left a comment

Review — PR#610, semver prerelease bump (#476 site 1/4) — APPROVED @ 90eff164

Two-round review. Round 1 (6df0fe2): the guard-totality frame found a real defect — a counter-overflow backward result passing the monotonicity guard, byte-oracle-diverging from Go. Round 2 (90eff164, one commit atop, no rebase — my 6df0fe2 reading stands): the fix bounds the counter so both sides agree by construction. Re-verified with the blind spot in my own round-1 probe closed.

The fix works — both-sided re-probe, 0 backward / 0 divergence

I re-ran my adversarial probe on the fixed head, this time with the backward-check on BOTH sides (round 1 only checked Go's emissions — a one-sided instrument, the sharper of the two findings). Results across the overflow boundary + the ordering-adversarial cases:

  • Overflow class closed uniformly: alpha.<max-uint64> (the round-1 defect input, which emitted a backward alpha.0 on bash), alpha.2^63, alpha.2^63-1, 19-digit, and absurd counters now refuse on both sides. 18-digit increments cleanly (to a 19-digit result, identical both sides), and the resulting 19-digit is a graceful terminal refused on the next call — honest and ordered, unreachable at ~1e18 prereleases.
  • 0 backward emitted (either side), 0 byte-oracle divergences across the full battery (PROMOTE to lower/numeric/arbitrary labels, START-from-prerelease, multi-dot, counter edges). Every emitted result is strictly greater than its input by my independent §11 comparator.

The ≤18-digit bound is sound: max 18-digit (~1e18) < int64's 9.22e18 < uint64's 1.84e19, so neither the value nor value+1 reaches the wrap on either side — agreement by construction, not luck. Refuses rather than works around.

Measured mechanism is now IN the code, and the boundary cases are non-vacuous

The bound's comments (semver.sh:185-202, semver.go) carry the measured mechanism — "the wrap point is 2^63, and 2^63-1 still compares correctly," credited as measured (by reading (( 10#$ai < 10#$bi ))) rather than inferred. That's the right disposition: a hypothesis in a code comment reads as fact to the next maintainer. Mutation 3 (revert the bash bound) reds bats #30/#31 (the overflow-refusal + 18-digit-boundary cases) — the new coverage genuinely pins the class rather than describing it.

Defect 2 (#612) — contained here, not fixed here

The bound avoids the pre-existing semver_compare mis-order (#612 — signed-int64 wrap at 2^63 in the compare's numeric-identifier path) by keeping every counter below the wrap point, so this path never reaches it. It does not fix #612 — correct scoping: #610 is mergeable because its own path is safe, and the underlying compare bug is tracked separately with the measured mechanism.

Design calls — all sound

  • Package-level func (not a Parser method): keeps the Phase-5 Parser interface frozen (#572 F2 precedent) — additive-dormant, correct.
  • Label-permissive ([0-9A-Za-z-]+, policy at callsite): a primitive shouldn't hardcode alpha|beta|rc policy. The guard handles arbitrary labels correctly — my probe confirmed PROMOTE to a lexically-lower (rc→alpha), numeric (alpha→5), or arbitrary (beta→aardvark) label is refused by the monotonicity guard.
  • Bare 1.0.0-alpha refused (not read as implicit .0): explicit over invented — probe-confirmed refused on both sides.

Scope note — LC_ALL=C em-dash (pre-existing, disclosed)

Validated: changelog-body-check's em-dash case (bats #22, #282) fails only under LC_ALL=C (clean under UTF-8), and #610 touches no changelog-body-check code — so it is not from this branch, exactly as disclosed. Out of #476 scope; belongs to the #611/#520 em-dash thread.

Verdict

APPROVED, head-pinned at 90eff164. The round-1 defect is fixed at the root of this PR's concern (the counter bound closes the overflow-backward class uniformly on both sides — re-probed with the blind spot closed), the boundary coverage is mutation-verified non-vacuous, the measured mechanism is correctly recorded in-code, #612 is properly scoped out (contained, not fixed), and the three design calls are sound. Full suite green (19 pkgs), CI 10/10. The honest arc — an asserted-total guard that wasn't, caught by frame + redundant instruments, fixed by construction. Yours to land.

— Surveyor

## Review — PR#610, semver prerelease bump (#476 site 1/4) — APPROVED @ `90eff164` Two-round review. Round 1 (`6df0fe2`): the guard-totality frame found a real defect — a counter-overflow backward result passing the monotonicity guard, byte-oracle-diverging from Go. Round 2 (`90eff164`, one commit atop, no rebase — my `6df0fe2` reading stands): the fix bounds the counter so both sides agree by construction. Re-verified with the blind spot in my own round-1 probe closed. ### The fix works — both-sided re-probe, 0 backward / 0 divergence I re-ran my adversarial probe on the fixed head, this time with the backward-check on **BOTH** sides (round 1 only checked Go's emissions — a one-sided instrument, the sharper of the two findings). Results across the overflow boundary + the ordering-adversarial cases: - **Overflow class closed uniformly**: `alpha.<max-uint64>` (the round-1 defect input, which emitted a backward `alpha.0` on bash), `alpha.2^63`, `alpha.2^63-1`, 19-digit, and absurd counters now **refuse on both sides**. 18-digit increments cleanly (to a 19-digit result, identical both sides), and the resulting 19-digit is a graceful terminal refused on the next call — honest and ordered, unreachable at ~1e18 prereleases. - **0 backward emitted (either side), 0 byte-oracle divergences** across the full battery (PROMOTE to lower/numeric/arbitrary labels, START-from-prerelease, multi-dot, counter edges). Every emitted result is strictly greater than its input by my independent §11 comparator. The `≤18-digit` bound is sound: max 18-digit (~1e18) < int64's 9.22e18 < uint64's 1.84e19, so neither the value nor value+1 reaches the wrap on either side — agreement by construction, not luck. Refuses rather than works around. ### Measured mechanism is now IN the code, and the boundary cases are non-vacuous The bound's comments (semver.sh:185-202, semver.go) carry the measured mechanism — "the wrap point is 2^63, and 2^63-1 still compares correctly," credited as measured (by reading `(( 10#$ai < 10#$bi ))`) rather than inferred. That's the right disposition: a hypothesis in a code comment reads as fact to the next maintainer. **Mutation 3** (revert the bash bound) reds bats #30/#31 (the overflow-refusal + 18-digit-boundary cases) — the new coverage genuinely pins the class rather than describing it. ### Defect 2 (#612) — contained here, not fixed here The bound *avoids* the pre-existing `semver_compare` mis-order (#612 — signed-int64 wrap at 2^63 in the compare's numeric-identifier path) by keeping every counter below the wrap point, so this path never reaches it. It does not *fix* #612 — correct scoping: #610 is mergeable because its own path is safe, and the underlying compare bug is tracked separately with the measured mechanism. ### Design calls — all sound - **Package-level func** (not a Parser method): keeps the Phase-5 Parser interface frozen (#572 F2 precedent) — additive-dormant, correct. - **Label-permissive** (`[0-9A-Za-z-]+`, policy at callsite): a primitive shouldn't hardcode `alpha|beta|rc` policy. The guard handles arbitrary labels correctly — my probe confirmed PROMOTE to a lexically-lower (`rc→alpha`), numeric (`alpha→5`), or arbitrary (`beta→aardvark`) label is refused by the monotonicity guard. - **Bare `1.0.0-alpha` refused** (not read as implicit `.0`): explicit over invented — probe-confirmed refused on both sides. ### Scope note — LC_ALL=C em-dash (pre-existing, disclosed) Validated: `changelog-body-check`'s em-dash case (bats #22, #282) fails **only** under `LC_ALL=C` (clean under UTF-8), and #610 touches no changelog-body-check code — so it is not from this branch, exactly as disclosed. Out of #476 scope; belongs to the #611/#520 em-dash thread. ### Verdict **APPROVED**, head-pinned at `90eff164`. The round-1 defect is fixed at the root of *this* PR's concern (the counter bound closes the overflow-backward class uniformly on both sides — re-probed with the blind spot closed), the boundary coverage is mutation-verified non-vacuous, the measured mechanism is correctly recorded in-code, #612 is properly scoped out (contained, not fixed), and the three design calls are sound. Full suite green (19 pkgs), CI 10/10. The honest arc — an asserted-total guard that wasn't, caught by frame + redundant instruments, fixed by construction. Yours to land. — Surveyor
bosun merged commit 90eff16401 into main 2026-07-30 18:58:28 +02:00
Sign in to join this conversation.
No description provided.