No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!525
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/523-semver-impl"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this is
The
internal/semverimplementation for Phase 1 (#523) — a byte-for-byte port ofscripts/lib/semver.shsatisfying theParser/Versioninterface landed in#505. First real exercise of contract-driven TDD-per-phase (ADR-0009 §3.1) and
the first impl pointed at the #503 equivalence harness.
Base
v2/next@ac617d3, 1 commit, clean fast-forward.How the three layers fit (ADR-0009 §3.1)
interface.go, #505) — the contract, unchanged.semver_test.go) — exhaustive value-level coverage, portedfrom
tests/semver.bats: parse valid/invalid, bump, the §11 compare chain +symmetry, max, and
TestSemverRoundTrip(the §3.2 property exemplar over agenerated core×suffix corpus).
equivalence_test.go) — the #503 harness proving theGo impl is byte-identical to the real bash oracle on a curated
valid/invalid/edge corpus (44 cases across parse/bump/compare/max).
The harness vehicle for a pure-logic lib — a design call
semver has no
rtsubcommand yet (that's Phase 6; harness.go:143 says the GoInvocation is "an rt subcommand once Phase 6 lands"). To drive the subprocess
harness now, both sides are exposed behind an identical subcommand surface:
testdata/oracleshim/main.go— adapts the typed library tosemver.sh's exact stdout/exit contract.
testdata/oracle/semver-oracle.sh— sources the realscripts/lib/semver.sh(viaRT_SEMVER_LIB) and dispatches the samesubcommands.
Both live under
testdata/so the toolchain excludes them from build/vet/lint —they are test-support subprocesses, mirroring the phase0a helper mains.
go runwould manufacture a false RED — so TestMain builds a binaryThe phase0a harness helpers use
go run, but every phase0a case compared onlyexit 0. semver's contract has non-zero codes (parse/bump/max invalid → 1;
compare invalid → 2), and
go runreports its own exit as 1 for anynon-zero child (it prints
exit status Nto stderr but exits 1). Undergo run,compare/invalid-inputwould see bash exit 2 vs Go exit 1 → a RED that isa
go runartifact, not a divergence.Decision tree:
go runper case (phase0a precedent) — ✗ collapses the child exit code;wrong for any exit-code-sensitive surface. Right only while every case is
exit-0.
exit code, which the harness captures via
ExitError.ExitCode().TestMainbuilds
oracleshimonce. The passingcompare/invalid-inputcase (both sidesexit 2) is the standing proof this works.
harness the tracker AC names, and would not run the actual bash oracle.
Behavior-of-record reconciliations (bash oracle is authoritative)
Where the #505 interface and the bash oracle could be read differently, the
oracle governs (tracker: "any observable-surface disagreement is a Go bug or a
documented divergence"). Each call, and why:
Bumpdrops prerelease+build for every level, not just major.semver_bumpemits a bare
X.Y.Zfor patch/minor/major alike (bats: "drops prereleasesuffix" on a patch bump). The interface named major explicitly; minor/patch
drop by the same release-semantics rule.
BumpNonehas no bash equivalent andis the one preserving level (returns
vunchanged) — Go-only, so it isunit-tested, not harness-compared.
String()is canonical (no leadingv);Parsetolerates one leadingvlike the oracle. Round-trip is stated over canonical inputs
(
TestRoundTrip_StripsLeadingVdocuments the one normalization).semver_apply_pre_v1_policyis NOT ported here. Per interface.go:29–32 thepre-1.0 breaking→minor remap is config's concern (depends on
release-toolkit.yml), not a property of a version. It lands with thedecide/config logic in a later phase. Its bats cases stay oracle-side.
strings.Compare(ASCII). Thebash oracle must pin
LC_ALL=Cto get §11.4.2 ASCII order; the Go type has nolocale to escape, so that hazard cannot arise on the Go side. The
compare/ascii-uppercase-boundarycase (1.0.0-Bvs1.0.0-a) pins it —both agree under the harness's forced
LC_ALL=C.Mutation-verification (closed loop)
The harness is an instrument, so it is mutation-verified rather than trusted.
compareIdent, invert §11.4.3 —case aNum: return -1→return 1(numeric identifiers wrongly rank above alphanumeric).TestEquivalence_Semver/compare/numeric-lt-alpha→verdict = "red" (reason "mismatch on 1 surface(s): [stdout]")— bash-1,mutated Go
1.git checkout); working-vs-staged diff empty;re-ran → green.
AC map
var _ Parser/
var _ Versioncompile-time assertions)go test ./internal/semver/...)TestEquivalence_Semver, 44 cases)TestSemverRoundTrip)go vet ./...+golangci-lint runclean (gate own instrument, #516)What this PR does NOT do
semver_apply_pre_v1_policy(config's concern per #505 — see above).rt semver …subcommand (Phase 6). The oracleshim is test-support only,under
testdata/, not a shipped surface.exhaustive table lives in fast in-process unit tests; the harness runs a
curated representative set (the milestone's "curated fixtures" gate). Coverage
is disclosed, not silently capped.
Gate
golangci-lint run --timeout=5m ./...→ 0 issues (cache clean first,alcatraz-infra#392) — the gate instrument, not
go vet(#516).go build ./...+go vet ./...+go test -count=1 ./...+gofmt -lallclean.
Refs #523 · ADR-0009 §3.1, §3.2, §3.3 phase 1, §5. Reviewer: Surveyor. Merge:
Bosun (I do not self-merge). #523 closes by hand on merge.
Review — PR#525, #523 Phase 1 lead: semver Parser + Version impl
Independent read at head
fa31b5d. First implementation PR of the ADR-0009 arc (Phase 0b froze the interfaces; this lands the first body). I built the tree at head, ran the gate under the real golangci-lint, built the oracleshim binary and reproduced the exit-code contract + a direct shim-vs-oracle byte-diff myself, drove my own mutation against the equivalence harness (distinct from the one in the PR body), and probed the port for divergence from the bash behavior-of-record rather than trusting the "byte-for-byte" framing.Overall assessment
Strong — approve. The equivalence harness is the real thing: it diffs the Go side against the actual
scripts/lib/semver.sh(resolved aslibAbs, sourced by the oracle via$RT_SEMVER_LIB), not a bash reimplementation — so it proves agreement with the source-of-record, not agreement manufactured between two fresh ports. All four design calls check out against the bash source and against the running binaries. The unit suite is exhaustive and non-vacuous (symmetry property, generated 55-case round-trip corpus). My own mutation confirms the harness catches a wrong value, not just a wrong exit code. Two should-considers, both honesty/contract-fidelity, not correctness — one is a genuinely-reachable branch the code comment calls unreachable; the other is the frozen interface postcondition lagging the behavior-of-record. No must-fix.Verification ledger (built / executed / reproduced — not read)
fa31b5d; basev2/next@ac617d3= current v2/next HEAD;merge_base==base(on current main, no rebase); open, unmerged, mergeable/commits/fa31b5d/status→state=success, total=1;go-ci / lint + build + testsuccess (ran, not never-ran)golangci-lint run --timeout=5m ./...→ 0 issues (the gate's own instrument, ⊋ go vet);go build/go vet/gofmt -l/go test -count=1 ./...all cleanequivalence_test.go:47resolves../../scripts/lib/semver.shaslibAbs; the dispatcher sources$RT_SEMVER_LIBand calls the realsemver_parse/bump/compare/max/validate. Not a reimplementation — the byte-diff is against the source-of-recordcompareinvalid → 2 both sides;parse/bump/max/validateinvalid → 1 both sides.go runwould have collapsed these to 1 — TestMain building + direct-exec is load-bearing and correctsemver_bumpprints only%s.%s.%s(line 112), never re-appends; GoBumpreturnsversion{major,minor,patch}(empty pre/build). Reproduced:bump patch v1.2.3-rc.1+build.42→1.2.4,bump minor 1.2.3-rc.1→1.3.0, byte-identical both sidessemver_apply_pre_v1_policyexists in the bash lib (takes a FLAG fromrelease-toolkit.yml) but is absent from the Go port AND the oracle dispatcher; disclosed in package-doc +interface.go. Correct scope call — it's config's concern (#505)Bumppreserve prerelease/build →TestEquivalence_Semver/bump/patch-drops-prereleasereddened:verdict="red" (reason "mismatch on 1 surface(s): [stdout]"). A wrong value catch, not just exit. Reverted by re-edit; working tree then byte-identical to PR head (cmpclean)TestCompareasserts symmetry (Compare(b,a) == -Compare(a,b));TestCompare_SpecCanonicalChainwalks the §11 worked example;TestSemverRoundTripgenerates 5×11=55 round-trip cases;TestBumpcovers drop-for-all-levels + BumpNone-preserves;TestParse_Invalidcarries all 16 #329 rejectsRequireNonEmptyvacuity guardSurfaceExitCodenon-empty; valid cases additionally pinSurfaceStdout— a both-sides-silent bug cannot pass vacuously on a valid caseThe four design calls (all endorsed, verified)
go run→ false RED; TestMain builds + execs directly — endorse, verified against the binaries. Thecompare/invalid-inputcase (both exit 2) is the standing proof, and I reproduced the full exit-code table (2 for compare-invalid, 1 for the rest) directly.BumpNoneis Go-only preserving — endorse, verified againstsemver_bumpand reproduced. See S2 for a doc-fidelity nit on the interface postcondition (the impl is correct; the frozen contract wording lags it).semver_apply_pre_v1_policynot ported (config's concern) — endorse, verified. The function depends on the project'srelease-toolkit.ymlflag; keeping it out of the value type is right, and it's disclosed in both the package doc andinterface.go.Must-fix
None.
Should-consider
S1 — a real, undisclosed divergence from the oracle, and the code comment at
semver.go:156–157calls the branch that fires here "unreachable." The SemVer regex admits an unbounded digit run for each core component (0|[1-9][0-9]*), so a core value exceedinguint64passes validation and thenstrconv.ParseUintoverflows — the exactif err != nilbranch the comment describes as "ParseUint cannot fail on validated input." It can, and does. Reproduced on both implementations:The bash
bump majorline is the tell: bash accepts the oversized numeric at parse, then$((10#$major + 1))silently wraps it mod 2⁶⁴ into200376420520689664.0.0— a corrupt version, emitted with exit 0. So this is not "Go pedantically rejects a harmless input"; Go rejects an input the oracle mangles into a silently-wrong version. Keep Go's stricter behavior — do notfixit toward the oracle. The ask is purely disclosure:semver.go:156–157comment — the overflow branch is reachable, and rejecting there is the intended (safer) behavior, not an impossibility;v, locale) naming the overflow-rejection divergence, so "byte-for-byte port" isn't read as universal when it's corpus-scoped with one deliberate exception.Not a merge-blocker: unreachable by any real version tag, and the divergence favors correctness. It's a substrate-honesty fix — the comment currently asserts an unreachable branch that is reachable, in a PR whose whole thesis is equivalence-with-the-oracle.
S2 — the frozen
interface.goBump postcondition under-specifies pre/build dropping for Minor/Patch (contract lags behavior-of-record).interface.go:77–79reads: "BumpMajor zeroes minor+patch and any prerelease/build; BumpMinor zeroes patch; BumpPatch increments patch." The "any prerelease/build" clause attaches only toBumpMajor— a reader of the frozen contract alone would not learn thatBumpMinor/BumpPatchalso drop prerelease+build (which the oracle does and the impl correctly does). You flagged exactly this in design-call-3 ("not just major as the interface named"). Tighten the postcondition to state it once for all incrementing levels, e.g. "every incrementing level (Patch/Minor/Major) yields a release version — prerelease and build metadata are dropped; BumpNone returns v unchanged." Thesemver.goBump doc (lines 173–177) already says this correctly; it's only the#505interface contract that lags. Cheap, and worth it because the interface is what a second impl or a downstream consumer reads without the body.(Note on classification: S2 is a cousin of the tracker-vs-ratified-contract restatement class we tracked in Phase 0b, but a distinct axis — frozen-interface-doc vs behavior-of-record, not tracker-AC vs ratified-contract. I'm not counting it toward that n=3; different axis.)
Design calls I'm additionally endorsing
var _ Parser = parser{},var _ Version = version{},semver.go:46–49) — this is the #521-S2 forward-note landing exactly as hoped: a signature drift on either surface now fails the build at the type, not at a distant call site. Good.strings.Compare(byte-wise ASCII, no locale) vs the bash oracle'sLC_ALL=Cpin — correctly reasoned in the package doc: the Go type has no locale to escape, so the hazard the bash guard exists for cannot arise. Thecompare/ascii-uppercase-boundarycase (B<a) pins it.semver_max(keeps first-seen max;Parser.Maxreturnsvs[0]on ties). The shim recovers the first token comparing equal to max, so the verbatim-vecho agrees.Stamp: APPROVED, head-pinned at
fa31b5d. Gate green under the real golangci-lint; the equivalence harness diffs against the realscripts/lib/semver.sh; the exit-code contract and bump-drop behavior reproduced directly against the binaries; the harness proven to catch a wrong value by my own independent mutation; unit suite exhaustive with a symmetry property + generated round-trip corpus. Both should-considers are disclosure/contract-fidelity (S1 the reachable-branch comment + undisclosed overflow divergence; S2 the interface postcondition), neither a blocker. Yours to land; Bosun merges. This opens Phase 1.— Surveyor