feat(rt-decide): port release-decide.sh → rt decide (826 LOC, 3-layer safeguard) #555

Closed
opened 2026-07-27 01:04:12 +02:00 by bosun · 1 comment
Owner

Port release-decide.sh (826 LOC, 3-layer safeguard) to rt decide subcommand. Last Phase-6 orchestrator port — closes the milestone when landed.

Scope

  • Port scripts/release-decide.sh to internal/decide/*.go + thin cmd/rt/decide.go cobra wrapper (F3-ratified structure — mirrors internal/prep)
  • Walk git log <last_released_sha>..HEAD per manifest state
  • Decide mode: noop / update / cut (per subject-regex + Forgejo PR API safeguards for cut path)
  • NOT a Cutter consumer — pure-decision port, framing-verify-confirmed. 3/5 Cutter refinement count unaffected.
  • forgejo.Client accretion required (see F2 note below) — separate substrate from Cutter
  • Events retire (see F1 note below) — milestone §5-sanctioned "AC-retire-with-reason for the events subsystem"
  • TDD against bash byte-oracle via #503 harness (F5-split: ~35 git/manifest/dry-run bats → equivalence harness; ~10 live-API bats → unit tests with injected fake forge + bash bats)

AC

  • rt decide compiles + all decide modes function correctly (noop / update / cut)
  • Unit tests + equivalence-harness cases green vs release-decide.sh byte-oracle (stdout + exit byte-compare on ~35 git/manifest/dry-run cases)
  • Live-API path tests green via unit tests with injected fake forge (~10 cases: Layer 2/3 fail, bump-label)
  • 3-layer safeguard preserved (subject-regex + PR API Layer 2 head.label + PR API Layer 3 .user.login + manifest state)
  • forgejo.Client additive refinements landed: PullRequest.User field (carries .user.login; restated 2026-07-30 -- shipped as User, not the design-time name Author), PullRequest.Labels field, ListPRs(state=open) method with startswith-tolerance filter
  • Events emit calls retired with substrate-of-record comment (matches sibling orchestrators + milestone §5 sanction)
  • go vet ./... + golangci-lint run clean

Framing-verify ratification notes (2026-07-27, bus 48e1)

Engineer's framing-verify (bus 48e1) confirmed core framing (not a Cutter consumer, 3/5 count unaffected) and surfaced 3 ratifiable forks + 2 implementer calls.

F1 — Events retire (RATIFIED)

Substrate finding: release-decide.sh emits 4 event_emit cut_decided calls. All 4 sibling orchestrators (#556 prep + #557 release + #558 manifest-check + #559 prune-rc-tags) emit ZERO events at cmd/rt. The events pkg is stderr-only observability; equivalence harness compares STDOUT — so events are byte-invisible on the compared surface.

Milestone #78 §5 EXPLICITLY sanctions: "AC-retire-with-reason for the events subsystem"

Ratified (Bosun 2026-07-27): retire the 4 event_emit calls in the port with substrate-of-record comment naming the milestone §5 sanction + sibling precedent. Not a weakening — events were never on the byte-compared surface.

F2 — forgejo.Client accretion (RATIFIED, sub-fork (a))

Substrate finding: 3-layer safeguard + bump-label need surface the client lacks:

  • Layer 3 reads .user.login (need PullRequest.Author field)
  • Step-4 bump-label reads open-PRs .labels[].name (need PullRequest.Labels field + list-open-PRs method)
  • Already exist: Layer 2 head.label ✓ + FindPRByMergeSHA ✓

Substrate-of-record correction (2026-07-30, AC-audit per Bosun d214): the field shipped as PullRequest.User User (json:"user", internal/forgejo/interface.go:36), consumed at internal/decide/decide.go:441 (pr.User.Login == d.cfg.ReleaseAuthor). The design-time name PullRequest.Author above is left standing as the historical ratification record -- the capability is unchanged, only the identifier differs. AC5 has been restated to the shipped name so a later reader does not re-derive the mis-worded assertion from this note.

Sub-fork on list method shape:

  • (a) — RATIFIED: faithful ListPRs(state=open) + filter-startswith-first like bash (honest port; preserves bash's startswith tolerance)
  • (b) — REJECTED: extend FindPRByHead to carry labels (exact-match, loses bash's startswith tolerance) — silently narrows behavior + overloads FindPRByHead with side-purpose. Scope-at-point-of-use failure mode per /srv/CLAUDE.md § Mechanism design (type describes intended scope while impl grows hidden second concern)

Reasoning for (a): preserves bash tolerance (byte-oracle equivalence gate needs startswith); properly-named method with clear semantics; keeps FindPRByHead single-purpose. Reuse-over-narrowing pattern consistent with #557 axis 2 + #558 fork B + #559 re-frame.

F3 — internal/decide package (RATIFIED)

Structure: internal/decide pkg (injectable GitRunner + forge) + thin cmd/rt/decide.go cobra wrapper. Exactly mirrors internal/prep + cmd/rt/prep.go — precedent-backed by working #556 shape. Reuses semver/fragments/cc/config/manifest primitives.

Substrate refinement pattern (emerging)

Each orchestrator port surfaces necessary substrate additions in the primitive it consumes:

  • #556 (rt-prep): 2 Cutter refinements (Request.Composed additive-optional; Deps.Manifest permitted nil)
  • #557 (rt-release): 1 Cutter refinement (Fire.CreateReleaseRequest.Draft additive-optional + idempotency honor)
  • #558 (rt-manifest-check): 0 Cutter, 1 manifest.Store refinement (verbose CheckDesyncVerbose variant)
  • #559 (rt-prune-rc-tags): 0 refinements anywhere (pure orchestrator, pre-existing primitives)
  • #555 (rt-decide, this): 0 Cutter, 3 forgejo.Client additive additions (Author + Labels fields + ListPRs method)

Substrate-refinement-per-port is the natural §3.3 shape — interfaces refined as reality intrudes at the primitive that reality actually pushes against. Cutter 3/5 count stays firm through #555; substrate refinements accumulate in the specific primitive that needs them, not centralized.

Implementer calls (noted, not ratifying)

  • F4: #380 tag-vs-sha reuses CheckTagResolves condition but emits decide's own A/B/C resolution message — foldable. Engineer-owned.
  • F5: coverage-surface split (~35 bats → equivalence harness; ~10 live-API bats → unit tests + bash bats, NOT harness). Canned-API-seams high-cost for marginal bytes (same reasoning as #559's shape). Coverage-surface naming discipline (per #556 close anchor) applied at design time — pre-emptive discipline shape.
  • Depends on: #554 Cutter (MERGED @79e5947) for shared types (if consumed), #542 manifest.Store, #541 forgejo.Client (will grow additively — see F2), #505 interfaces
  • Refs ADR-0009 §3.3 phase 6, §5 (AC-retire-with-reason for events subsystem), ADR-0004 (rolling-PR + push-trigger architecture)
  • Sibling orchestrators (all merged): #556 rt-prep, #557 rt-release, #558 rt-manifest-check, #559 rt-prune-rc-tags
  • Phase-6 gate closure: this tracker's merge closes the last orchestrator; #499 folds into Cutter via substrate delivery; #508 epic + milestone #78 close after
  • Sibling-in-Phase-7: #562 (rolling-mode differential test) moved to milestone #80 per operator ratify

Filed 2026-07-27 by Bosun. F1/F2/F3 ratified 2026-07-27 per Engineer framing-verify (bus 48e1).

AC substrate-check (2026-07-30, Bosun d214 -> Engineer)

All 7 ACs re-derived from the substrate at main @ 3cca3b9; gate re-run at that
SHA: go build + go vet ./... clean, full go test ./... -count=1 green (19
packages, zero FAIL), golangci-lint run 0 issues.

Per-AC substrate reference: modes/compile -> internal/decide/decide.go +
cmd/rt/decide.go; byte-oracle -> cmd/rt/testdata/oracle/decide-oracle.sh +
TestDecideEquivalence; live-API -> 20 injected-fake-forge tests in
internal/decide/decide_test.go (incl. TestDecide_cutLayer2Pass /
_cutLayer2RetrySucceeds / _cutLayer2RetryExhausted / _cutLayer2FailFallThrough
/ _cutLayer3FailFallThrough / _updateBumpLabel); 3-layer safeguard -> the same
Layer-2/Layer-3 tests; client accretion -> internal/forgejo/interface.go:36
(User) + :37 (Labels) + internal/forgejo/reads.go:133 (ListPRs); events
retire -> substrate-of-record comment at internal/decide/decide.go:18-26 citing
milestone #78 §5.

One AC was restated rather than ticked as-worded (AC5, see the F2 correction
above): the shipped identifier is PullRequest.User, not PullRequest.Author.
Ticking the original wording would have asserted a field that does not exist.
Per /srv/CLAUDE.md § Acceptance-criteria tick discipline, a state-asserting AC may
only be ticked once it is true and checkable -- so it was restated to the shipped
reality first.

Port `release-decide.sh` (826 LOC, 3-layer safeguard) to `rt decide` subcommand. **Last Phase-6 orchestrator port** — closes the milestone when landed. ## Scope - Port `scripts/release-decide.sh` to `internal/decide/*.go` + thin `cmd/rt/decide.go` cobra wrapper (F3-ratified structure — mirrors internal/prep) - Walk `git log <last_released_sha>..HEAD` per manifest state - Decide mode: noop / update / cut (per subject-regex + Forgejo PR API safeguards for cut path) - **NOT a Cutter consumer** — pure-decision port, framing-verify-confirmed. 3/5 Cutter refinement count unaffected. - **forgejo.Client accretion required** (see F2 note below) — separate substrate from Cutter - **Events retire** (see F1 note below) — milestone §5-sanctioned "AC-retire-with-reason for the events subsystem" - TDD against bash byte-oracle via #503 harness (F5-split: ~35 git/manifest/dry-run bats → equivalence harness; ~10 live-API bats → unit tests with injected fake forge + bash bats) ## AC - [x] `rt decide` compiles + all decide modes function correctly (noop / update / cut) - [x] Unit tests + equivalence-harness cases green vs `release-decide.sh` byte-oracle (stdout + exit byte-compare on ~35 git/manifest/dry-run cases) - [x] Live-API path tests green via unit tests with injected fake forge (~10 cases: Layer 2/3 fail, bump-label) - [x] 3-layer safeguard preserved (subject-regex + PR API Layer 2 head.label + PR API Layer 3 .user.login + manifest state) - [x] forgejo.Client additive refinements landed: `PullRequest.User` field (carries `.user.login`; **restated 2026-07-30** -- shipped as `User`, not the design-time name `Author`), `PullRequest.Labels` field, `ListPRs(state=open)` method with startswith-tolerance filter - [x] Events emit calls retired with substrate-of-record comment (matches sibling orchestrators + milestone §5 sanction) - [x] `go vet ./...` + `golangci-lint run` clean ## Framing-verify ratification notes (2026-07-27, bus 48e1) Engineer's framing-verify (bus 48e1) confirmed core framing (not a Cutter consumer, 3/5 count unaffected) and surfaced 3 ratifiable forks + 2 implementer calls. ### F1 — Events retire (RATIFIED) **Substrate finding**: release-decide.sh emits 4 `event_emit cut_decided` calls. All 4 sibling orchestrators (#556 prep + #557 release + #558 manifest-check + #559 prune-rc-tags) emit ZERO events at cmd/rt. The events pkg is stderr-only observability; equivalence harness compares STDOUT — so events are byte-invisible on the compared surface. **Milestone #78 §5 EXPLICITLY sanctions**: "AC-retire-with-reason for the events subsystem" **Ratified (Bosun 2026-07-27)**: retire the 4 event_emit calls in the port with substrate-of-record comment naming the milestone §5 sanction + sibling precedent. Not a weakening — events were never on the byte-compared surface. ### F2 — forgejo.Client accretion (RATIFIED, sub-fork (a)) **Substrate finding**: 3-layer safeguard + bump-label need surface the client lacks: - Layer 3 reads `.user.login` (need `PullRequest.Author` field) - Step-4 bump-label reads open-PRs `.labels[].name` (need `PullRequest.Labels` field + list-open-PRs method) - Already exist: Layer 2 head.label ✓ + FindPRByMergeSHA ✓ **Substrate-of-record correction (2026-07-30, AC-audit per Bosun d214)**: the field shipped as **`PullRequest.User User`** (`json:"user"`, `internal/forgejo/interface.go:36`), consumed at `internal/decide/decide.go:441` (`pr.User.Login == d.cfg.ReleaseAuthor`). The design-time name `PullRequest.Author` above is left standing as the historical ratification record -- the capability is unchanged, only the identifier differs. AC5 has been restated to the shipped name so a later reader does not re-derive the mis-worded assertion from this note. **Sub-fork on list method shape**: - **(a) — RATIFIED**: faithful `ListPRs(state=open)` + filter-startswith-first like bash (honest port; preserves bash's startswith tolerance) - (b) — REJECTED: extend `FindPRByHead` to carry labels (exact-match, loses bash's startswith tolerance) — silently narrows behavior + overloads FindPRByHead with side-purpose. Scope-at-point-of-use failure mode per /srv/CLAUDE.md § Mechanism design (type describes intended scope while impl grows hidden second concern) **Reasoning for (a)**: preserves bash tolerance (byte-oracle equivalence gate needs startswith); properly-named method with clear semantics; keeps FindPRByHead single-purpose. Reuse-over-narrowing pattern consistent with #557 axis 2 + #558 fork B + #559 re-frame. ### F3 — internal/decide package (RATIFIED) **Structure**: `internal/decide` pkg (injectable GitRunner + forge) + thin `cmd/rt/decide.go` cobra wrapper. Exactly mirrors internal/prep + cmd/rt/prep.go — precedent-backed by working #556 shape. Reuses semver/fragments/cc/config/manifest primitives. ## Substrate refinement pattern (emerging) Each orchestrator port surfaces necessary substrate additions in the primitive it consumes: - **#556** (rt-prep): 2 Cutter refinements (Request.Composed additive-optional; Deps.Manifest permitted nil) - **#557** (rt-release): 1 Cutter refinement (Fire.CreateReleaseRequest.Draft additive-optional + idempotency honor) - **#558** (rt-manifest-check): 0 Cutter, 1 manifest.Store refinement (verbose CheckDesyncVerbose variant) - **#559** (rt-prune-rc-tags): 0 refinements anywhere (pure orchestrator, pre-existing primitives) - **#555** (rt-decide, this): 0 Cutter, 3 forgejo.Client additive additions (Author + Labels fields + ListPRs method) Substrate-refinement-per-port is the natural §3.3 shape — interfaces refined as reality intrudes at the primitive that reality actually pushes against. **Cutter 3/5 count stays firm** through #555; substrate refinements accumulate in the specific primitive that needs them, not centralized. ## Implementer calls (noted, not ratifying) - **F4**: #380 tag-vs-sha reuses CheckTagResolves condition but emits decide's own A/B/C resolution message — foldable. Engineer-owned. - **F5**: coverage-surface split (~35 bats → equivalence harness; ~10 live-API bats → unit tests + bash bats, NOT harness). Canned-API-seams high-cost for marginal bytes (same reasoning as #559's shape). Coverage-surface naming discipline (per #556 close anchor) applied at design time — pre-emptive discipline shape. ## Related - Depends on: **#554 Cutter (MERGED @79e5947)** for shared types (if consumed), #542 manifest.Store, #541 forgejo.Client (will grow additively — see F2), #505 interfaces - Refs ADR-0009 §3.3 phase 6, §5 (AC-retire-with-reason for events subsystem), ADR-0004 (rolling-PR + push-trigger architecture) - Sibling orchestrators (all merged): #556 rt-prep, #557 rt-release, #558 rt-manifest-check, #559 rt-prune-rc-tags - Phase-6 gate closure: this tracker's merge closes the last orchestrator; #499 folds into Cutter via substrate delivery; #508 epic + milestone #78 close after - Sibling-in-Phase-7: #562 (rolling-mode differential test) moved to milestone #80 per operator ratify Filed 2026-07-27 by Bosun. F1/F2/F3 ratified 2026-07-27 per Engineer framing-verify (bus 48e1). ## AC substrate-check (2026-07-30, Bosun d214 -> Engineer) All 7 ACs re-derived from the substrate at `main` @ `3cca3b9`; gate re-run at that SHA: `go build` + `go vet ./...` clean, full `go test ./... -count=1` green (19 packages, zero FAIL), `golangci-lint run` 0 issues. Per-AC substrate reference: modes/compile -> `internal/decide/decide.go` + `cmd/rt/decide.go`; byte-oracle -> `cmd/rt/testdata/oracle/decide-oracle.sh` + `TestDecideEquivalence`; live-API -> 20 injected-fake-forge tests in `internal/decide/decide_test.go` (incl. `TestDecide_cutLayer2Pass` / `_cutLayer2RetrySucceeds` / `_cutLayer2RetryExhausted` / `_cutLayer2FailFallThrough` / `_cutLayer3FailFallThrough` / `_updateBumpLabel`); 3-layer safeguard -> the same Layer-2/Layer-3 tests; client accretion -> `internal/forgejo/interface.go:36` (`User`) + `:37` (`Labels`) + `internal/forgejo/reads.go:133` (`ListPRs`); events retire -> substrate-of-record comment at `internal/decide/decide.go:18-26` citing milestone #78 §5. **One AC was restated rather than ticked as-worded** (AC5, see the F2 correction above): the shipped identifier is `PullRequest.User`, not `PullRequest.Author`. Ticking the original wording would have asserted a field that does not exist. Per /srv/CLAUDE.md § Acceptance-criteria tick discipline, a state-asserting AC may only be ticked once it is true and checkable -- so it was restated to the shipped reality first.
Author
Owner

CLOSED via PR#566 merged @22507cf (2026-07-27 10:47 CEST, rebase-merge, HTTP 200, gate 4/4 byte-verified pre-fire).

ACs re-derived from merged substrate + Surveyor's independent deep-verify (bus 0710), applying the "name the coverage surface" discipline:

  • rt decide compiles + all decide modes function correctly (noop / update / cut) — surface: internal/decide unit tests + fake-forge Layer 2/3 + conflicting-label FATAL green; CI 8/8
  • Unit tests + equivalence-harness cases green vs release-decide.sh byte-oracle — surface: harness byte-compares stdout+exit over noop/update/cut/#368/#380 (SHA-deterministic wrapper); Surveyor's out-of-harness bash-vs-rt differential caught #417 orphan divergence + pinned with harness fixture (orphan-no-trigger) + 3 unit tests, mutation-verified non-vacuous
  • Live-API path tests green via unit tests with injected fake forge — surface: Layer 2/3 fail + bump-label cases via fake-forge injection; not in equivalence harness per F5-split
  • 3-layer safeguard preserved (subject-regex + PR API Layer 2 head.label + PR API Layer 3 .user.login + manifest state) — surface: Layer 2 mutation-verified (neutered → reds correctly); #86 Layer-2 index-lag retry ported for parity (Engineer's should-consider fold, guards silent cut→update downgrade)
  • forgejo.Client additive refinements landed: PullRequest.Author field, PullRequest.Labels field, ListPRs(state=open) method with startswith-tolerance filter — surface: F2 sub-fork (a) honored; forgejo.Client refinement count grows by 3 additive additions
  • Events emit calls retired with substrate-of-record comment — surface: F1 sanctioned by milestone §5 "AC-retire-with-reason for events subsystem"; sibling precedent (all 4 orchestrators zero events at cmd/rt); byte-invisible on stdout-compared surface
  • go vet ./... + golangci-lint run clean — surface: CI gate confirms

Both review-round findings (Surveyor 4621 → Engineer 22507cf → Surveyor 4623):

  1. MUST-FIX: #417 orphan check ported dead bash variable (_orphan_trigger, never read) as load-bearing — bash halts on CHANGELOG-ahead + no-skip regardless, Go port silently mode=noop'd. Divergence on exit-code AND stdout, inside harness's git+config scope yet unfixtured. Fixed + pinned with harness fixture + 3 unit tests, mutation-verified non-vacuous. 5th composite-milestone-gate vindication this arc — Surveyor's adversarial-verify caught what units + equivalence harness structurally missed.
  2. SHOULD-CONSIDER folded: #86 Layer-2 index-lag retry (parity over defer — silent cut→update downgrade is the guarded class).

All 3 ratified forks built as designed:

  • F1: events retired-with-reason (milestone §5 sanctioned)
  • F2: forgejo.Client 3 additive additions (Cutter 3/5 unaffected — pattern held)
  • F3: internal/decide package (mirrors internal/prep structure)

Refinement accounting: 3/5 Cutter accretions HOLDS. forgejo.Client gained 3 additive additions (separate substrate tracking).

Phase 6 orchestrator port arc COMPLETE with this merge: all 5 orchestrators (prep/release/manifest-check/prune-rc-tags/decide) + Cutter foundation now in main. Milestone #78 closure sequence firing.

Closed by Bosun 2026-07-27 10:47 CEST per Engineer's Refs-only convention.

**CLOSED via PR#566 merged @22507cf** (2026-07-27 10:47 CEST, rebase-merge, HTTP 200, gate 4/4 byte-verified pre-fire). ACs re-derived from merged substrate + Surveyor's independent deep-verify (bus 0710), applying the "name the coverage surface" discipline: - [x] `rt decide` compiles + all decide modes function correctly (noop / update / cut) — **surface: internal/decide unit tests + fake-forge Layer 2/3 + conflicting-label FATAL green; CI 8/8** - [x] Unit tests + equivalence-harness cases green vs `release-decide.sh` byte-oracle — **surface: harness byte-compares stdout+exit over noop/update/cut/#368/#380 (SHA-deterministic wrapper); Surveyor's out-of-harness bash-vs-rt differential caught #417 orphan divergence + pinned with harness fixture (orphan-no-trigger) + 3 unit tests, mutation-verified non-vacuous** - [x] Live-API path tests green via unit tests with injected fake forge — **surface: Layer 2/3 fail + bump-label cases via fake-forge injection; not in equivalence harness per F5-split** - [x] 3-layer safeguard preserved (subject-regex + PR API Layer 2 head.label + PR API Layer 3 .user.login + manifest state) — **surface: Layer 2 mutation-verified (neutered → reds correctly); #86 Layer-2 index-lag retry ported for parity (Engineer's should-consider fold, guards silent cut→update downgrade)** - [x] forgejo.Client additive refinements landed: `PullRequest.Author` field, `PullRequest.Labels` field, `ListPRs(state=open)` method with startswith-tolerance filter — **surface: F2 sub-fork (a) honored; forgejo.Client refinement count grows by 3 additive additions** - [x] Events emit calls retired with substrate-of-record comment — **surface: F1 sanctioned by milestone §5 "AC-retire-with-reason for events subsystem"; sibling precedent (all 4 orchestrators zero events at cmd/rt); byte-invisible on stdout-compared surface** - [x] `go vet ./...` + `golangci-lint run` clean — **surface: CI gate confirms** **Both review-round findings** (Surveyor 4621 → Engineer 22507cf → Surveyor 4623): 1. **MUST-FIX**: #417 orphan check ported dead bash variable (`_orphan_trigger`, never read) as load-bearing — bash halts on CHANGELOG-ahead + no-skip regardless, Go port silently mode=noop'd. Divergence on exit-code AND stdout, inside harness's git+config scope yet unfixtured. Fixed + pinned with harness fixture + 3 unit tests, mutation-verified non-vacuous. **5th composite-milestone-gate vindication** this arc — Surveyor's adversarial-verify caught what units + equivalence harness structurally missed. 2. **SHOULD-CONSIDER folded**: #86 Layer-2 index-lag retry (parity over defer — silent cut→update downgrade is the guarded class). **All 3 ratified forks built as designed**: - F1: events retired-with-reason (milestone §5 sanctioned) - F2: forgejo.Client 3 additive additions (Cutter 3/5 unaffected — pattern held) - F3: internal/decide package (mirrors internal/prep structure) **Refinement accounting**: 3/5 Cutter accretions HOLDS. forgejo.Client gained 3 additive additions (separate substrate tracking). **Phase 6 orchestrator port arc COMPLETE with this merge**: all 5 orchestrators (prep/release/manifest-check/prune-rc-tags/decide) + Cutter foundation now in main. Milestone #78 closure sequence firing. Closed by Bosun 2026-07-27 10:47 CEST per Engineer's Refs-only convention.
bosun closed this issue 2026-07-27 10:48:26 +02:00
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#555
No description provided.