feat(gates): compose-verify preflight — Go port (#534 PR 3/3) #540

Merged
bosun merged 1 commit from i/534-compose-verify into v2/next 2026-07-26 02:26:42 +02:00
Owner

What

Third and final of Phase 3's three cut-time gate ports (#534). Ports compose-verify.sh — the cut-time preflight — to Go, the orchestrator that fires the other two gates. Also documents the #498 fragment-coverage gate design decision. This PR lands #534's close and Phase 3 milestone #75.

PR sequence: PR 1 register-check (merged @ 8cbf5ca) → PR 2 changelog-body-check (merged @ 6b22ee4) → PR 3 compose-verify (this).

The preflight

internal/gates.ComposeVerify(cl, version) ComposeVerifyResult is the cut-time gate that verifies a composed CHANGELOG's target section is compose-current + gate-clean:

  1. Extract the requested version's section (sectionContentByVersion). Absent or empty → exit 2.
  2. Coupling guard (#439): the cut version must be the newest versioned section — a cut against a non-newest section is itself a compose-path bypass symptom. Mismatch → exit 2.
  3. Gate 1RegisterCheckStdin over the section content (PR 1's gate).
  4. Gate 2ChangelogBodyCheck over the whole changelog (PR 2's gate).

Trivalent exit via ComposeVerifyResult.Exit(): 2 (precondition — section-not-found / no-version / coupling-mismatch), 1 (either gate failed), 0 (clean). The three Phase-3 gates now compose exactly as the bash pipeline does — this is the surface internal/release.Cutter.Prepare consumes in Phase 6.

Design decisions (decision-tree, not conclusion)

1. Lenient extraction, matching the bash helpers — not Parser.LatestVersion. currentVersion + sectionContentByVersion port changelog_get_current_version / changelog_get_section_content (lenient grep/awk), not the strict changelog.Parser.LatestVersion (which adds ErrMalformedHeading). compose-verify must agree with the bash oracle's exit code on any input, including a malformed changelog — same lenient-vs-strict rationale as PR 2's target resolution. It also faithfully replicates the bash quirk that the version is interpolated into the match regex raw (dots as regex any-char) — byte-identical on real X.Y.Z versions; the pathological "1.0.0 matches 1x0x0" case never arises from a real cut version (disclosed in-code).

2. The check LOGIC, not the CLI. ComposeVerify(cl, version) takes the changelog bytes + version directly. The --version/--changelog flag parsing, the bad-args exit 2, and the diagnostic prose are a Phase-6 cmd concern — the port delivers the gate logic + its byte-equivalence.

Disclosed boundary (fold — review 4575): in the raw-regex-quirk region only, sectionContentByVersion matches a malformed heading via dots-as-any (request 1.0.0 vs ## [v1x0x0]), but currentVersion's strict digit/dot pattern rejects it → Go returns ComposeVerifyNoVersion (exit 2), while bash's set -euo pipefail aborts the newest-version assignment (exit 1). This is out-of-domain (the composer only emits well-formed headings; all 7 fixtures + the 6 in-domain sweep cases byte-match) and safe-direction (both non-zero → cut blocked). Go's exit 2 — a named precondition — is retained deliberately over matching bash's set -e artifact (same posture as semver's uint64-overflow, #525). Disclosed in-code on ComposeVerifyNoVersion; the earlier "unreachable once a section was found" comment was corrected. Comment/doc-only — zero logic change, so the 7 fixtures stay byte-identical.

#498 fragment-coverage gate — design decision (Fork A, Opt 1)

Per #534 AC4 and the Fork-A resolution, this PR documents the design decision (docs/fragment-coverage-gate.md); the cross-repo implementation stays #498's own arc.

  • Decision: Option 2 — explicit no-fragment sentinel. A PR is coverage-clean with EITHER a changelog.d/<id>.<kind>.md fragment OR an explicit no-changelog: <reason> sentinel, moving the signal from absence of a fragment (indistinguishable from a forgotten one) to one of two present forms a gate can act on. Chosen over a label (Option 1 — mutable, out-of-band, reasonless), a .none.md marker (Option 3 — pollutes the C4 fragment grammar + compose path), and doc-only (Option 4 — the status-quo gap).
  • Implementation deferred — and why it must be. #498's load-bearing structural constraint: the paths: filter gates invocation consumer-side, so a toolkit-only landing passes its own tests, closes green, and leaves the gap live in every consumer. The fix is two-part (toolkit reusable workflow + per-consumer trigger migration in the three exposed repos), so it stays #498's separate cross-repo arc rather than bloating a Go-engine PR or closing #498 dishonestly. The doc records the mechanism that arc will implement.

Verification (closed loop)

7 byte-equivalence cases vs the REAL compose-verify.sh (which finds its sibling gates + lib/changelog.sh), comparing the 0/1/2 exit — the whole observable, since the script logs only to stderr. Cases span exit 0/1/2: clean-coupled (0), section-not-found (2), v-prefix-stripped (0), coupling-mismatch (2), gate-1 register-drift (1), gate-2 body-check-fail (1), gate-2 body-check-warn-passes (0).

Harness teeth mutation-verified on two orchestration axes (each reddening only its target, reverted by re-edit byte-identical):

  • disable the coupling guard → coupling-mismatch RED (Go exit 0 vs bash exit 2).
  • disarm gate 1 → register-drift RED (Go exit 0 vs bash exit 1).

⚠️ Mutation-verify requires go test -count=1 (the oracleshim is runtime-built, invisible to go test's cache).

Plus 9 unit subtests: each exit path by Reason (clean-coupled, v-prefix, section-not-found, coupling-mismatch, gate-1 drift, gate-2 fail) + the lenient currentVersion / sectionContentByVersion helpers (by-version extraction, blank-edge strip, absent-version empty, Unreleased-skip).

Gate

golangci-lint run --timeout=5m (cache clean first, #392) → 0 issues · go build ./... · go vet ./... · go test -count=1 ./... green · gofmt -l clean · shellcheck clean · register-check.sh clean on docs/fragment-coverage-gate.md.

What this does NOT do

  • Does not implement the #498 fragment-coverage gate — only the design decision is documented here; the two-part cross-repo implementation is #498's own arc (Fork A Opt 1).
  • Does not parse CLI flags--version/--changelog + bad-args exit 2 are Phase-6 cmd wiring (design note 2).
  • Does not wire the gates into internal/release.CutterPrepare consumes ComposeVerify in Phase 6.

PR 3/3 lands #534's close (all three gate ACs + the fragment-coverage design AC) and Phase 3 milestone #75. Refs #534 · reviewer @surveyor · merge @bosun (no self-merge). On merge, Bosun ticks the #534 ACs and closes the tracker + milestone by hand (Refs-only, consistent with #532 / #533).

## What Third and **final** of Phase 3's three cut-time gate ports (#534). Ports `compose-verify.sh` — the cut-time preflight — to Go, the orchestrator that fires the other two gates. Also documents the **#498 fragment-coverage gate design decision**. This PR lands #534's close and Phase 3 milestone #75. PR sequence: PR 1 register-check (merged @ 8cbf5ca) → PR 2 changelog-body-check (merged @ 6b22ee4) → **PR 3 compose-verify (this)**. ## The preflight `internal/gates.ComposeVerify(cl, version) ComposeVerifyResult` is the cut-time gate that verifies a composed CHANGELOG's target section is compose-current + gate-clean: 1. **Extract** the requested version's section (`sectionContentByVersion`). Absent or empty → exit 2. 2. **Coupling guard** (#439): the cut version must be the newest versioned section — a cut against a non-newest section is itself a compose-path bypass symptom. Mismatch → exit 2. 3. **Gate 1** — `RegisterCheckStdin` over the section content (PR 1's gate). 4. **Gate 2** — `ChangelogBodyCheck` over the whole changelog (PR 2's gate). Trivalent exit via `ComposeVerifyResult.Exit()`: **2** (precondition — section-not-found / no-version / coupling-mismatch), **1** (either gate failed), **0** (clean). The three Phase-3 gates now compose exactly as the bash pipeline does — this is the surface `internal/release.Cutter.Prepare` consumes in Phase 6. ## Design decisions (decision-tree, not conclusion) **1. Lenient extraction, matching the bash helpers — not `Parser.LatestVersion`.** `currentVersion` + `sectionContentByVersion` port `changelog_get_current_version` / `changelog_get_section_content` (lenient grep/awk), *not* the strict `changelog.Parser.LatestVersion` (which adds `ErrMalformedHeading`). compose-verify must agree with the bash oracle's **exit code on any input**, including a malformed changelog — same lenient-vs-strict rationale as PR 2's target resolution. It also faithfully replicates the bash quirk that the version is interpolated into the match regex raw (dots as regex any-char) — byte-identical on real X.Y.Z versions; the pathological "1.0.0 matches 1x0x0" case never arises from a real cut version (disclosed in-code). **2. The check LOGIC, not the CLI.** `ComposeVerify(cl, version)` takes the changelog bytes + version directly. The `--version`/`--changelog` flag parsing, the bad-args exit 2, and the diagnostic prose are a Phase-6 `cmd` concern — the port delivers the gate logic + its byte-equivalence. **Disclosed boundary (fold — review 4575):** in the raw-regex-quirk region only, `sectionContentByVersion` matches a **malformed** heading via dots-as-any (request `1.0.0` vs `## [v1x0x0]`), but `currentVersion`'s strict digit/dot pattern rejects it → Go returns `ComposeVerifyNoVersion` (exit 2), while bash's `set -euo pipefail` aborts the newest-version assignment (exit 1). This is **out-of-domain** (the composer only emits well-formed headings; all 7 fixtures + the 6 in-domain sweep cases byte-match) and **safe-direction** (both non-zero → cut blocked). Go's exit 2 — a named precondition — is retained deliberately over matching bash's `set -e` artifact (same posture as semver's uint64-overflow, #525). Disclosed in-code on `ComposeVerifyNoVersion`; the earlier "unreachable once a section was found" comment was corrected. Comment/doc-only — zero logic change, so the 7 fixtures stay byte-identical. ## #498 fragment-coverage gate — design decision (Fork A, Opt 1) Per #534 AC4 and the Fork-A resolution, this PR **documents the design decision** (`docs/fragment-coverage-gate.md`); the cross-repo **implementation stays #498's own arc**. - **Decision: Option 2 — explicit no-fragment sentinel.** A PR is coverage-clean with EITHER a `changelog.d/<id>.<kind>.md` fragment OR an explicit `no-changelog: <reason>` sentinel, moving the signal from *absence of a fragment* (indistinguishable from a forgotten one) to *one of two present forms* a gate can act on. Chosen over a label (Option 1 — mutable, out-of-band, reasonless), a `.none.md` marker (Option 3 — pollutes the C4 fragment grammar + compose path), and doc-only (Option 4 — the status-quo gap). - **Implementation deferred — and why it must be.** #498's load-bearing structural constraint: the `paths:` filter gates *invocation* consumer-side, so a toolkit-only landing passes its own tests, closes green, and leaves the gap live in every consumer. The fix is two-part (toolkit reusable workflow **+** per-consumer trigger migration in the three exposed repos), so it stays #498's separate cross-repo arc rather than bloating a Go-engine PR or closing #498 dishonestly. The doc records the mechanism that arc will implement. ## Verification (closed loop) **7 byte-equivalence cases** vs the REAL `compose-verify.sh` (which finds its sibling gates + `lib/changelog.sh`), comparing the 0/1/2 exit — the whole observable, since the script logs only to stderr. Cases span exit 0/1/2: clean-coupled (0), section-not-found (2), v-prefix-stripped (0), coupling-mismatch (2), gate-1 register-drift (1), gate-2 body-check-fail (1), gate-2 body-check-warn-passes (0). **Harness teeth mutation-verified** on two orchestration axes (each reddening only its target, reverted by re-edit byte-identical): - disable the coupling guard → `coupling-mismatch` RED (Go exit 0 vs bash exit 2). - disarm gate 1 → `register-drift` RED (Go exit 0 vs bash exit 1). ⚠️ **Mutation-verify requires `go test -count=1`** (the `oracleshim` is runtime-built, invisible to `go test`'s cache). Plus **9 unit subtests**: each exit path by `Reason` (clean-coupled, v-prefix, section-not-found, coupling-mismatch, gate-1 drift, gate-2 fail) + the lenient `currentVersion` / `sectionContentByVersion` helpers (by-version extraction, blank-edge strip, absent-version empty, Unreleased-skip). ## Gate `golangci-lint run --timeout=5m` (cache clean first, #392) → **0 issues** · `go build ./...` · `go vet ./...` · `go test -count=1 ./...` green · `gofmt -l` clean · `shellcheck` clean · `register-check.sh` clean on `docs/fragment-coverage-gate.md`. ## What this does NOT do - **Does not implement the #498 fragment-coverage gate** — only the design decision is documented here; the two-part cross-repo implementation is #498's own arc (Fork A Opt 1). - **Does not parse CLI flags** — `--version`/`--changelog` + bad-args exit 2 are Phase-6 `cmd` wiring (design note 2). - **Does not wire the gates into `internal/release.Cutter`** — `Prepare` consumes `ComposeVerify` in Phase 6. --- **PR 3/3 lands #534's close** (all three gate ACs + the fragment-coverage design AC) and **Phase 3 milestone #75**. Refs #534 · reviewer @surveyor · merge @bosun (no self-merge). On merge, Bosun ticks the #534 ACs and closes the tracker + milestone by hand (Refs-only, consistent with #532 / #533).
feat(gates): compose-verify preflight — Go port (#534 PR 3/3)
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 15s
e9261e01ee
Ports compose-verify.sh — the cut-time preflight — to Go, the last of Phase 3's
three gate ports and the orchestrator that fires the other two. Also documents
the #498 fragment-coverage gate design decision.

internal/gates.ComposeVerify(cl, version) extracts the requested version's
section, asserts it is the newest versioned section (the #439 coupling guard — a
cut against a non-newest section is a compose-path bypass symptom), then fires
RegisterCheckStdin over the section content + ChangelogBodyCheck over the whole
changelog. Trivalent exit: 2 (precondition — section-not-found / no-version /
coupling-mismatch), 1 (either gate failed), 0 (clean).

Lenient extraction (currentVersion + sectionContentByVersion) matches the bash
changelog_get_current_version / changelog_get_section_content helpers, NOT the
strict changelog.Parser.LatestVersion — compose-verify must agree with the bash
oracle's exit on any input incl. a malformed changelog (same lenient-vs-strict
rationale as body-check's target resolution). The CLI flag surface
(--version/--changelog, bad-args exit 2) is a Phase-6 cmd concern.

#498 fragment-coverage design decision (Fork A Opt1, documented per #534 AC4):
docs/fragment-coverage-gate.md ratifies Option 2 — a PR is coverage-clean with
EITHER a changelog.d fragment OR an explicit `no-changelog: <reason>` sentinel,
moving the signal from absence (indistinguishable from a forgotten fragment) to
one-of-two-present-forms. The two-part cross-repo IMPLEMENTATION (toolkit
reusable workflow + per-consumer trigger migration in the three exposed repos)
is a load-bearing structural constraint that stays #498's own arc — a
toolkit-only landing would leave the gap live in every consumer.

Verification: 7 byte-equivalence cases vs the REAL compose-verify.sh (exit 0/1/2
across clean-coupled, section-not-found, v-prefix, coupling-mismatch, gate-1
register drift, gate-2 body-check fail, gate-2 warn-passes). Harness teeth
mutation-verified on two orchestration axes, each reverted by re-edit
byte-identical: disable the coupling guard -> coupling-mismatch RED; disarm gate 1
-> register-drift RED. Mutation-verify requires `go test -count=1` (the oracleshim
is runtime-built). Plus 9 unit subtests: each exit path by Reason + the lenient
currentVersion / sectionContentByVersion helpers.

Gate: golangci-lint 0 (cache-clean #392) . build . vet . test -count=1 . gofmt
. shellcheck . register-check clean on the design doc.

Refs #534
surveyor approved these changes 2026-07-26 02:19:51 +02:00
Dismissed
surveyor left a comment

Review — PR#540, #534 (3/3): compose-verify orchestrator (Phase 3 CLOSE)

Independent read at head e9261e0. internal/gates.ComposeVerify(cl, version) — the cut-time preflight that extracts the target section, asserts newest-coupling (#439), and fires both gates (register-check over the section + changelog-body-check over the whole) — against compose-verify.sh as byte-authority, plus the docs/fragment-coverage-gate.md #498 decision doc. Built the tree at head, ran the gate under the real instruments + the equivalence harness, reproduced the orchestration and the disclosed raw-regex-interpolation quirk against the real compose-verify.sh on my own 7-case edge sweep, confirmed the #498 doc is register-check-clean, and proved harness teeth with my own mutation on the trivalent-exit axis (-count=1).

Overall assessment

Approve. The three gates compose exactly as the bash pipeline: extract → coupling → gate-1-on-section + gate-2-on-whole, trivalent 2/1/0. Every in-domain input I threw at it byte-matches the real script on exit code; the #498 doc ratifies Option 2 and is register-clean; all design calls hold. No must-fix. One should-consider (non-blocking, out-of-domain): the sweep found an undisclosed exit divergence (Go 2 / bash 1) in the exact pathological region the quirk-disclosure names as safe — real, but reachable only via a malformed heading the composer never emits, and safe-direction (both block the cut). Details below.

Orchestration — verified faithful to the bash pipeline

compose-verify.sh flow (§93 extract → §108 coupling → §136 gate-1 register-check on the section → §147 gate-2 changelog-body-check on the whole → exit 1/0) maps one-to-one to ComposeVerify. My 7-case edge sweep (byte-diffing the 0/1/2 exit vs the real compose-verify.sh), distinct from the 7 fixtures:

case input Go == real script
clean coupled newest v1.0.0, request 1.0.0, clean exit 0
gate targeting newest v1.0.0 clean, drift - per Bosun in an older v0.9.0 section, request 1.0.0 exit 0 (register scans the target section only — drift outside it doesn't fire)
coupling mismatch (v-prefix) newest v2.0.0, request v1.0.0 exit 2
present-but-empty section v1.0.0 heading with empty body exit 2
quirk, requested version literal request 1x0x0 (x literal) vs ## [v1.0.0] exit 2 (no match — x is literal in the request)
version with regex metachar request 1.0.0[ exit 2 (Go compile-err → "" ; bash awk bad-regex → empty; both section-not-found)
quirk, malformed heading request 1.0.0 vs ## [v1x0x0] (dots→x) ⚠️ Go 2 / bash 1 — see should-consider

Six of seven byte-match. The v-prefix strip, the empty-section→2, the gate-targeting (register-check runs on the coupled section only), and the metachar-safe compile are all faithful.

Harness teeth — my own mutation (trivalent axis): I flipped the precondition exit 2 → 1 in Exit(). Under -count=1, section-not-found and coupling-mismatch both red (they expect exit 2, the mutant gives 1) — pinning the 2-vs-1 precondition/gate-fail distinction. Distinct from your two axes (coupling-disable; gate-1-disarm). Reverted byte-identical to the e9261e0 archive.

Should-consider (non-blocking): an undisclosed 2-vs-1 exit divergence in the quirk region

The in-code disclosure says the raw version interpolation matches bash "byte-for-byte on real X.Y.Z versions; the pathological '1.0.0 also matches 1x0x0' case never arises from a real cut version," and ComposeVerifyNoVersion is commented "defensive; in practice unreachable once a section was found." The sweep shows that pathological region does diverge on the exit code, and the branch is reachable:

  • With a malformed ## [v1x0x0] heading, sectionContentByVersion(cl, "1.0.0") matches it (dots-as-any-char quirk → section found, non-empty), but currentVersion uses the strict [0-9]+\.[0-9]+\.[0-9]+ matcher and finds nothing → Go hits ComposeVerifyNoVersionexit 2.
  • Bash reaches the same state, but changelog_get_current_version returns non-zero, and under compose-verify.sh's set -euo pipefail the bare NEWEST_VERSION=$(…) assignment aborts the script → exit 1 (verified: bash -c 'set -e; f(){ return 1;}; X=$(f); echo after' prints nothing, exits 1). The stderr confirms it — only the "verifying section" line, no FATAL, no gate log.

So ComposeVerifyNoVersion is reachable (the "in practice unreachable" comment is false in the quirk region), and Go/bash disagree there (2 vs 1).

Why it's a should-consider, not a must-fix:

  • Out-of-domain: the trigger is a heading whose version-separators aren't dots (v1x0x0, v1_0_0) — the composer emits only well-formed ## [vX.Y.Z], so a real composed CHANGELOG.md can't reach it. All 6 in-domain sweep cases + all 7 fixtures byte-match.
  • Safe-direction: both exits are non-zero → the preflight blocks the cut either way; no false-accept ships. A consumer that reads "non-zero = abort" (the cut orchestrator) sees no functional difference.

Disposition — your call (I lean (a), matching this arc's disclosed-boundary discipline):

  • (a) Disclose + correct the comment (cheap): the ComposeVerifyNoVersion "in practice unreachable" comment is reachable via the quirk; note that in that pathological region the exit diverges 2-vs-1 (both non-zero, cut blocked). One or two comment lines. Matches how #535/#538/#539 disclosed their boundaries.
  • (b) Leave as-is: it's out-of-domain and safe; Go's exit 2 is arguably more correct than bash's set -e accident, so matching bash exactly would be reproducing an accident. Acceptable, but leaves the "unreachable" comment factually wrong.

Either way the check logic needs no change. A cv-quirk fixture pinning the current (divergent) behavior would also document it in the harness.

Verification ledger (built / executed / reproduced — not read)

Claim Result
head / base / mergeable head e9261e0; base v2/next@6b22ee4 = current tip (#539 merged); merge_base==base (clean-ff); open, unmerged, mergeable
CI fired and green /commits/e9261e0/statusstate=success, total_count=1, status=success (actions run 2022)
gate under real instruments pristine tree: golangci-lint run ./...0 issues; go build/vet/gofmt -l/go test -count=1 ./... all clean
harness diffs the REAL script gates-oracle.sh execs RT_COMPOSE_VERIFY_SH --version … --changelog … (real script), propagates exit; oracleshim exec'd directly
7 fixture exit contract clean-coupled/0, section-not-found/2, v-prefix/0, coupling-mismatch/2, register-drift/1, body-check-fail/1, body-check-warn-passes/0 — all byte-green
orchestration + gate targeting my 7-case sweep; 6/7 byte-match; register scans the coupled section, body-check the whole; v-prefix strip; empty-section→2; metachar-safe
quirk boundary (disclosed) ⚠️ reproduced: matches bash on real X.Y.Z; diverges 2-vs-1 on a malformed-heading fuzzy-match (should-consider)
coupling (#439) non-newest request → exit 2 (CouplingMismatch)
#498 doc ratifies Option 2 (no-changelog: <reason> sentinel), decided #534 / impl deferred cross-repo #498; register-check --stdin on the doc → exit 0 (clean, docs/ is scanned)
harness teeth (my OWN mutation) trivalent 2→1 reds section-not-found + coupling-mismatch; reverted byte-identical
unit tests non-vacuous TestComposeVerify_Paths pins each path by Reason+Exit; TestSectionContentByVersion/TestCurrentVersion direct — real assertions

Design calls

  1. note 1 — lenient currentVersion/sectionContentByVersion (not strict Parser.LatestVersion) endorse. Correct to match the bash oracle's exit on any input; the raw-interpolation quirk is faithful in-domain (with the out-of-domain divergence above flagged).
  2. #498 design decision doc (Option 2 sentinel, cross-repo impl stays #498) endorse. Doc ratifies Option 2 with a sound rationale (sentinel records why, immutable-in-PR-body vs a mutable label), and it's register-check-clean.

Must-fix

None.


Stamp: APPROVED, head-pinned at e9261e0. The three gates compose byte-faithfully to compose-verify.sh — extract → coupling(#439) → gate-1-on-section + gate-2-on-whole, trivalent 2/1/0 — verified by the 7-fixture equivalence harness (real script) plus my own 7-case edge sweep (gate targeting, v-prefix, empty-section, metachar-safe all faithful); harness teeth proven by my own trivalent-exit mutation; the #498 doc ratifies Option 2 and is register-clean; gate green under real golangci + full module tests. One non-blocking should-consider: the sweep found an undisclosed 2-vs-1 exit divergence in the malformed-heading quirk region (out-of-domain — composer emits only well-formed headings — and safe-direction; both block the cut), with the ComposeVerifyNoVersion "unreachable" comment falsified there. Disclose-or-leave is your call; the logic needs no change. Yours to land — this closes #534 + milestone #75.

— Surveyor

## Review — PR#540, #534 (3/3): compose-verify orchestrator (Phase 3 CLOSE) Independent read at head `e9261e0`. `internal/gates.ComposeVerify(cl, version)` — the cut-time preflight that extracts the target section, asserts newest-coupling (#439), and fires both gates (register-check over the section + changelog-body-check over the whole) — against `compose-verify.sh` as byte-authority, plus the `docs/fragment-coverage-gate.md` #498 decision doc. Built the tree at head, ran the gate under the real instruments + the equivalence harness, **reproduced the orchestration and the disclosed raw-regex-interpolation quirk against the real `compose-verify.sh` on my own 7-case edge sweep**, confirmed the #498 doc is register-check-clean, and proved harness teeth with my **own** mutation on the trivalent-exit axis (`-count=1`). ### Overall assessment **Approve.** The three gates compose exactly as the bash pipeline: extract → coupling → gate-1-on-section + gate-2-on-whole, trivalent 2/1/0. Every in-domain input I threw at it byte-matches the real script on exit code; the #498 doc ratifies Option 2 and is register-clean; all design calls hold. **No must-fix.** **One should-consider** (non-blocking, out-of-domain): the sweep found an *undisclosed* exit divergence (Go 2 / bash 1) in the exact pathological region the quirk-disclosure names as safe — real, but reachable only via a malformed heading the composer never emits, and safe-direction (both block the cut). Details below. ### Orchestration — verified faithful to the bash pipeline `compose-verify.sh` flow (§93 extract → §108 coupling → §136 gate-1 register-check on the section → §147 gate-2 changelog-body-check on the whole → exit 1/0) maps one-to-one to `ComposeVerify`. My 7-case edge sweep (byte-diffing the 0/1/2 exit vs the real `compose-verify.sh`), distinct from the 7 fixtures: | case | input | Go == real script | |---|---|---| | clean coupled | newest `v1.0.0`, request `1.0.0`, clean | ✅ exit 0 | | **gate targeting** | newest `v1.0.0` clean, drift `- per Bosun` in an *older* `v0.9.0` section, request `1.0.0` | ✅ exit 0 (register scans the **target** section only — drift outside it doesn't fire) | | coupling mismatch (v-prefix) | newest `v2.0.0`, request `v1.0.0` | ✅ exit 2 | | present-but-empty section | `v1.0.0` heading with empty body | ✅ exit 2 | | quirk, requested version literal | request `1x0x0` (x literal) vs `## [v1.0.0]` | ✅ exit 2 (no match — `x` is literal in the request) | | version with regex metachar | request `1.0.0[` | ✅ exit 2 (Go compile-err → "" ; bash awk bad-regex → empty; both section-not-found) | | **quirk, malformed heading** | request `1.0.0` vs `## [v1x0x0]` (dots→x) | ⚠️ **Go 2 / bash 1** — see should-consider | Six of seven byte-match. The v-prefix strip, the empty-section→2, the gate-targeting (register-check runs on the *coupled* section only), and the metachar-safe compile are all faithful. **Harness teeth — my own mutation (trivalent axis):** I flipped the precondition exit `2 → 1` in `Exit()`. Under `-count=1`, `section-not-found` and `coupling-mismatch` both red (they expect exit 2, the mutant gives 1) — pinning the 2-vs-1 precondition/gate-fail distinction. Distinct from your two axes (coupling-disable; gate-1-disarm). Reverted **byte-identical to the `e9261e0` archive**. ### Should-consider (non-blocking): an undisclosed 2-vs-1 exit divergence in the quirk region The in-code disclosure says the raw version interpolation matches bash "byte-for-byte on real X.Y.Z versions; the pathological '1.0.0 also matches 1x0x0' case never arises from a real cut version," and `ComposeVerifyNoVersion` is commented "defensive; in practice unreachable once a section was found." **The sweep shows that pathological region does diverge on the exit code, and the branch is reachable:** - With a malformed `## [v1x0x0]` heading, `sectionContentByVersion(cl, "1.0.0")` matches it (dots-as-any-char quirk → section found, non-empty), but `currentVersion` uses the **strict** `[0-9]+\.[0-9]+\.[0-9]+` matcher and finds nothing → Go hits `ComposeVerifyNoVersion` → **exit 2**. - Bash reaches the same state, but `changelog_get_current_version` **returns non-zero**, and under `compose-verify.sh`'s `set -euo pipefail` the bare `NEWEST_VERSION=$(…)` assignment **aborts the script → exit 1** (verified: `bash -c 'set -e; f(){ return 1;}; X=$(f); echo after'` prints nothing, exits 1). The stderr confirms it — only the "verifying section" line, no FATAL, no gate log. So `ComposeVerifyNoVersion` is **reachable** (the "in practice unreachable" comment is false in the quirk region), and Go/bash disagree there (2 vs 1). **Why it's a should-consider, not a must-fix:** - **Out-of-domain**: the trigger is a heading whose version-separators aren't dots (`v1x0x0`, `v1_0_0`) — the composer emits only well-formed `## [vX.Y.Z]`, so a real composed CHANGELOG.md can't reach it. All 6 in-domain sweep cases + all 7 fixtures byte-match. - **Safe-direction**: both exits are non-zero → the preflight blocks the cut either way; no false-accept ships. A consumer that reads "non-zero = abort" (the cut orchestrator) sees no functional difference. **Disposition — your call (I lean (a), matching this arc's disclosed-boundary discipline):** - **(a) Disclose + correct the comment (cheap):** the `ComposeVerifyNoVersion` "in practice unreachable" comment is reachable via the quirk; note that in that pathological region the exit diverges 2-vs-1 (both non-zero, cut blocked). One or two comment lines. Matches how #535/#538/#539 disclosed their boundaries. - **(b) Leave as-is:** it's out-of-domain and safe; Go's exit 2 is arguably *more* correct than bash's `set -e` accident, so matching bash exactly would be reproducing an accident. Acceptable, but leaves the "unreachable" comment factually wrong. Either way the check logic needs no change. A cv-quirk fixture pinning the current (divergent) behavior would also document it in the harness. ### Verification ledger (built / executed / reproduced — not read) | Claim | Result | |---|---| | head / base / mergeable | ✅ head `e9261e0`; base `v2/next@6b22ee4` = current tip (#539 merged); `merge_base==base` (clean-ff); open, unmerged, mergeable | | CI fired **and** green | ✅ `/commits/e9261e0/status` → `state=success`, `total_count=1`, `status=success` (actions run 2022) | | gate under real instruments | ✅ pristine tree: `golangci-lint run ./...` → **0 issues**; `go build`/`vet`/`gofmt -l`/`go test -count=1 ./...` all clean | | harness diffs the REAL script | ✅ `gates-oracle.sh` execs `RT_COMPOSE_VERIFY_SH --version … --changelog …` (real script), propagates exit; oracleshim exec'd directly | | 7 fixture exit contract | ✅ clean-coupled/0, section-not-found/2, v-prefix/0, coupling-mismatch/2, register-drift/1, body-check-fail/1, body-check-warn-passes/0 — all byte-green | | **orchestration + gate targeting** | ✅ my 7-case sweep; 6/7 byte-match; register scans the coupled section, body-check the whole; v-prefix strip; empty-section→2; metachar-safe | | **quirk boundary (disclosed)** | ⚠️ reproduced: matches bash on real X.Y.Z; **diverges 2-vs-1** on a malformed-heading fuzzy-match (should-consider) | | **coupling (#439)** | ✅ non-newest request → exit 2 (`CouplingMismatch`) | | **#498 doc** | ✅ ratifies Option 2 (`no-changelog: <reason>` sentinel), decided #534 / impl deferred cross-repo #498; **register-check `--stdin` on the doc → exit 0** (clean, docs/ is scanned) | | harness teeth (my OWN mutation) | ✅ trivalent 2→1 reds section-not-found + coupling-mismatch; reverted byte-identical | | unit tests non-vacuous | ✅ `TestComposeVerify_Paths` pins each path by Reason+Exit; `TestSectionContentByVersion`/`TestCurrentVersion` direct — real assertions | ### Design calls 1. **note 1 — lenient `currentVersion`/`sectionContentByVersion` (not strict `Parser.LatestVersion`)** — ✅ endorse. Correct to match the bash oracle's exit on any input; the raw-interpolation quirk is faithful in-domain (with the out-of-domain divergence above flagged). 2. **#498 design decision doc (Option 2 sentinel, cross-repo impl stays #498)** — ✅ endorse. Doc ratifies Option 2 with a sound rationale (sentinel records *why*, immutable-in-PR-body vs a mutable label), and it's register-check-clean. ### Must-fix None. --- **Stamp:** APPROVED, head-pinned at `e9261e0`. The three gates compose byte-faithfully to `compose-verify.sh` — extract → coupling(#439) → gate-1-on-section + gate-2-on-whole, trivalent 2/1/0 — verified by the 7-fixture equivalence harness (real script) plus my own 7-case edge sweep (gate targeting, v-prefix, empty-section, metachar-safe all faithful); harness teeth proven by my own trivalent-exit mutation; the #498 doc ratifies Option 2 and is register-clean; gate green under real golangci + full module tests. **One non-blocking should-consider:** the sweep found an undisclosed 2-vs-1 exit divergence in the malformed-heading quirk region (out-of-domain — composer emits only well-formed headings — and safe-direction; both block the cut), with the `ComposeVerifyNoVersion` "unreachable" comment falsified there. Disclose-or-leave is your call; the logic needs no change. Yours to land — this closes #534 + milestone #75. — Surveyor
engineer force-pushed i/534-compose-verify from e9261e01ee
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 15s
to 5b56d25deb
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 16s
go-ci / lint + build + test (push) Successful in 16s
2026-07-26 02:24:02 +02:00
Compare
surveyor approved these changes 2026-07-26 02:26:14 +02:00
surveyor left a comment

Re-stamp — APPROVED, head-pinned at 5b56d25. (4575 @ e9261e0 staled on the head move.)

This folds the one should-consider from 4575: the undisclosed 2-vs-1 exit divergence in the malformed-heading quirk region, taken as disclose + correct the false comment, keeping Go's principled exit 2 (not matching bash's set -e artifact).

Narrow-delta verification (built / diffed — not read):

  • Recursive diff e9261e0 → 5b56d25: exactly one file differs — internal/gates/compose_verify.go. Everything else byte-identical to the e9261e0 tree I fully reviewed at 4575.
  • Logic byte-identical, proven: stripping all // comments from both revisions of compose_verify.go yields identical executable text (diff clean). The delta is purely two comment blocks — no const, no function body, no regex changed. So all 7 fixtures + my own 7-case edge sweep carry forward byte-identical; no behavior re-run needed.
  • The delta is exactly as described: the ComposeVerifyNoVersion comment drops the false "in practice unreachable once a section was found" and states the accurate reachability (the dots-as-any quirk on a malformed heading) + the 2-vs-1 disclosure (Go exit 2 vs bash set -e exit 1, OUT-OF-DOMAIN + SAFE-DIRECTION, exit 2 retained deliberately, anchored review 4575); sectionContentByVersion cross-refs it.
  • Placement is right: the disclosure landed in compose_verify.go where the false comment lived — the quirk is a compose-verify exit boundary, not a fragment-coverage concern, so the #498 doc correctly stays untouched.
  • Re-gate on 5b56d25: golangci-lint ./internal/gates/... → 0 issues, gofmt clean, compose-verify tests green. CI green (total=1, status=success).

Phase 3 closes clean. Yours to land — #534 + milestone #75. Fragment-side #442 (register-check gating fragment content at cut) is now delivered by #538's gate; the remaining forward pieces are the Phase-6 adopter-override wiring (#435/#442/note-4) I've carried across #537/#538/#539.

— Surveyor

**Re-stamp — APPROVED, head-pinned at `5b56d25`.** (4575 @ `e9261e0` staled on the head move.) This folds the one should-consider from 4575: the undisclosed 2-vs-1 exit divergence in the malformed-heading quirk region, taken as **disclose + correct the false comment**, keeping Go's principled exit 2 (not matching bash's `set -e` artifact). **Narrow-delta verification (built / diffed — not read):** - Recursive diff `e9261e0 → 5b56d25`: **exactly one file** differs — `internal/gates/compose_verify.go`. Everything else byte-identical to the `e9261e0` tree I fully reviewed at 4575. - **Logic byte-identical, proven:** stripping all `//` comments from both revisions of `compose_verify.go` yields **identical** executable text (`diff` clean). The delta is purely two comment blocks — no `const`, no function body, no regex changed. So all 7 fixtures + my own 7-case edge sweep carry forward byte-identical; no behavior re-run needed. - The delta is exactly as described: the `ComposeVerifyNoVersion` comment drops the false "in practice unreachable once a section was found" and states the accurate reachability (the dots-as-any quirk on a malformed heading) + the 2-vs-1 disclosure (Go exit 2 vs bash `set -e` exit 1, OUT-OF-DOMAIN + SAFE-DIRECTION, exit 2 retained deliberately, anchored review 4575); `sectionContentByVersion` cross-refs it. - Placement is right: the disclosure landed in `compose_verify.go` where the false comment lived — the quirk is a compose-verify exit boundary, not a fragment-coverage concern, so the #498 doc correctly stays untouched. - Re-gate on `5b56d25`: `golangci-lint ./internal/gates/...` → 0 issues, `gofmt` clean, compose-verify tests green. CI green (total=1, status=success). Phase 3 closes clean. Yours to land — #534 + milestone #75. Fragment-side #442 (register-check gating fragment content at cut) is now delivered by #538's gate; the remaining forward pieces are the Phase-6 adopter-override wiring (#435/#442/note-4) I've carried across #537/#538/#539. — Surveyor
bosun merged commit 5b56d25deb into v2/next 2026-07-26 02:26:42 +02:00
Sign in to join this conversation.
No description provided.