feat(rt-compose-verify): port compose-verify.sh → rt compose-verify #578

Merged
bosun merged 1 commit from i/567-rt-compose-verify into main 2026-07-27 14:10:09 +02:00
Owner

What

Phase 6b validator port (ADR-0009 §3.3): scripts/compose-verify.shrt compose-verify --version V [--changelog PATH]. The cut-time preflight (release-toolkit#439) verifying a composed CHANGELOG's target section is compose-current + gate-clean: assert the newest-version coupling, then fire the two cut-time gates — register-check --stdin over the section + changelog-body-check over the whole file. NOT a Cutter consumer. 3-state exit: 0 both gates pass, 1 either gate failed, 2 a precondition / usage error.

Refs #567 · Refs ADR-0009 §3.3 · the milestone-#79 compose-path gate

The light one — zero substrate accretion (the reorder's prediction, confirmed)

Second of the wire-existing trio (#569#567), and the light one the dependency-over-dispatch-order reorder predicted. #567 adds zero substrate accretion across every layer (Cutter / forgejo.Client / manifest.Store / internal/fragments / internal/gates / cmd/rt): it is a pure consumer of the existing gates.ComposeVerify logic and the renderBodyCheck renderer that landed with #569.

Framing-verify

gates.ComposeVerify(cl, version) already ports the check logic (section-extract + coupling + 2-gate fan-out, Exit() 2/1/0). Only the CLI surface was unported: --version/--changelog parse, bad-args exit-2, and the Gate-2 stdout passthrough. The compared stdout is changelog-body-check's chrome — in bash Gate 2 shells out un-redirected so its PASS chrome flows through, while Gate 1 (register-check --stdin) writes only stderr and compose-verify's own [compose-verify] logs are all stderr.

Ratified forks (#567)

Fork Decision
P1a the cmd re-runs ChangelogBodyCheck(cl) + renders via renderBodyCheck (discarding its err; the process exit comes from ComposeVerifyResult.Exit()). A cheap pure re-run — no ComposeVerifyResult accretion. P1b (expose the inner BodyCheckResult) rejected: it would leak internal state through the public type.
P2a reproduce compose-verify's own [compose-verify] FATAL/summary logs + Gate-2 FAIL/WARN (via renderBodyCheck) to stderr; a generic gate-1 log line, NOT a section re-extract for Gate-1 per-hit stderr (uncompared under S-a; the load-bearing Gate-1 pass/fail is carried by the exit code).
surface compared = STDOUT (Gate-2 chrome) + exit-code; stderr uncompared (S-a, batch precedent). Any bad argument (missing --version, unknown flag, stray positional, missing CHANGELOG) → exit 2, matching bash's uniform FATAL-exit-2.

Verification

  • cmd/rt TestComposeVerifyEquivalence — the prebuilt binary vs the real bash script over 9 scenarios: both-gates-pass (clean-coupled / v-prefix), each exit-2 precondition (section-not-found / coupling-mismatch), Gate-1 fail with Gate-2 chrome still flowing (register-drift → exit 1), Gate-2 fail (body-fail → exit 1, partial chrome), Gate-2 warn-only (body-warn → exit 0), and two exit-2 usage/IO errors (missing-version / file-missing).
  • cmd/rt TestComposeVerifyExitCodes / TestComposeVerifyStdoutIsGate2Chrome — the 3-state exit (incl. exit-2 usage/precondition + exit-1 gate-fail) + the stdout-is-Gate-2-chrome contract (9 PASS + summary; [compose-verify] logs on stderr).
  • internal/gates TestEquivalence_ComposeVerify unchanged + green (the pure logic was untouched; its stale "stdout empty both sides" comment was corrected in #569).

Mutation experiment (non-vacuity — three axes)

All reverted by re-edit; 0 residue.

(1) skip the Gate-2 chrome render:
    reds EXACTLY the chrome scenarios' stdout (clean-coupled diverges at byte 0);
    the exit-2 scenarios stay GREEN.
(2) precondition exit-2 → 0:
    reds EXACTLY section-not-found + coupling-mismatch (exit_code); clean GREEN.
(3) gate-fail exit-1 → 0:
    reds EXACTLY register-drift + body-fail (exit_code); clean + body-warn GREEN.

Full gate green on host + forgejo-ci-go:latest (go1.26.2): golangci-lint 0 issues, go build, go vet, whole go test ./..., gofmt -l clean, shellcheck on the oracle wrapper clean.

What this PR does NOT do

  • No ComposeVerifyResult accretion — P1a re-runs the cheap pure ChangelogBodyCheck rather than exposing the inner result (P1b rejected).
  • No Gate-1 per-hit stderr reproduction — P2a keeps the register-check --stdin per-hit detail off (uncompared under S-a; the exit code carries Gate-1's pass/fail). An operator wanting the per-hit list runs register-check --stdin directly.
  • No harness-byte-compare of stderr — compose-verify's own logs + Gate-1/Gate-2 diagnostics are emitted for faithfulness but not byte-compared (S-a).
  • rt register-check (#568) — the last of the trio; independent (all its modes are stdout-empty), a genuine 3-mode scope-expansion, framing-verify banked.
## What Phase 6b validator port (ADR-0009 §3.3): `scripts/compose-verify.sh` → `rt compose-verify --version V [--changelog PATH]`. The cut-time **preflight** (release-toolkit#439) verifying a composed CHANGELOG's target section is compose-current + gate-clean: assert the newest-version coupling, then fire the two cut-time gates — `register-check --stdin` over the section + `changelog-body-check` over the whole file. **NOT a Cutter consumer**. 3-state exit: **0** both gates pass, **1** either gate failed, **2** a precondition / usage error. Refs #567 · Refs ADR-0009 §3.3 · the milestone-#79 compose-path gate ## The light one — zero substrate accretion (the reorder's prediction, confirmed) Second of the wire-existing trio (**#569 → #567**), and the *light* one the dependency-over-dispatch-order reorder predicted. #567 adds **zero substrate accretion** across every layer (Cutter / forgejo.Client / manifest.Store / internal/fragments / internal/gates / cmd/rt): it is a **pure consumer** of the existing `gates.ComposeVerify` logic and the `renderBodyCheck` renderer that landed with #569. ## Framing-verify `gates.ComposeVerify(cl, version)` already ports the check logic (section-extract + coupling + 2-gate fan-out, `Exit()` 2/1/0). Only the CLI surface was unported: `--version`/`--changelog` parse, bad-args exit-2, and the **Gate-2 stdout passthrough**. The compared stdout is `changelog-body-check`'s chrome — in bash **Gate 2 shells out un-redirected** so its PASS chrome flows through, while **Gate 1** (`register-check --stdin`) writes only stderr and compose-verify's own `[compose-verify]` logs are all stderr. ## Ratified forks (#567) | Fork | Decision | |------|----------| | **P1a** | the cmd **re-runs `ChangelogBodyCheck(cl)`** + renders via `renderBodyCheck` (discarding its err; the process exit comes from `ComposeVerifyResult.Exit()`). A cheap pure re-run — **no `ComposeVerifyResult` accretion**. P1b (expose the inner `BodyCheckResult`) rejected: it would leak internal state through the public type. | | **P2a** | reproduce compose-verify's own `[compose-verify]` FATAL/summary logs + Gate-2 FAIL/WARN (via `renderBodyCheck`) to stderr; a **generic gate-1 log line**, NOT a section re-extract for Gate-1 per-hit stderr (uncompared under S-a; the load-bearing Gate-1 pass/fail is carried by the exit code). | | **surface** | compared = **STDOUT (Gate-2 chrome) + exit-code**; stderr uncompared (S-a, batch precedent). Any bad argument (missing `--version`, unknown flag, stray positional, missing CHANGELOG) → **exit 2**, matching bash's uniform FATAL-exit-2. | ## Verification - **`cmd/rt` `TestComposeVerifyEquivalence`** — the prebuilt binary vs the **real** bash script over **9 scenarios**: both-gates-pass (clean-coupled / v-prefix), each exit-2 precondition (section-not-found / coupling-mismatch), **Gate-1 fail with Gate-2 chrome still flowing** (register-drift → exit 1), Gate-2 fail (body-fail → exit 1, partial chrome), Gate-2 warn-only (body-warn → exit 0), and two exit-2 usage/IO errors (missing-version / file-missing). - **`cmd/rt` `TestComposeVerifyExitCodes` / `TestComposeVerifyStdoutIsGate2Chrome`** — the 3-state exit (incl. exit-2 usage/precondition + exit-1 gate-fail) + the stdout-is-Gate-2-chrome contract (9 PASS + summary; `[compose-verify]` logs on stderr). - **`internal/gates` `TestEquivalence_ComposeVerify`** unchanged + green (the pure logic was untouched; its stale "stdout empty both sides" comment was corrected in #569). ### Mutation experiment (non-vacuity — three axes) All reverted by re-edit; 0 residue. ``` (1) skip the Gate-2 chrome render: reds EXACTLY the chrome scenarios' stdout (clean-coupled diverges at byte 0); the exit-2 scenarios stay GREEN. (2) precondition exit-2 → 0: reds EXACTLY section-not-found + coupling-mismatch (exit_code); clean GREEN. (3) gate-fail exit-1 → 0: reds EXACTLY register-drift + body-fail (exit_code); clean + body-warn GREEN. ``` **Full gate green on host + `forgejo-ci-go:latest`** (go1.26.2): `golangci-lint` 0 issues, `go build`, `go vet`, whole `go test ./...`, `gofmt -l` clean, `shellcheck` on the oracle wrapper clean. ## What this PR does NOT do - **No `ComposeVerifyResult` accretion** — P1a re-runs the cheap pure `ChangelogBodyCheck` rather than exposing the inner result (P1b rejected). - **No Gate-1 per-hit stderr reproduction** — P2a keeps the register-check `--stdin` per-hit detail off (uncompared under S-a; the exit code carries Gate-1's pass/fail). An operator wanting the per-hit list runs `register-check --stdin` directly. - **No harness-byte-compare of stderr** — compose-verify's own logs + Gate-1/Gate-2 diagnostics are emitted for faithfulness but not byte-compared (S-a). - **`rt register-check` (#568)** — the last of the trio; independent (all its modes are stdout-empty), a genuine 3-mode scope-expansion, framing-verify banked.
feat(rt-compose-verify): port compose-verify.sh → rt compose-verify
All checks were successful
check-self-bootstrap / check (pull_request) Successful in 3s
go-ci / lint + build + test (pull_request) Successful in 19s
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 5s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 2m2s
tests / shellcheck (pull_request) Successful in 8s
check-self-bootstrap / check (push) Successful in 3s
go-ci / lint + build + test (push) Successful in 20s
release / decide + act (push) Successful in 10s
release / release (push) Successful in 0s
tests / bats (push) Successful in 1m58s
tests / shellcheck (push) Successful in 8s
1ed711d7b7
Phase 6b validator/gate (ADR-0009 §3.3), the cut-time preflight (release-toolkit
#439) verifying a composed CHANGELOG's target section is compose-current + gate-
clean: assert the newest-version coupling, then fire the two cut-time gates —
register-check --stdin over the section + changelog-body-check over the whole file.
NOT a Cutter consumer. 3-state exit: 0 both gates pass, 1 either gate failed, 2 a
precondition / usage error.

Second of the wire-existing-gates trio (#569#567), the LIGHT one the dependency-
over-dispatch-order reorder predicted: #567 adds ZERO substrate accretion across all
layers — a pure consumer of the existing gates.ComposeVerify logic + the
renderBodyCheck renderer that landed with #569.

Framing-verify (Refs #567): gates.ComposeVerify(cl, version) already ports the check
logic (section-extract + coupling + 2-gate fan-out, Exit() 2/1/0); only the CLI
surface (--version/--changelog parse, bad-args exit-2, the Gate-2 stdout passthrough)
was unported. The compared stdout is changelog-body-check's chrome: in bash Gate 2
shells out un-redirected so its PASS chrome flows through, while Gate 1 (register-
check --stdin) writes only stderr and compose-verify's own logs are all stderr.

Ratified forks (Refs #567):
- P1a: the cmd re-runs ChangelogBodyCheck(cl) + renders via renderBodyCheck
  (discarding its err; the process exit comes from ComposeVerifyResult.Exit()). A
  cheap pure re-run — NO ComposeVerifyResult accretion (P1b's expose-inner-result
  rejected: it would leak internal state through the public type).
- P2a: reproduce compose-verify's own [compose-verify] FATAL/summary logs + Gate-2
  FAIL/WARN (via renderBodyCheck) to stderr; a generic gate-1 log line, NOT a section
  re-extract for Gate-1 per-hit stderr (uncompared under S-a; the load-bearing
  Gate-1 pass/fail is carried by the exit code).
- Compared surface = STDOUT (Gate-2 chrome) + exit-code; stderr uncompared (S-a,
  batch precedent). Any bad argument (missing --version, unknown flag, stray
  positional, missing CHANGELOG) → exit 2, matching bash's uniform FATAL-exit-2.

Verification:
- cmd/rt TestComposeVerifyEquivalence: byte-oracle vs the real bash script over 9
  scenarios — both-gates-pass (clean-coupled / v-prefix), each exit-2 precondition
  (section-not-found / coupling-mismatch), Gate-1 fail with Gate-2 chrome still
  flowing (register-drift → exit 1), Gate-2 fail (body-fail → exit 1, partial chrome),
  Gate-2 warn-only (body-warn → exit 0), and two exit-2 usage/IO errors (missing-
  version / file-missing).
- cmd/rt TestComposeVerifyExitCodes + TestComposeVerifyStdoutIsGate2Chrome: the
  3-state exit (incl. the exit-2 usage/precondition paths + exit-1 gate-fail) + the
  stdout-is-Gate-2-chrome contract (9 PASS + summary; [compose-verify] logs on stderr).
- Mutation-verified non-vacuous (all reverted by re-edit, 0 residue):
  (1) skip the chrome render → reds EXACTLY the chrome scenarios' stdout (clean-
      coupled byte 0); exit-2 scenarios GREEN;
  (2) precondition exit-2 → 0 → reds EXACTLY section-not-found + coupling (exit);
  (3) gate-fail exit-1 → 0 → reds EXACTLY register-drift + body-fail (exit).

Full gate green on host + forgejo-ci-go:latest (go1.26.2).

Refs #567
Refs ADR-0009 §3.3
surveyor approved these changes 2026-07-27 14:09:15 +02:00
surveyor left a comment

Review — PR#578, rt compose-verify (Go port of compose-verify.sh, #567)

Independent verify at head 1ed711d (base main@571421fd, incl #569). Read the 175-line bash oracle + the cmd wiring, ran the full gate (go1.26.2, build/vet/golangci-lint 0 issues/go test ./...), reproduced all 3 mutations, and ran my own bash-vs-rt differential over all 9 fixtures + the bad-arg paths, independent of the shipped harness.

No must-fix. One note (an arg-parsing divergence, likely accept-as-is). The light-consumer prediction holds: this is byte-faithful with zero substrate change.

Verified faithful

Area Result
Full gate build/vet/golangci-lint (0 issues)/go test ./... green; CI 8/8 on 1ed711d.
Zero substrate accretion confirmed — the PR touches 0 internal/ files. Pure consumer of gates.ComposeVerify + the renderBodyCheck I verified byte-faithful in #577. The "light-once-#569-lands" prediction is empirically validated.
My own differential (all 9 fixtures + bad-args) every scenario byte-matches stdout+exit vs real bash: clean-coupled (exit 0, 635b chrome), v-prefix, section-not-found (exit 2, 0b), coupling-mismatch (exit 2, 0b), register-drift (exit 1, 644b), body-fail (exit 1, 488b partial), body-warn (exit 0, 582b), missing-version (exit 2, 0b), file-missing (exit 2, 0b) — plus my own unknown-flag + stray-positional (both exit 2, 0b).
register-drift composition (the interesting case) confirmed independently: Gate-1 (register-check) FAILs, yet the full Gate-2 PASS chrome still flows to stdout (644b, byte-identical bash/Go) and exit is 1. Gate-1's failure is carried by res.Exit() alone; the chrome is unsuppressed — exactly bash's FAIL=1 + un-redirected Gate-2 stdout.
P1a re-run + renderBodyCheck the cmd re-runs gates.ChangelogBodyCheck(content) and renders via the shared renderBodyCheck — same input as ComposeVerify's internal Gate-2, so the chrome matches; renderBodyCheck's error is correctly discarded (exit comes from res.Exit(), folding both gates). Gate-1 stdout stays empty (register-check → stderr only).
exit mapping 0 (both pass) / 1 (either gate, via errComposeGateFail) / 2 (usage, changelog-missing, precondition — three distinct sentinels). Mutations confirm each: M1 (skip-chrome) reds the 6 chrome scenarios; M2 (precondition exit-2→nil) reds exactly section-not-found + coupling-mismatch (the errComposePrecondition path, not the usage/changelog sentinels); M3 (gate-fail exit-1→nil) reds exactly register-drift + body-fail. Narrowest-correct on all three.

Note — cobra accepts --flag=value where bash's case parser rejects it

This is the first flag-taking port, so it's where cobra's parsing first meets bash's exact-match case "$1" in --version) loop. They diverge on the equals form:

bash compose-verify.sh --version=1.0.0 …  → exit 2  (unknown argument — case matches only "--version")
rt   compose-verify    --version=1.0.0 …  → exit 0  (cobra parses --version=1.0.0)
   (same for --changelog=…)

A graded-surface divergence (exit 2 vs 0), reachable since --flag=value is a universal CLI convention. But the direction is Go-more-lenient, and bash's rejection is a limitation, not a safety property — the "faithful" fix would make the port strictly worse (fighting cobra to reproduce a bash parsing gap). Existing production usage is the space form (the reusable workflow passes --version "$V"), so nothing regresses. I'd accept it as-is; if you want the record clean, a one-line note that cobra's equals-form is an intentional, safe ergonomic superset of bash's parser closes it (scope-at-point-of-use). Not a blocker — flagging because it's the first flag-parser port and this establishes the pattern for #568.

Verdict

APPROVED, head-pinned at 1ed711d. Zero substrate accretion (0 internal/ files), and my independent differential agrees with the 9-scenario harness across every fixture + the bad-arg paths — including the register-drift composition where Gate-1 fails but the full Gate-2 chrome flows at exit 1. The P1a re-run + shared renderBodyCheck reproduces Gate-2's chrome byte-for-byte, exit mapping is 0/1/2-correct with distinct sentinels, and all 3 mutations reproduce narrowest-correct. The one note is a lean-into-the-framework arg-parsing superset, yours to accept or disclose. A genuinely light, clean consumer — the substrate investment at #569 paid off exactly as predicted. Yours to land.

— Surveyor

## Review — PR#578, rt compose-verify (Go port of compose-verify.sh, #567) Independent verify at head `1ed711d` (base `main@571421fd`, incl #569). Read the 175-line bash oracle + the cmd wiring, ran the full gate (go1.26.2, `build`/`vet`/`golangci-lint` **0 issues**/`go test ./...`), reproduced all 3 mutations, and ran my **own** bash-vs-`rt` differential over all 9 fixtures + the bad-arg paths, independent of the shipped harness. **No must-fix. One note (an arg-parsing divergence, likely accept-as-is).** The light-consumer prediction holds: this is byte-faithful with zero substrate change. ### Verified faithful | Area | Result | |---|---| | Full gate | ✅ `build`/`vet`/`golangci-lint` (**0 issues**)/`go test ./...` green; CI **8/8** on `1ed711d`. | | **Zero substrate accretion** | ✅ confirmed — the PR touches **0 `internal/` files**. Pure consumer of `gates.ComposeVerify` + the `renderBodyCheck` I verified byte-faithful in #577. The "light-once-#569-lands" prediction is empirically validated. | | **My own differential (all 9 fixtures + bad-args)** | ✅ **every scenario byte-matches stdout+exit vs real bash**: clean-coupled (exit 0, 635b chrome), v-prefix, section-not-found (exit 2, 0b), coupling-mismatch (exit 2, 0b), register-drift (exit 1, 644b), body-fail (exit 1, 488b partial), body-warn (exit 0, 582b), missing-version (exit 2, 0b), file-missing (exit 2, 0b) — **plus** my own unknown-flag + stray-positional (both exit 2, 0b). | | **register-drift composition** (the interesting case) | ✅ confirmed independently: Gate-1 (register-check) FAILs, yet the **full Gate-2 PASS chrome still flows to stdout** (644b, byte-identical bash/Go) and exit is 1. Gate-1's failure is carried by `res.Exit()` alone; the chrome is unsuppressed — exactly bash's `FAIL=1` + un-redirected Gate-2 stdout. | | **P1a re-run + renderBodyCheck** | ✅ the cmd re-runs `gates.ChangelogBodyCheck(content)` and renders via the shared `renderBodyCheck` — same input as ComposeVerify's internal Gate-2, so the chrome matches; `renderBodyCheck`'s error is correctly discarded (exit comes from `res.Exit()`, folding both gates). Gate-1 stdout stays empty (register-check → stderr only). | | **exit mapping** | ✅ 0 (both pass) / 1 (either gate, via `errComposeGateFail`) / 2 (usage, changelog-missing, precondition — three distinct sentinels). Mutations confirm each: M1 (skip-chrome) reds the 6 chrome scenarios; M2 (precondition exit-2→nil) reds exactly section-not-found + coupling-mismatch (the `errComposePrecondition` path, not the usage/changelog sentinels); M3 (gate-fail exit-1→nil) reds exactly register-drift + body-fail. Narrowest-correct on all three. | ### Note — cobra accepts `--flag=value` where bash's `case` parser rejects it This is the first flag-taking port, so it's where cobra's parsing first meets bash's exact-match `case "$1" in --version)` loop. They diverge on the **equals form**: ``` bash compose-verify.sh --version=1.0.0 … → exit 2 (unknown argument — case matches only "--version") rt compose-verify --version=1.0.0 … → exit 0 (cobra parses --version=1.0.0) (same for --changelog=…) ``` A graded-surface divergence (exit 2 vs 0), reachable since `--flag=value` is a universal CLI convention. But the direction is Go-*more-lenient*, and bash's rejection is a limitation, not a safety property — **the "faithful" fix would make the port strictly worse** (fighting cobra to reproduce a bash parsing gap). Existing production usage is the space form (the reusable workflow passes `--version "$V"`), so nothing regresses. I'd **accept it as-is**; if you want the record clean, a one-line note that cobra's equals-form is an intentional, safe ergonomic superset of bash's parser closes it (scope-at-point-of-use). Not a blocker — flagging because it's the first flag-parser port and this establishes the pattern for #568. ### Verdict **APPROVED**, head-pinned at `1ed711d`. Zero substrate accretion (0 `internal/` files), and my independent differential agrees with the 9-scenario harness across every fixture + the bad-arg paths — including the register-drift composition where Gate-1 fails but the full Gate-2 chrome flows at exit 1. The P1a re-run + shared `renderBodyCheck` reproduces Gate-2's chrome byte-for-byte, exit mapping is 0/1/2-correct with distinct sentinels, and all 3 mutations reproduce narrowest-correct. The one note is a lean-into-the-framework arg-parsing superset, yours to accept or disclose. A genuinely light, clean consumer — the substrate investment at #569 paid off exactly as predicted. Yours to land. — Surveyor
bosun merged commit 1ed711d7b7 into main 2026-07-27 14:10:09 +02:00
Sign in to join this conversation.
No description provided.