chore(migration): merge v2/next → main at Phase 5 close (ADR-0009 substrate lands on main; operator-ratified 2026-07-26) #550

Merged
bosun merged 51 commits from v2/next into main 2026-07-26 22:50:34 +02:00
Owner

v2/next → main merge (Phase 5 close, operator-ratified 2026-07-26)

Coordination tracker: #544

Substrate operation: land the ADR-0009 Go substrate onto main. Preserves existing bash substrate (scripts/release-prep.sh + scripts/release-decide.sh + scripts/draft-release.sh remain at their paths + rolling-PR machinery on main continues to work). Additive merge — Go substrate lands ALONGSIDE bash.

What lands

Phase 0a → Phase 5 substrate:

  • Phase 0a (#71): Go module root scaffold (cmd/rt cobra skeleton, 10 internal/* stubs) + CI (go-ci + goreleaser + composite action) + equivalence-harness framework
  • Phase 0b (#72): C1-C7 contracts + property invariants + Go interfaces for all internal/* packages + rt subcommand skeleton + exit-code contract
  • Phase 1 (#73): internal/semver + internal/conventionalcommits implementations (byte-oracle vs bash)
  • Phase 2 (#74): internal/config + internal/events implementations (byte-oracle vs bash where central; contract-authority where no oracle exists)
  • Phase 3 (#75): internal/fragments + internal/changelog (register-scrub obligation fulfilled) + internal/gates (compose-verify + register-check + changelog-body-check)
  • Phase 4 (#76): internal/manifest.Store + internal/forgejo.Client (15-method surface + hybrid harness)
  • Phase 5 (#77): internal/bake.Baker (byte-exactness) + internal/selfboot.CommitBake (self-bootstrap)

13 packages on internal/ (verdict + semver + conventionalcommits + config + events + fragments + changelog + register + gates + manifest + forgejo + bake + selfboot) + cmd/rt + full contract set.

8 milestones closed (Phase 0a + 0b + 1 + 2 + 3 + 4 + 5-substrate; Phase 5 completes with THIS merge landing #544).

20 PRs merged across the arc on v2/next.

v1.0.0-alpha.0 published at git.frankenbit.de/frankenbit/release-toolkit/releases/tag/v1.0.0-alpha.0.

Cross-references

  • Operator ratification: 2026-07-26 dialogue (Phase 5 close chosen as earlier merge timing vs ADR-0009 §3.3 v1.0.0 DoD)
  • Shell substrate disposition: preserved on main; rolling-PR machinery on main continues to work; Go substrate additive; shell scripts will be removed post-Phase-6 per operator disposition
  • Substrate-of-record patterns codified: 6× substrate-check-design-claim-before-pr; contract-authority-when-no-oracle-exists (3×); apparatus-not-world diagnosis class; API-client-with-limited-oracle harness class; harness-vehicle-of-record; 5× tracker-vs-ratified-contract restatement pattern
  • Closes: coordination via #544
  • Refs: ADR-0009 §3.3 phase 5 row, §7 (self-bootstrap Framework-B unlock)

Bosun-authored on operator ratification.

## v2/next → main merge (Phase 5 close, operator-ratified 2026-07-26) **Coordination tracker**: #544 **Substrate operation**: land the ADR-0009 Go substrate onto main. Preserves existing bash substrate (scripts/release-prep.sh + scripts/release-decide.sh + scripts/draft-release.sh remain at their paths + rolling-PR machinery on main continues to work). Additive merge — Go substrate lands ALONGSIDE bash. ## What lands Phase 0a → Phase 5 substrate: - **Phase 0a** (#71): Go module root scaffold (cmd/rt cobra skeleton, 10 internal/* stubs) + CI (go-ci + goreleaser + composite action) + equivalence-harness framework - **Phase 0b** (#72): C1-C7 contracts + property invariants + Go interfaces for all internal/* packages + rt subcommand skeleton + exit-code contract - **Phase 1** (#73): internal/semver + internal/conventionalcommits implementations (byte-oracle vs bash) - **Phase 2** (#74): internal/config + internal/events implementations (byte-oracle vs bash where central; contract-authority where no oracle exists) - **Phase 3** (#75): internal/fragments + internal/changelog (register-scrub obligation fulfilled) + internal/gates (compose-verify + register-check + changelog-body-check) - **Phase 4** (#76): internal/manifest.Store + internal/forgejo.Client (15-method surface + hybrid harness) - **Phase 5** (#77): internal/bake.Baker (byte-exactness) + internal/selfboot.CommitBake (self-bootstrap) **13 packages on internal/** (verdict + semver + conventionalcommits + config + events + fragments + changelog + register + gates + manifest + forgejo + bake + selfboot) + cmd/rt + full contract set. **8 milestones closed** (Phase 0a + 0b + 1 + 2 + 3 + 4 + 5-substrate; Phase 5 completes with THIS merge landing #544). **20 PRs merged** across the arc on v2/next. **v1.0.0-alpha.0 published** at git.frankenbit.de/frankenbit/release-toolkit/releases/tag/v1.0.0-alpha.0. ## Cross-references - Operator ratification: 2026-07-26 dialogue (Phase 5 close chosen as earlier merge timing vs ADR-0009 §3.3 v1.0.0 DoD) - Shell substrate disposition: preserved on main; rolling-PR machinery on main continues to work; Go substrate additive; shell scripts will be removed post-Phase-6 per operator disposition - Substrate-of-record patterns codified: 6× substrate-check-design-claim-before-pr; contract-authority-when-no-oracle-exists (3×); apparatus-not-world diagnosis class; API-client-with-limited-oracle harness class; harness-vehicle-of-record; 5× tracker-vs-ratified-contract restatement pattern - Closes: coordination via #544 - Refs: ADR-0009 §3.3 phase 5 row, §7 (self-bootstrap Framework-B unlock) Bosun-authored on operator ratification.
Analysis-only ADR (no Go code): decision surface for operator ratification.
Grounds the tracker's Go lean in the measured substrate (~5341 LOC / 8 libs /
533 bats), establishes the workflow-YAML-as-API-boundary constraint (port is
adopter-transparent but not implementation-cheap — two independent axes),
enumerates the ratification forks (binary distribution, orchestrator
granularity, IPC collapse, #172 invariant, migration cadence), a phased
~12-20 EW effort estimate, and a risk register centered on the byte-exact
self-bootstrap/bake machinery (verified against build_bake.sh:84-164).

Supersedes ADR-0001 conditionally (only on v2.0.0 ratification; shell remains
substrate-of-record until then). Does not gate v1.0.0.

Refs #367
Bosun review catch on #473: the per-phase high bars sum to 21, but the
aggregate cited ~12–20. Match the aggregate to the table (table is ground
truth) + name the sum inline, so the record artifact carries no arithmetic
drift. Cosmetic; order-of-magnitude framing unchanged.

Refs #367
First of the architecture-doc arc (document-first strategy). Documents the
current single-thread-by-construction cut model (grounded in release.yml
push:main trigger, single manifest, stable-only tag discovery), the ratified
target two-line model (main=v1.x shell / v2/next=v2.x Go, shared tag namespace
+ SemVer §11 precedence), and the operational discipline for concurrent cuts.

Load-bearing finding: the prerelease WRITE-SIDE is unimplemented — #347 gave
compare/ordering, but no cut path emits X.Y.Z-alpha.N (semver_bump strips
prerelease, config_render_tag is {version}-only, bump_override rejects it,
LAST_TAG excludes *-*; rc tags are repin-only non-releases). Specs the
four-site capability + flags it as a v1.0.0 pre-cut candidate that gates
v2/next's first alpha regardless of the port. Also surfaces trigger-branch
parameterization as a second substrate item.

Refs #367
- §3.1 item 2: reclassify config_render_tag from verify-during-impl to
  confirmed-no-change-at-render. Surveyor verified config_render_tag
  "2.0.0-alpha.1" -> v2.0.0-alpha.1 (verbatim {version} substitution,
  suffix-transparent today); the residual belongs to items 1+3 feeding it a
  suffix-bearing version, not to the render primitive.
- §4 nit: paths-ignore ref release.yml:31 -> :35 (:31 was a comment line;
  the directive is at :35, verified).

Refs #367
Doc 2 of the architecture-doc arc. Turns ADR-0008's workflow-YAML-as-API-boundary
finding from prose into testable contracts:

- config.schema.json — JSON Schema for release-toolkit.yml (grounded in config.sh
  v0.1 schema: release_type enum required, version_files basename-constrained,
  tag_format/{version}-only, publish_mode/section_format/post_bump_hooks, defaults)
- manifest.schema.json — JSON Schema for .release-toolkit-manifest.json (schema:1
  + last_released_{sha,version,tag,at}, grounded in reusable-release.yml:425-433)
- workflow-api.md — typed spec of the reusable-workflow uses:@<ref> input/output
  surface + the secrets:inherit / RELEASE_TOOLKIT_TOKEN platform-quirk contract
- README.md — framing: contracts (not docs) enable cross-substrate equivalence,
  additive-only evolution, schema-first for new features

Both schemas validated well-formed. Contracts bind SHAPE; behavior (resolution
layers, decision logic) is Arc42 §6 Runtime View, explicitly non-scope here.

Refs #367
Surveyor should-consider on #475: the JSON Schema `default` keyword sat on
inconsistent layers — publish_mode/version stamped the terminal default
(draft/1) while default_branch stamped the config-layer "" — though all three
getters return "" at the config layer.

Adopt one convention: `default` = config-layer value; terminal defaults named
in the description. Swept the WHOLE resolved-downstream category (disclose-and-
extend), not just the named publish_mode site: publish_mode + version both drop
the terminal-value `default` + describe the downstream resolution; default_branch
was already config-layer-correct. Protects a harness that reads defaults off the
schema from mistaking a terminal default for the config-file default.

Refs #367
The first-pass baseline (5126fe9 + 1675808) was measured on a stale i/204
checkout (cc3328b), undercounting the real port surface — most
consequentially the cut-time validator subsystem (~1,120 LOC) that was
absent from the phase table entirely.

Re-measured against the v2/next authoritative tree:
- ~8,159 LOC total across scripts/**/*.sh; ~7,851 in port scope
  (lib 3,649 + top-level orchestrators/validators 4,202); ~308 ancillary
  dev-tools/hooks excluded from the cmd/rt surface
- 11 libs (was 8); 769 tests across 30 files (was 533/20)
- phase table: true per-module LOC + a new phase 6b (cut-time validators)
- effort ~12–21 → ~16–29 EW, re-grounded per-phase (NOT a LOC-proportional
  multiply): clean-seam growth credited sub-linearly, risk-driven bake
  phase held near-constant, omitted validator subsystem added as new work

Structural conclusions unaffected — the workflow-YAML-as-API boundary,
the bake byte-exact/detached-commit design-invariant, the 4a–4e fork
leans, and the defer recommendation all hold; none depended on absolute
LOC. Added a Correction (2026-07-07) section disclosing root cause +
methodology + conclusions-unaffected (disclose, don't silently overwrite).
Review 3728 caught an asymmetry: the correction named the 8,159→7,851
step (308 ancillary) but left a parallel 7,851→7,341 step silent — the
phase-table LOC summed to 7,341, leaving 510 LOC in 5 unphased top-level
scripts, two of them cut-time validators of the same family phase 6b adds.

Classified the 5 by the authoritative signal (does a reusable workflow
invoke it per cycle?), not by name:
- fragment-check (57), manifest-precheck (117), preflight-push-whitelist
  (143) → port scope, phase 6b (all workflow-invoked gates)
- prune-rc-tags (107) → port scope, P6 (post-cut cleanup, reusable-release.yml:843)
- setup-bump-labels (86) → no workflow callsite → ancillary, excluded

4 of 5 are port scope (extends the review's 2-fold/3-exclude suggestion
with callsite evidence: preflight-push-whitelist + prune-rc-tags are
per-cycle machinery, not operator utilities). Result: port scope
7,851→7,765 (−86 excluded), ancillary 308→394, and the phase table now
sums to 7,765 end-to-end. EW aggregate unchanged (~16–29) — the folded
424 LOC are small mechanical validators/cleanup within existing bars.

Added a denominator-reconciliation table to the Correction section.
Companion to ADR-0008; operationalizes §6 R1/R3/R4. Defines the test
strategy that makes the adopter-transparency claim (§1) enforceable:

- The equivalence contract: bash ≡ Go byte-identical on 5 observable
  surfaces (GITHUB_OUTPUT, stdout key=value, committed artifacts, Forgejo
  API payloads, exit codes), plus the ASCII-only bats-assertion-lockstep
  byte-constraint (AGENTS.md:304-311) as a hard input to the Go log() shim.
- The 769-test oracle put to work: as-is bash regression floor + a
  $SCRIPT→rt resolution shim that reuses assertion bodies verbatim (the
  cheapest first differential check).
- Bake byte-exact sub-strategy (R1, highest-risk): blob-hash differential,
  the 483d mutation pair (build_bake.bats:119-135 + csb.bats:255-281 +
  workflows.bats:168-184) must survive the port; Phase-5-first crucible.
- Mutation-verification methodology extended: every ported invariant
  carries its experiment; the harness itself must be mutation-verified
  (a harness that passes on a diverged Go impl is a placebo).
- Test seams (R4) as the harness control surface; keep the env-var
  contract through side-by-side, deprecate to interface-injection post-2.0.
- Fixture strategy from the no-fixtures-dir reality (2 inline idioms).
- CI: nothing runs on v2/next → local bats until port branches target
  main; names the toolkit-CI-needs-both-toolchains non-contradiction.

All file:line anchors verified against e048bb0; corrected 3 inherited
ADR-0008 R1 anchors that were stale vs the current tree.
Doc 4/6 of the v2/next document-first arc. Documents the current (v1.x
shell) release-toolkit architecture as substrate-of-record; ADR-0008's
Go port is the forward-looking decision this informs.

This tranche (loaded-context, no QM dependency):
- README index + reading guide + verification posture
- §1 Introduction & Goals (what it is, requirements, top-5 quality
  goals, stakeholders)
- §2 Architecture Constraints (Forgejo Actions platform quirks as
  contract-not-impl, bash/ASCII-lockstep/single-source-of-truth, process)
- §3 Context & Scope (business + technical context, scope boundary)
- §4 Solution Strategy (fundamental decisions → ADRs, 3-layer
  decomposition, quality-goal→approach mapping)

§5-7 (building-block/runtime/deployment) drafted next + routed to QM
(substrate-oracle). §8-12 follow.
Substrate-heavy trio, drafted from the #367 re-measure + QM substrate-oracle
pointers + a verify-at-source reading pass over integration.md / reusable-release.yml
/ AGENTS.md §2. All file:line anchors verified against the v2/next tip (e048bb0).

§5 Building Block View: the reusable/internal API boundary (ADR-0008 §1), the
orchestrator/library/validator decomposition with verified LOC, and level-3 zooms
(release-decide 3-layer safeguard, forgejo-api client, build_bake). α/γ deferred to §6.

§6 Runtime View: single-job internal dispatch (not a workflow_call chain), the
3-layer cut safeguard, the cut sequence with fail-loud ordering, idempotent re-run
(#128/#134), the α/γ manifest split (ADR-0007), mid-runtime bake-reset guards
(#268), and cut-cancellation recovery (#417).

§7 Deployment View: tag-as-deployment-unit, self-bootstrap lag, α/γ manifest
propagation, event-decoupled Codeberg mirror (#427).

Two corrections to the oracle pointers surfaced during the verify pass and are
folded + flagged for QM: the runtime is a single job, not a workflow_call chain;
the idempotent pre-check is stamped #128/#134, not #452.
Completes the 12-section Arc42 set for the v1.x shell architecture (#367
document-first arc). Loaded-context sections synthesizing the substrate already
read for §5-7 + the ADR index + AGENTS.md maintainer disciplines.

§8 Cross-cutting Concepts: self-consistency (build-bake/483d guard), idempotency
+ fail-loud, dry-run seams, testability seams, ASCII-lockstep harness fidelity,
layered config resolution, structured logging, security/token model (α/γ).

§9 Architecture Decisions: ADR-0001..0008 index with status + relationships;
ADR-0008 the one non-accepted (decision surface).

§10 Quality Requirements: quality tree + 10 stimulus-response scenarios mapped
to the enforcing mechanism; priority trade-offs.

§11 Risks & Technical Debt: self-bootstrap risks, rc-tag orphan classes,
platform/portability risks, feature gaps (#476/#477 prerelease write-side NOT
shipped), and the standing v2-port open decision.

§12 Glossary: adopter/reusable/orchestrator/cut/rolling/manifest/fragment/α-γ/
build-bake/re-pin/bootstrap-lag/483d and the API-boundary finding.

All anchors verified against the v2/next tip.
Two non-gating precision catches from QM's substrate-oracle verify-after (§5-7
verified against e048bb0):

- §5.2: validator aggregate header ~1,437 → ~1,261 to match the 6-row table sum
  (400+369+175+143+117+57); the 3 lib entries the 1,437 folded in are already
  counted in the library aggregate (3,649).
- §6.2: the cut-safeguard diagram's 'any layer fails → SAFEGUARD_FAIL' gloss was
  over-broad — split the two mode=update paths: layer-2/3 failure stamps
  SAFEGUARD_FAIL and degrades LOUDLY (the suspicious case, :653-662); a layer-1
  miss (not a prepare commit at all) falls through QUIETLY with no stamp (:662).

QM confirmed all §5 LOC, ~35 §6 anchors, all §7 anchors, α/γ keying on
RELEASE_TOKEN_OVERRIDE, ADR-0007 ordering, and the single-job framing as
substrate-accurate.
Two non-gating pre-merge finalization items from Surveyor's #481 approval:

1. Claim-honesty (substrate-state-vs-doc-text): QM's §5-7 verify-after completed
   (2026-07-08), so the 'QM substrate-verify pending' banners in §5/§6/§7 + the
   README status column + the README prose clause were stale against the merged
   doc-of-record. Flipped all to 'QM substrate-verified' with the confirmation
   scope (every §5 LOC, ~35 §6 anchors, §7 layout+mirrors). §6.7's 'flagged for
   QM' clause → 'confirmed by QM' (the two oracle-pointer corrections are now
   confirmed).

2. Nit: §9.1 'the only non-accepted ADR' → 'the only open/unratified ADR' — 0006
   is 'decided' (also not 'accepted'), so 'non-accepted' was imprecise vs the §9
   table; 'open/unratified' is exact.

No content/anchor changes — status-honesty + one word.
Part 1 — ADR-0008 systemic anchor refresh. The scoped '3 stale anchors' (§6 R1
/§4c) turned out to be ~8 stale anchor-sets once verified at source; a full sweep
(sub-agent-built byte-precise correction table, all 17 anchors checked) found:
- STALE (8, fixed): §1 callsite series :225,282,288,306,619,632 → :266,349,366,
  389,407,843,856 (and count ~6→7); §4 inline-jq :312-335 → :413-436; §4 tee
  GITHUB_OUTPUT :225 → :266; §5 awk-normalizer draft-release.sh:126-156 → :131-161;
  §6 R1 three-actor bake release-prep.sh:541-546 → :564-569 + reusable-release.yml
  :390-395 → :490-495; §6 R2 repin.sh:76 → :86; §6 R5 secrets block :77-89 → :72-83.
- CURRENT (9, untouched): release-prep.sh:296-332, build_bake.sh:84-164/:90-94/:69/
  :144-150/:105, reusable-release.yml:407/:366/:843.
File growth (not content change) drove the drift; disclose-and-extend → swept the
whole category rather than patching only the 3 originally named.

Part 2 — coupled test-strategy.md refinements (Surveyor 3731, now unblocked by the
ADR refresh, spec-first-then-consumer):
- §6b: dropped the rot-magnet note (hard-coded the now-fixed stale line#s) → plain
  cite, since the ADR anchors are refreshed + consistent.
- §1 ASCII constraint: reframed as harness-fidelity (§3b-derived), explicitly NOT a
  §1 adopter-surface element (adopters never observe log() text) + added the #149
  YAML-lint-security second reason (two independent gates).
- §3b: added the run --separate-stderr stream-fidelity caveat (register-check.bats
  :175) — stream routing is part of the byte-equivalence surface.

All anchors verified byte-precise against the v2/next tip (51cef71).
Adds ADR-0009 executing ADR-0008's 'go' fork under Framework B
(operator ratification 2026-07-24):

  - Ratifies Go substrate as v1.0.0 (not v2.0.0); collapses
    branch-strategy.md's two-line model into single line
  - Adds contract-driven, TDD-per-phase workflow (§3.1)
  - Splits Phase 0 into 0a (scaffold + harness) and 0b
    (contracts + interfaces)
  - Contract inventory C1–C7 with current-state markers (§3.2)
  - Substrate discipline continuity catalog (§5) — bash patterns
    banked in the 2026-07-13 → 2026-07-24 arc mapped to
    idiomatic Go equivalents
  - #499 fail-atomic transactional cut as first-class internal/release
    design, not translation follow-up (§6). Sentinel-error fail-loud
    surface with structured error classes.
  - Tracker disposition table under Framework B (§8)
  - 5 explicit open sub-forks named for future ratification (§9)

Depends on ADR-0008 (analysis + phase table + Phase-5 spike outcome).
Supersedes ADR-0001 at v1.0.0 cut. Supersedes docs/architecture/branch-strategy.md
at v1.0.0 cut.

Refs #367 #476 #477 #498 #499 #336 #337 #338 #480.
Folds all 7 findings from PR#500 review (Surveyor id 2418):

  S1 (phase-number collision §3.3 vs §3.5) — §3.5 now explicitly names
     ADR-0008's Phase 8 rather than colliding with this ADR's §3.3 Phase 8
     (prerelease-cutting). Adds inline note preventing future readers from
     tripping the same ambiguity.

  S4 (rollback across irreversible boundary) — §6 rewritten to distinguish
     the reversible prefix (a) CHANGELOG seal + (b) manifest update from
     the irreversible suffix (c) tag creation + (d) release publish. Prefix
     rolls back; suffix uses idempotent replay. Matches #499 remedy-3
     (fail-atomic step ordering).

  S2 (§5 missing locale/subprocess-output pattern) — added row for LC_ALL=C
     + parse-structural-not-rendered discipline. Live in the Go substrate
     because ADR-0008 R2 lands on git-subprocess as the floor.

  S3 (§9 criterion inconsistency) — states inclusion criterion explicitly
     (strategic decisions only the operator can make). Drops former #5
     (composite-action shape → prototyping in Phase 0a, not operator
     ratification). Names what's NOT included and why (implementation shape
     resolved in phase work; author-register calls resolved in §8
     dispositions). Renumbered to 3 items from 5.

  N1 (§1 citation drift) — 'shell tools any admin can patch' quote is in
     #367's language-choice section, not ADR-0008 §2. Fixed citation.

  N2 (§5 rows 3+8 near-duplicate labels) — row 3 relabeled 'Precondition
     check inside the irreversible-action method'; row 8 relabeled
     'Two-phase-commit ordering'. Distinguishes method-scope vs
     section-scope.

  N3 (§5 trivalent row silent on exit-code boundary) — added note that
     Verdict.ExitCode() preserves the three-valued contract at the rt
     subcommand surface; workflow branches on 0/1/2 exit codes.

N-merge (status flip proposed→accepted) — deferred to the ratifying merge
commit, per the ADR-status-flip discipline this ADR itself invokes.

No content dispositioned as push-back — all 7 were legitimate.

Refs #500.
Per the ADR-status-flip discipline this ADR invokes for ADR-0001:
proposed→accepted flip lands in the ratifying merge commit, not
after. Ratification anchored:

- Operator ratification 2026-07-24 (this session)
- Surveyor review 4525 APPROVED at head 04a69a3, official=true, stale=false
- Fold review 4524 findings committed at 04a69a3 (7 folds, all faithful
  to Surveyor's framing — S4 + S1 sharper than the review asked for)

Refs #500 #367.
Phase 0a scaffold (ADR-0009 §3.3): establish the Go module root and a
compile-green skeleton for the v1.0.0 port. No functional code yet — each
internal package is a doc-comment naming its scope + phase + boundary
contract with a TODO(port) marker; each cmd/rt subcommand fails loud with a
TODO(port) error until its phase lands.

- go.mod: module git.frankenbit.de/frankenbit/release-toolkit, go 1.24
  (minimal directive verified green under the 1.24 language version; no
  1.26-specific features used, so CI in #502 can pick any go >= 1.24)
- cmd/rt/main.go: cobra root + subcommands decide, prep, release,
  manifest-check, prune-rc-tags (all five are Phase 6 orchestrators per
  ADR-0009 §3.3 row 6)
- internal/{semver,conventionalcommits,config,changelog,fragments,forgejo,
  bake,events,manifest,release}: package skeletons, doc-comment only

Verified: go build ./... green; go vet clean; rt --help lists the
subcommand surface; gofmt clean. Interfaces land Phase 0b (#505).

Refs ADR-0009 §3.1, §3.2 (C5), §3.3 (phase 0a, row 6).
chore(scaffold): CI (go-ci) + goreleaser + composite-action bootstrap (#502)
Some checks failed
go-ci / lint + build + test (pull_request) Successful in 7s
go-ci / lint + build + test (push) Successful in 6s
goreleaser / build + publish rt asset (push) Failing after 1m11s
3af1442650
Wire the CI + release-asset pipeline for the Go substrate (ADR-0009 Phase
0a). Turns Engineer's #510 local "build green" claim into a CI-enforced
stamp authored by an independent actor (§5 discipline).

- .forgejo/workflows/go-ci.yml — go build + go test + golangci-lint on
  PR + push to [main, v2/next]. Fires on v2/next so the port arc is gated
  as it lands, not only at the eventual v2/next -> main merge. Mirrors the
  sibling tmux-tell Go CI (same `go` runner, ships Go + golangci-lint).
- .golangci config: none added — golangci-lint runs its default set and is
  0-issue green; a pinned .golangci.yml can land when a phase needs it.
- .goreleaser.yaml + .forgejo/workflows/goreleaser.yml — build rt-linux-amd64
  (CGO_ENABLED=0, version stamped via -ldflags) + sha256 checksums.txt,
  published to Forgejo Releases on tag push. linux/amd64 only for v1.0.0;
  other platforms are ADR-0009 §9 sub-fork #1.
- action.yml (repo root) — composite bootstrap per ADR-0008 §4a lean-D:
  resolve os/arch, fetch the released asset + checksums.txt, sha256-verify,
  add rt to PATH. Minimal per ADR-0009 §9 (prototyped in Phase 0a).
- .forgejo/workflows/composite-smoke.yml — scratch consumer that fetches rt
  via the composite and runs `rt --version` (AC3 live smoke;
  workflow_dispatch since it needs a published release).
- cmd/rt/main.go — add `version` var + cobra Version so `rt --version`
  works and goreleaser can stamp the tag.

Gates proven to redden on planted defects (§5 positive control): go
build/test/lint each fail on their own defect class; the composite
checksum-verify rejects a tampered + an unlisted asset. Full local gate
green (build, vet, test, golangci-lint, gofmt, goreleaser check); goreleaser
snapshot produces rt-linux-amd64 + checksums.txt with the version stamped.

Refs #502 · ADR-0009 §3.3 (phase 0a) / §5 / §9 · ADR-0008 §4a · milestone #71

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
feat(harness): byte-equivalence differential harness framework (#503)
Some checks failed
go-ci / lint + build + test (push) Failing after 5s
16a21c595d
Phase 0a of the ADR-0009 Go port: the load-bearing instrument every
subsequent phase's gate reuses (ADR-0009 sec.3.1/sec.3.3-phase0a). Given a
bash Invocation and an equivalent Go Invocation over a shared fixture,
harness.Evaluate runs each side in its own throwaway scratch copy, captures
the five observable surfaces (test-strategy.md sec.1) from both, and grades
the pair with a trivalent verdict.

Surfaces: stdout, exit_code, github_output ($GITHUB_OUTPUT sink),
git_artifacts (working-tree delta by content hash), forgejo_payloads (dry-run
API sink). A Case declares which to Compare (default: all five).

ADR-0009 sec.5 disciplines the instrument carries:
- Trivalent Verdict (green/red/cannot-grade) with ExitCode() -> 0/1/2; no
  boolean gate results. Harness-local for 0a; canonical type lands in 0b (#505).
- Positive control: RequireNonEmpty surfaces empty on both sides yield
  cannot-grade, never a vacuous green (the empty-population trap cannot pass).
- LC_ALL=C forced on every subprocess non-overridably (locale-safe subprocess;
  the precondition lives in the runner, not caller memory).

The instrument is mutation-verified (test-strategy.md sec.7.2): three narrow
mutations each redden exactly their guard and revert byte-clean --
M1 (disable differ) -> the two mismatch tests; M2 (disable positive control)
-> RejectsVacuousPositiveArm; M3 (drop LC_ALL=C) -> ForcesLCAllC. Full loop in
the PR body.

Design calls (decision-tree in PR body): internal/harness (not top-level --
private migration instrument, internal->public is the reversible lift);
harness-local Verdict (0b not landed, framework stands alone).

Scope: this is the framework. Wiring the 769-test bats oracle (sec.3b) and
each impl's real dry-run seams lands per phase (forgejo Phase 4, orchestrators
Phase 6). test-strategy.md sec.2a records the implementation status.

Local verification (no CI fires on v2/next -- every gate is branches:[main],
Go CI is #502, not landed): go build/vet/gofmt clean, go test ./... green,
-race clean on internal/harness.

Review round 1 (Surveyor 4532):
- S1: Evaluate fails loud when a RequireNonEmpty surface is not in Compare --
  it was silently un-checked, a vacuous green in the one mechanism the harness
  exists to guarantee. validateCase errors, naming the offending surface.
- S2: the capture sinks (GITHUB_OUTPUT, RT_HARNESS_FORGEJO_SINK) are forced
  last like LC_ALL, so a Case cannot redirect a surface into silence.
Both guards mutation-verified (each sole-reddens its new test, reverts clean);
test-strategy.md sec.2a aligned to the delivered guarantee.

Refs #503
fix(goreleaser): unset ambient GITHUB_TOKEN so goreleaser sees one token (#502)
Some checks failed
go-ci / lint + build + test (push) Failing after 5s
goreleaser / build + publish rt asset (push) Successful in 1m20s
271cc0150c
The v1.0.0-alpha.0 goreleaser run failed at 0s with "multiple tokens
found, but only one is allowed: GITHUB_TOKEN, GITEA_TOKEN" (run 18823).

Root cause: the Forgejo runner auto-injects GITHUB_TOKEN into every
step's environment. The goreleaser step also sets GITEA_TOKEN (correct
for the Gitea/Forgejo release client), so goreleaser v2 sees two tokens
and refuses to guess — a pre-flight config error, before build. It is
NOT the token-scope-insufficient signal (goreleaser never reached the
release-create call).

Fix: `unset GITHUB_TOKEN` at the top of the goreleaser run step, keeping
GITEA_TOKEN. On Forgejo/Gitea, GITEA_TOKEN is the one goreleaser wants.

Closed-loop verified locally: both tokens set reproduces the exact 0s
"multiple tokens" error; `unset GITHUB_TOKEN` first yields "using token
from $GITEA_TOKEN" -> build -> release succeeded (0 occurrences of
"multiple tokens"). goreleaser check + YAML parse still green.

Post-merge: re-fire via workflow_dispatch goreleaser.yml on v2/next with
tag=v1.0.0-alpha.0 (the tag already exists at 3af1442; no re-tag). That
run reaches release-create and becomes the AC4 token-scope proof.

Refs #502 · #511 · goreleaser run 18823

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
fix(harness): discard os.RemoveAll errors in cleanup defers (errcheck) (#516)
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 7s
go-ci / lint + build + test (push) Successful in 7s
d9f611c68e
Two best-effort scratch-dir teardowns in the equivalence harness left
os.RemoveAll's return unchecked, which golangci-lint's errcheck (a default
linter, stricter than go vet) flags:

  capture.go:38   defer os.RemoveAll(sinkDir)
  capture.go:117  cleanup = func() { os.RemoveAll(dir) }

Discard the error explicitly: both are teardown of harness-owned temp dirs
with no recovery action, and a failed cleanup must not mask the run's real
result.

Escaped review because go-ci never ran on #512 (its base d4f8f88 predated
go-ci.yml, which landed in #511), so the harness merged un-linted; the latent
errcheck failure then reddened the v2/next base gate for every downstream PR.

Verified: golangci-lint run --timeout=5m -> 0 issues; go build ./... +
go test -count=1 ./... + gofmt + -race all green.

Refs #516, #503, #512
fix(action): authenticated asset fetch + fail-loud HTML guard (#502)
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 7s
go-ci / lint + build + test (push) Successful in 7s
2de5372d07
The composite-smoke run (18829) failed at the composite's fetch: on
git.frankenbit.de (REQUIRE_SIGNIN_VIEW=true) an ANONYMOUS asset fetch
returns HTTP 200 with a ~12.7KB HTML sign-in page, not the binary —
and `curl -f` accepts it because 200 is "success". The 200 masks a
wrong-body substitution (the reflex-table "clean 200 that means wrong
body" class).

Two-part fix (fix the cause + harden the detector):
- action.yml gains a `token` input, passed as an Authorization header
  (never in the URL/logs). composite-smoke.yml passes the runner's
  secrets.GITHUB_TOKEN. Optional + defaulted empty so a fully-public
  instance still works anonymously; required on REQUIRE_SIGNIN_VIEW.
- A fail-loud content guard rejects an HTML body for either fetched
  file with an actionable message ("need auth / token under-scoped"),
  instead of the confusing downstream "not listed in checksums.txt"
  (the manifest fetch is ALSO HTML). Closes the class for any cause.

Closed-loop verified against the LIVE v1.0.0-alpha.0 release:
- no token  → guard fires loud → exit 1 (reproduces run 18829's cause);
- with token→ checksum OK → `rt 1.0.0-alpha.0` prints → exit 0
  (AC3 live path proven end to end against the real published asset).
YAML valid; shellcheck clean.

AC4 already proven: goreleaser run 18828 published rt-linux-amd64 (3.9MB)
+ checksums.txt, so GITEA_TOKEN's release-create scope is sufficient.

Post-merge: re-fire composite-smoke.yml (workflow_dispatch, version=
v1.0.0-alpha.0) for the AC3-live CI stamp; no re-tag.

Refs #502 · #511 · #514 · composite-smoke run 18829

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
feat(contracts): write C1–C7 contract shapes + property invariants (#504)
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 7s
go-ci / lint + build + test (push) Successful in 7s
c1b5cd031e
Phase 0b of the ADR-0009 Go port: land the seven first-class contracts under
docs/architecture/contracts/ so every downstream phase targets a fixed shape
(ADR-0009 §3.1 contract-driven, §3.2 inventory). Shapes are written here;
each is refined by the phase that implements it (C4/C6 → Phase 3, C5 → Phase 6,
C7 → Phase 4).

New (C4–C7 + properties):
- C4 fragment-format.md — filename grammar (<id>.<kind>.md), the seven kinds →
  bump → section, BREAKING marker, body constraints (brevity/single-line/
  register-scrub/density), transactional lifecycle. Byte-authority: fragments.sh
  + register-patterns.sh + fragment-check.sh + conventions.md.
- C6 changelog-format.md — version-heading + section-header grammar, the TWO
  section orderings reconciled (CHANGELOG_STANDARD_SECTIONS {Added Changed Fixed
  Removed Deprecated Upgrade} vs fragment-fold {…Security Internal}), None.
  sentinel, the checks-1..9 enforcement layer (new-sections-only, #392).
- C5 cli-surface.md — the full 11-subcommand surface (5 in scaffold today; #506
  adds the 6b validators) + the 0/1/2 trivalent exit-code contract (§5 Verdict).
- C7 forgejo-responses.md — reference of the response fields the engine consumes
  across the API surface + the resilience transport contract. Typed Go structs
  are #505/Phase 4 (seam stated).
- C3 workflow-api.schema.json — machine-checkable inputs/outputs lifted from
  workflow-api.md, so a surface change is catchable at PR time.
- property-invariants.md — the four non-schema contracts: semver round-trip,
  manifest round-trip, bake byte-exactness, fragment-consumption transactionality
  (ADR-0009 §6 fail-atomic cut).

Refined:
- C2 manifest.schema.json — $comment expressing the read(write(m))==m + canonical-
  serialization round-trip invariant, cross-linked to property-invariants.md.
- C1 config.schema.json — reviewed against Phase-2 needs; comprehensive as-is,
  no structural change (a valid "refine if needed" outcome; documented in README).
- contracts/README.md — three-contracts table → C1–C7 + properties + validation.

Validation (AC2/AC3) — validate-grammars.sh, reproducible + trivalent:
- C4 grammar vs the historical fragment corpus (changelog.d/ is empty between
  cuts by lifecycle, so the corpus is drawn from git history — 157 files). The
  corpus carries its own positive control: accepts 154 valid, rejects exactly
  the three known conventional-commit-type mistakes (153.feat.md,
  164-fix-self-pin.fix.md, 3.fixed-2.md — the #170/#9 incidents).
- C6 grammar vs the shipped CHANGELOG.md — 51 version headings + 123 section
  headers all parse.
- Empty corpus / missing CHANGELOG → cannot-grade (exit 2), never a vacuous pass
  (shallow-clone safe). Per-grammar accept/reject controls prove the regex
  discriminates before the corpus verdict is trusted (the internal/harness
  instrument-must-fire discipline, #503).

Schema-version decision: kept JSON Schema 2020-12 for C3 to match the existing
C1/C2 set (the ADR §3.2 table's "draft-04" and the #504 AC's "draft-04 or -07"
are met in intent — every schema declares its $schema; consistency across the
set is chosen over the literal draft number). Flagged for review.

Gate: golangci-lint run --timeout=5m → 0 issues; go build ./... + go test
-count=1 ./... + gofmt -l clean (no Go touched — pure docs/schema/shell);
shellcheck clean on validate-grammars.sh; all contract .json parse (jq).
Meta-schema validation is jq-parse-level — no jsonschema tool on host; the new
C3 schema mirrors the already-validated C1/C2 structurally.

Refs #504
fix(contracts): fold PR#518 review should-considers S1–S4 (#504)
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 7s
go-ci / lint + build + test (push) Successful in 7s
cef845ab3c
Surveyor's PR#518 review (4543) landed five should-considers; #518 merged at
c1b5cd0 before these could fold, so they re-land here off current v2/next.

S1 — the AC2/AC3 validator was looser than the C6 contract it checks. The
version-heading regex used one optional-date group `( … )?` spanning both the
Unreleased and the versioned-cut form, so a dateless cut `## [v1.2.3]` passed —
contradicting changelog-format.md §1 ("date MUST be YYYY-MM-DD" for a cut). Fixed
by making it a union: `^## \[Unreleased\]$` (no date) OR a cut form with a
MANDATORY date. Fixed in both validate-grammars.sh and changelog-format.md §1;
a dateless-cut negative control now guards it.

  Tightening the control surfaced a SECOND latent bug (present since #518): the
  separator was a `[-—]` byte-class. Under LC_ALL=C a multibyte em-dash
  (U+2014 = 3 bytes) inside a bracket expression decomposes into four single-byte
  members, so ` [-—] ` matches one byte where the em-dash needs three — the
  documented em-dash separator (the tmux-tell shape) silently never matched. The
  real CHANGELOG uses ` - ` so it never surfaced on the corpus; an explicit
  em-dash control caught it. Fixed to a `(-|—)` alternation (matches the literal
  3-byte em-dash) in both files.

S2 — fragment-format.md §5 over-claimed the validator "fails loud if the counts
drift". The verdict is invariant-based and size-independent: a shallower clone
with fewer historical fragments (e.g. accepted=134) still passes correctly. The
doc now describes the actual invariant (fails loud when a valid-kind name is
rejected or a control fails; counts are diagnostic). Doc softened, script
unchanged — invariant-based is the right design.

S3 — cli-surface.md C5 labelled `manifest-check`→6b and `prune-rc-tags`→8. Both
are Phase 6 orchestrators per ADR-0009 §3.3 row 6 AND cmd/rt/main.go (which C5
cites as source-of-record: "phase 6 orchestrator" ×5). Corrected both to 6. (Same
loose-grouping-over-canonical-table slip as PR#510 S1 — re-banked.)

S4 — ADR-0009 §3.2 table listed C1 as "draft-04"; the shipped config.schema.json
is 2020-12. Table corrected to 2020-12 (the tracker AC5 was already restated to
2020-12 by Bosun on the #518 merge).

S5 (README v2.0.0 vs the v1 renumber) is pre-existing and repo-wide (12 docs
carry v2 references); a partial fix would be inconsistent, so it is filed as a
separate doc-renumber-sweep tracker rather than folded here.

Gate: golangci-lint run --timeout=5m → 0; go build + go test -count=1 ./... +
gofmt clean (no Go touched); shellcheck clean; validate-grammars.sh green with
all controls (dateless-cut + em-dash + garbage) firing.

Refs #504
feat(interfaces): Go interfaces for all internal/* packages (#505)
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 8s
go-ci / lint + build + test (push) Successful in 7s
eeb8fd02e1
Phase 0b interface definitions for every internal package: compile-green,
each method doc-commented with pre/post/error semantics and a contract link.
Implementations land per-phase; the equivalence harness catches regressions
against observable behavior regardless (ADR-0009 §3.1).

## The harness.Verdict lift-vs-alias fork — LIFTED

The named Phase 0b architectural decision (#503 harness doc, ADR-0009 §5):
lift the trivalent Verdict into a new leaf package internal/verdict that both
the harness and every gate-returning surface import. internal/harness now
aliases it (type Verdict = verdict.Verdict), so its Phase 0a API is unchanged
and its tests pass untouched through the alias (behavior-preserving lift).

Rejected — keeping Verdict canonical in internal/harness and importing it from
gate code: that inverts layering (a migration TEST instrument imported by
production gate code). A leaf package depended on by both has zero import
cycles and single-sources the 0/1/2 exit-code contract (cli-surface.md §2).

## Interfaces (11 packages)

- semver: Parser, Version (round-trip invariant; shared BumpLevel lives here)
- conventionalcommits: Parser, Commit
- config: Loader, Config (contract C1)
- changelog: Composer, Parser, Section/VersionSection (C6; kind->section map)
- fragments: Reader, Fragment, Kind (C4; ErrUnknownKind is the fail-loud gate)
- forgejo: Client (15 domain methods) + typed request/response structs (C7)
- bake: Baker (byte-exactness property; LC_ALL=C git subprocess discipline)
- events: Emitter, Event (best-effort; the one fail-loud exception, #159)
- manifest: Store, Manifest (round-trip invariant, contract C2)
- release: Cutter (ADR-0009 §6 fail-atomic transactional cut) + Transaction
- verdict: the lifted canonical trivalent gate-result type

## Discipline (ADR-0009 §5)

- No method returns bool for a gate result: gate/validation methods return
  error (fail-loud sentinel) or carry verdict.Verdict; the only bools are
  parsed data attributes (Commit.IsBreaking, Release.Draft), each doc-noted.
- Sentinel errors per package for every fallible surface. Two packages define
  none, by design and disclosed: verdict (a pure value type, no fallible
  operation) and events (pure-observability, contractually never fails the
  caller per #159 — a sentinel would be unused and contradict its contract).
- release.Cutter puts the byte-check adjacent to fire INSIDE Fire
  (expectedHead SHA), so a caller cannot forget the precondition.

## Gate

golangci-lint run --timeout=5m -> 0 issues (cache clean first, alcatraz#392);
go build + go vet + go test -count=1 ./... + gofmt all clean.

Refs #505
Refs ADR-0009 sections 3.1, 5, 6
feat(cli): rt subcommand skeleton + trivalent exit-code wiring (#506)
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 7s
go-ci / lint + build + test (push) Successful in 7s
ac617d34ea
Complete the C5 CLI surface (contracts/cli-surface.md): all 11 subcommands as
fail-loud skeletons, the 0/1/2 trivalent exit-code contract wired through
internal/verdict, and a byte-exact `rt --help` snapshot test.

## Subcommand surface (C5 section 1)

Adds the 6 Phase-6b cut-time validators (compose-verify, register-check,
changelog-body-check, fragment-check, preflight-push-whitelist,
manifest-precheck) to the 5 Phase-6 orchestrators already scaffolded. All 11
appear in `rt --help`. cobra's generated `completion` command is disabled (not
part of the C5 contract; keeps the help snapshot clean); `help` remains.

## Exit-code contract wired (C5 section 2, ADR-0009 section 5)

main() now maps a command outcome to the process exit code via
internal/verdict.Verdict.ExitCode(): green->0, red->1, cannot-grade->2. A
skeleton subcommand grades as cannot-grade (2), NOT red (1) -- a command that
isn't implemented has produced no verdict, and collapsing that into a graded
negative would misreport it. A cobra arg-parse/usage error (unknown command)
exits 1, explicitly outside the 0/1/2 graded contract (C5 section 2). This
replaces the scaffold's flat exit-1, which the #501 scaffold deferred to Phase 6.

## Tests

- cmd/rt: help-snapshot byte-compare (drift guard), --version, all-11-present +
  count, every skeleton exits cannot-grade, unknown-cmd exits 1, and a unit
  table for the error->exit mapping.
- internal/verdict: Verdict.ExitCode unit table incl. zero-value/unknown ->
  cannot-grade fail-safe (folds PR#521 review 4547 S3 -- the mapping test
  belongs in the verdict package where the type lives).

## Scope: per-subcommand flags deferred to Phase 6 (per C5 section 1)

The tracker scope names "flags matching current bash script flag surface". The
C5 contract authored in #504 (section 1, Surveyor-approved) deliberately does
NOT freeze flag surfaces at 0b -- they are refined per orchestrator in Phase 6
against reality. The skeletons therefore carry no flags yet; the committed
`rt --help` snapshot is the top-level reference, and per-subcommand flag
references land as each orchestrator is ported. Flagged for review.

## Gate

golangci-lint run --timeout=5m -> 0 issues (cache clean first, alcatraz#392);
go build + go vet + go test -count=1 ./... + gofmt all clean.

Refs #506
Refs ADR-0009 sections 3.2 C5, 3.3 phase 0b, 5
feat(semver): implement Parser + Version against #505 interface (#523)
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 9s
go-ci / lint + build + test (push) Successful in 8s
fa31b5d31b
Port scripts/lib/semver.sh to Go, satisfying the Parser/Version interface
landed in #505. The bash implementation is the behavior-of-record; the Go side
is proven byte-identical against it by the #503 equivalence harness on a curated
valid/invalid/edge corpus, with exhaustive value-level coverage in unit tests
ported from tests/semver.bats.

## What lands

- internal/semver/semver.go: strict SemVer 2.0.0 parser (grammar mirrors the
  bash regex identifier-class for identifier-class), Version.String (canonical,
  round-trippable), Compare (§11 prerelease precedence + §10 build-ignored),
  Bump, Max. Compile-time interface-satisfaction assertions (var _ Parser /
  var _ Version) per PR#521 review S2.
- internal/semver/semver_test.go: bats-derived table tests (parse valid/invalid,
  bump, compare §11 chain + symmetry, max) + TestSemverRoundTrip property
  invariant (ADR-0009 §3.2 exemplar: Parse(x).String() == x over a generated
  corpus).
- internal/semver/equivalence_test.go + testdata: the #503 harness pointed at
  semver. A Go oracleshim (testdata/oracleshim) and a bash dispatcher
  (testdata/oracle/semver-oracle.sh) expose both implementations behind an
  identical subcommand surface; the harness byte-diffs stdout + exit_code.

## Behavior-of-record reconciliations (bash oracle is authoritative)

- Bump drops prerelease+build for patch, minor, AND major (semver_bump emits a
  bare X.Y.Z for every level; the interface named major explicitly, minor/patch
  drop by the same release-semantics rule). BumpNone is Go-only (no bash
  equivalent) and returns v unchanged.
- String emits the spec-canonical form (no leading 'v'); Parse tolerates one
  leading 'v' like the oracle. The round-trip invariant is stated over canonical
  inputs.
- semver_apply_pre_v1_policy is NOT ported here: per the #505 interface it is
  config's concern (depends on release-toolkit.yml), not a property of a version.
- Prerelease lexical order uses Go's byte-wise strings.Compare (ASCII); the bash
  oracle pins LC_ALL=C for the same order (§11.4.2). The Go type has no locale to
  escape, so that hazard cannot arise on the Go side.

## Harness vehicle + exit-code fidelity

The Go side of each equivalence case execs a prebuilt binary, NOT `go run`:
observed that `go run` reports its own exit as 1 for any non-zero child exit,
which would manufacture a false RED on the invalid-input cases (compare exits 2;
parse/bump/max exit 1). TestMain builds oracleshim once; the passing
compare/invalid-input case (both sides exit 2) is the standing proof the binary
approach preserves the child's code.

## Gate

golangci-lint run --timeout=5m -> 0 issues (cache clean first, alcatraz#392);
go build + go vet + go test -count=1 ./... + gofmt all clean. Harness teeth
mutation-verified: inverting §11.4.3 (numeric-vs-alphanumeric precedence) reds
compare/numeric-lt-alpha on the stdout surface; reverted to green.

Refs #523
Refs ADR-0009 sections 3.1, 3.2, 3.3 phase 1, 5
Fold PR#525 review 4551 should-considers (S1+S2), both surfaced by Surveyor on
the merged #523 semver impl. Landing them here since #525 fast-forwarded before
the re-touch; #524 is the natural fold surface (S2 is contract-adjacent to cc's
bump-suggest). Doc/comment/test only — no logic change.

S1 -- the ParseUint block carried a FALSE "cannot fail on validated input"
comment. The grammar bounds identifier SHAPE, not MAGNITUDE, so a core exceeding
uint64 overflows and the error branch IS reachable. Correct the comment, add the
overflow rejection as a third "differs by design" bullet in the package doc, and
add TestParse_RejectsOverflow to prove the branch fires. Go rejects such input
(ErrInvalidVersion); the bash oracle silently wraps mod 2^64 (semver_bump on
99999999999999999999999.0.0 yields a corrupt 200376420520689664.0.0 at exit 0).
This is the one place the port is deliberately STRICTER than its oracle -- a
wrapped version is a corrupt version, so it is a documented divergence, NOT an
equivalence case.

S2 -- the #505 Bump postcondition attached "any prerelease/build" only to
BumpMajor, but Minor and Patch drop them too (semver_bump emits a bare X.Y.Z for
every level). Tighten the interface doc to state the drop once for all
incrementing levels; BumpNone remains the sole preserving level. The
implementation already behaved this way (bump/patch-drops-prerelease is green).

Refs #525
Refs #523
feat(conventionalcommits): implement Parser + Commit against #505 interface (#524)
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 8s
go-ci / lint + build + test (push) Successful in 8s
3cca869939
Port the pure functions of scripts/lib/conventional-commits.sh to Go, satisfying
the Parser/Commit interface landed in #505. The bash implementation is the
behavior-of-record; the #503 equivalence harness proves the Go side byte-identical
against the REAL lib on a curated corpus, with the bats-derived table covered
in-process by unit tests.

## What lands

- internal/conventionalcommits/conventionalcommits.go: subject parser (type /
  scope / breaking `!` / description, lowercase-type-only like the oracle),
  BumpLevelFor (breaking->major, feat->minor, fix->patch, else none), and
  CategorizeRange (aggregate highest-wins bump + commits grouped by
  Keep-a-Changelog section). var _ Parser / var _ Commit compile assertions.
- conventionalcommits_test.go: bats-derived table (parse valid/invalid, bump,
  body BREAKING CHANGE detection) + CategorizeRange grouping/order/empty.
- equivalence_test.go + testdata (Go oracleshim + bash cc-oracle.sh dispatcher):
  the #503 harness diffs cc_parse_subject and cc_bump_level_from_subject against
  the Go impl on 33 curated cases (every recognized type, scoped/unscoped, the
  `!` marker, the BREAKING CHANGE footer both spellings, malformed).

## ParseCommit — additive constructor beside the frozen interface

Commit.IsBreaking must also reflect a BREAKING CHANGE body trailer (per the #505
contract), but the interface's only constructor, ParseSubject(subject), sees just
the subject. So a package-level ParseCommit(subject, body) constructor is added
beside the interface: ParseSubject stays the subject-only entry; ParseCommit is
what the range-scanning path uses when a body is available. Flagged for review --
this is additive surface the frozen interface does not name.

## Scope boundary: pure logic, not git integration

The bash lib's git-walking helpers (cc_list_commits_since /
cc_determine_bump_since / cc_categorize_commits_since) shell out to git log and
emit CHANGELOG markdown. Those are NOT ported: CategorizeRange takes an
already-resolved []Commit (the commit source is a Phase-6 git-adapter concern),
and markdown rendering + the register-name scrub (#442) belong to the Phase-3
changelog composer (#507). This package returns the section grouping as DATA. So
the harness diffs the two PURE oracle functions; the aggregate/grouping is
unit-tested against the lib's documented rules.

## Oracle-corrected test assumption

A hand-written "feat: (colon + trailing space) is malformed" assumption was
WRONG: probed against the oracle, cc_parse_subject accepts it with description
" " (one space; [[:space:]]* consumes zero, .+ takes the space). The impl already
matched the oracle byte-for-byte; the test assumption was the error. Corrected and
locked in the equivalence corpus (parse/whitespace-desc). The RE2-vs-POSIX regex
split I worried about does not materialize here -- verified, not assumed.

## Gate

golangci-lint run --timeout=5m -> 0 issues (cache clean first, alcatraz#392);
go build + go vet + go test -count=1 ./... + gofmt all clean. Harness teeth
mutation-verified: dropping body BREAKING CHANGE detection reds both
bump/footer-* cases on stdout (bash major vs go patch); reverted byte-identical.

Refs #524
Refs ADR-0009 sections 3.1, 3.3 phase 1, 5
feat(config): implement Loader + Config against #505 interface + C1 schema (#527)
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 15s
go-ci / lint + build + test (push) Successful in 15s
5c8ec7ef77
Port scripts/lib/config.sh (the getters + config_validate) to Go, satisfying the
Loader/Config interface landed in #505. The bash lib is the behavior-of-record;
the #503 equivalence harness proves the Go Load path byte-identical against the
REAL lib on a curated fixture corpus, with the bats-derived table covered
in-process by unit tests.

## What lands

- internal/config/config.go: Load = parse (yaml.v3, KnownFields) + validate
  (config_validate's checks in order) + project into a defaulted Config. Getter
  default semantics mirrored per-field -- three distinct bash behaviors:
  empty->default for _config_get_field getters, empty->preserved for yq // getters,
  version_files missing->[VERSION] vs []->empty. Plus RenderTag + DefaultPath.
  var _ Loader compile assertion.
- internal/config/interface.go: completes the Config struct against the full C1
  schema (grow-as-consumed, per the #505 struct doc) + adds ErrParse (additive
  third sentinel; see below).
- config_test.go: bats-derived table (validate accept/reject, getters, defaults,
  traversal, basename, version-set, section_format, pre_v1, hooks) + the
  milestone-74 gate (every real consumer config validates unchanged) + the
  Go-stricter unknown-key + parse-vs-schema-distinct pins.
- equivalence_test.go + testdata (Go oracleshim + bash config-oracle.sh dispatcher):
  the #503 harness diffs config_validate + every getter against the Go Load path on
  ~60 curated cases; TestMain builds the shim to a BINARY (not go run, which
  collapses a non-zero child exit to 1).

## C1 schema differs from config_validate -- Load follows the oracle (+ 1 divergence)

config.schema.json and the bash config_validate disagree on 4 axes: version
required (oracle) vs optional (schema); version in {1} (oracle #335) vs any-int
(schema); ../VERSION traversal rejected (oracle #181) vs accepted (schema
basename-pattern); unknown keys ignored (oracle) vs rejected (schema
additionalProperties:false).

Load follows config_validate -- the oracle adopters actually run + the home of the
#181/#335 guards -- on the first three, matching byte-for-byte. On the fourth Load
is deliberately STRICTER (rejects the unknown key), because the #505 interface doc
dictates it ("a typo is a hard error, not a silently-ignored key"). This is the ONE
divergence, disclosed like semver's uint64-overflow: the equivalence harness grades
that pair RED intentionally (TestEquivalence_UnknownKeyDivergence) + a unit test
pins Go's rejection. Reconciling config.schema.json to the oracle is filed as #529
(depends on this landing).

## ErrParse -- additive third sentinel

#505 named ErrNotFound + ErrSchemaViolation. The AC ("parse errors distinct from
schema-violation errors") needs a third: a syntax error (not YAML) is ErrParse; a
valid-YAML-wrong-shape (yaml.TypeError, incl. an unknown key) is ErrSchemaViolation.
Additive beyond #505, flagged -- same precedent as cc's ParseCommit constructor.

## Scope boundary: not ported

resolve_publish_mode (env + workflow-input layering -- an orchestrator concern,
Phase 6); config_render_tag's implicit DEFAULT-path arg (RenderTag is a Config
method, so the config is already loaded). #337 (broaden basenames to
pyproject/Cargo.toml) stays SEPARATE -- folding it would break byte-equivalence
(config_validate REJECTS pyproject.toml today, #213). New dep gopkg.in/yaml.v3:
no stdlib YAML; KnownFields(true) gives the C1 unknown-key rejection, and
TypeError-vs-syntax-error gives the parse/schema sentinel split.

## Gate

golangci-lint run --timeout=5m -> 0 issues (cache clean first, alcatraz#392);
go build + go vet + go test -count=1 ./... + gofmt all clean. Harness teeth
mutation-verified on BOTH arms: disabling the traversal guard reds
validate/vf-traversal-supported-basename (exit_code bash 1 vs go 0); breaking the
section-separator default reds section-separator/minimal-default (stdout " - " vs
" -- "). Both reverted byte-identical. NOTE: the traversal fixture ../../etc/secret
did NOT isolate the guard (basename "secret" is rejected independently), so
../VERSION + sub/../VERSION fixtures (supported basename, traversal-only) were added
to vary the guard's actual axis.

Refs #527
Refs ADR-0009 sections 3.1, 3.3 phase 2, 5
Fold Surveyor's review-4555 non-blocking items for #527. PR#530 fast-forward-merged
before I could re-touch it (the arc's 4th stamp->merge race), so they land on the
#528 branch -- the same fold-forward as #525's items into #526.

- S1: two FROZEN Config field-doc EXAMPLES were factually wrong, and #527's own
  validator falsifies them: ReleaseType said "standard"/"rolling" (actual set:
  node/go/python/multi); PublishMode said "draft"/"publish" (actual: draft/
  immediate). "Keep the frozen field exact" protects the CONTRACT -- the names +
  types, which stay byte-unchanged from #521 -- NOT a doc example the same PR's
  validator contradicts. Corrected to the enforced value sets.
- nit-a: a comment on Load's non-ENOENT read-error mapping (EACCES/EISDIR ->
  ErrNotFound) documenting the intentional exit-code-equivalent collapse vs the
  bash oracle.
- nit-b: the milestone-#74 gate (TestLoad_RealConsumerConfigs) now FAILS rather
  than Skips on an absent config, so a path drift reddens the gate instead of
  passing vacuously.

Doc/comment/test only; no logic change (Surveyor verified the logic on #530).

Refs #527
feat(events): implement Emitter + event types against #505 interface (#528)
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 15s
go-ci / lint + build + test (push) Successful in 15s
4757790479
Port scripts/lib/events.sh (event_emit) to Go, satisfying the Emitter/Event
interface landed in #505. The bash lib is the behavior-of-record; the #503
equivalence harness proves the Go Emit output byte-identical against the REAL lib.

## What lands

- internal/events/events.go: Emit serializes {ts, kind, ...fields[, verdict]} as one
  compact JSON line, field order preserved. jq-compatible escaping via encoding/json
  with SetEscapeHTML(false) (verified byte-identical to jq -c). Best-effort: never
  returns an error, never aborts the caller, defines no sentinel error (the #159
  pure-observability guarantee -- the one intentional deviation from section-5
  fail-loud). var _ Emitter compile assertion.
- events_test.go: field order, no-field / empty-ts, special-char round-trip, the
  verdict trailing field, always-valid-JSON fuzz, and never-aborts (nil + failing
  sink -- the Go analogue of the bats jq-missing test).
- equivalence_test.go + testdata (Go oracleshim + bash events-oracle.sh dispatcher):
  the #503 harness diffs event_emit vs Go Emit on 15 cases. TestMain builds the shim
  to a BINARY (not go run). The dispatcher shadows `date` (deterministic ts) and
  redirects event_emit's stderr to stdout -- event_emit writes JSON to stderr and
  the harness captures stdout -- so the two surfaces are comparable.
- docs/architecture/contracts/events.md: the wire-format contract (AC4).

## Verdict serialization (design call)

The #505 Event carries a trivalent Verdict (green/red/cannot-grade; "" = none). It
serializes as a trailing "verdict" field when non-empty -- placed last so the
interface's stated {ts, kind, ...fields} shape is preserved and a plain event stays
byte-identical to bash. bash has no verdict concept; the harness maps Go's Verdict
to a trailing verdict= field on the bash side to prove the two match.

## Escaping boundary (disclosed)

Go encoding/json (HTML-off) matches jq -c byte-for-byte over the realistic value
domain (quotes, backslashes, =, tabs, newlines, <>&, common unicode -- all verified).
Two exotic chars diverge: DEL (0x7f) and U+2028/U+2029; both stay valid JSON and
neither occurs in release-pipeline event values. Same disclose-the-boundary shape as
semver's ASCII-vs-locale note.

## Events is CENTRAL, not scattered

scripts/lib/events.sh is a dedicated 3.4K lib (not printf scattered across
orchestrators), so the harness case is a real byte-oracle, not a synthesized one.

## Gate

golangci-lint run --timeout=5m -> 0 issues (cache clean first, alcatraz#392); go
build + go vet + go test -count=1 ./... + gofmt all clean. Harness teeth
mutation-verified: renaming the verdict key "verdict"->"grade" reds
emit-verdict/green (stdout mismatch). Reverted byte-identical.

Refs #528
Refs ADR-0009 sections 3.1, 3.3 phase 2, 5
chore(contracts): reconcile config.schema.json (C1) with config_validate oracle
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 15s
go-ci / lint + build + test (push) Successful in 15s
8c2475976d
The C1 schema was looser than the config_validate byte-oracle on three axes
(surfaced by the #527 framing-verify). Tighten the schema to match the oracle
— the same accept/reject surface the v2 Go Load (internal/config) already
enforces:

  1. version REQUIRED — add to `required`; config_validate rejects an absent
     version (bats "missing version returns 1").
  2. version in {1} — add `enum: [1]`; #335 rejects 2/999/0. The set grows as
     new schema versions ship (mirrors SUPPORTED_SCHEMA_VERSIONS).
  3. version_files repo-relative — the basename-only pattern accepted
     `../VERSION` and `/VERSION`; tighten to reject absolute paths and `..`
     traversal segments (#181 repo-relative guard) while keeping the #213
     basename set {VERSION, package.json}.

Axis 4 covers the full #181 guard (absolute + traversal), not just the `../`
example named in the tracker: config_validate rejects a leading-`/` absolute
path too (config.go:183), the same schema-looser-than-oracle divergence under
the same issue. Disclosed.

The unknown-key axis stays as-is (`additionalProperties: false`): the schema
and Go Load reject an unknown key; only bash silently ignores it. That is the
one intentional Go>bash divergence (per the #505 interface), not a drift to
reconcile.

Also refresh the internal/config package doc, which described the schema as
"drifted ... reconcile is a separate follow-up" — now stale once this lands.
No behavior change (schema is a doc-of-contract; Go Load hand-rolls and does
not read it).

Post-review fold (#529 review 4559): disclose the residual over-strictness in
the version_files description — the tightened pattern also rejects a trailing-
slash form (`VERSION/`) and an empty entry, which config_validate and Go Load
basename-normalize or skip and thus accept. Safe direction (over-strict, not
under-strict), out of the real-config domain, named in the artifact per the
disclosed-boundary discipline.

Verification (see PR body): the tightened version_files pattern matches the
oracle on 22 accept/reject vectors (ECMA-262), independently re-verified by the
reviewer against the real bash + real Go oracles on a 31-vector sweep; the 3
real consumer configs validate under the tightened schema; a 7-config negative
control confirms each tightened axis rejects what config_validate rejects; C4/C6
grammar validator green; full Go gate green.

Refs #529
feat(fragments): implement Reader + Fragment against C4 grammar + bash oracle
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 15s
go-ci / lint + build + test (push) Successful in 16s
6a591bbc07
Phase 3 opens with the fragment engine. internal/fragments implements the
Phase 0b (#505) Reader/Fragment surface against the C4 filename grammar (#504)
and the scripts/lib/fragments.sh byte-oracle.

Read implements the C4 grammar directly: a fragment is <id>.<kind>.md with a
non-empty id and a recognized kind. Read fails loud — ErrUnknownKind (kind not
one of the seven) / ErrEmptyID (empty id) — never a warn-and-drop nil, which is
the #170/#9 silent-content-loss guard. List is the back-compat enumerator
(warn-and-skip), AssertNoUnknownKinds is the fail-loud gate release-prep calls
first. Fragment carries ID/Kind/Body/IsBreaking/BumpLevel; the breaking marker
(^BREAKING[ -]CHANGE([: ]|$)) promotes a bump to major regardless of kind.

AggregateBump (= determine_bump_from_fragments) lands as a package-level helper
beside the frozen Reader interface — an aggregate over []Fragment is not a
Reader method — mirroring conventionalcommits.CategorizeRange.

The bash oracle splits the grammar across fragment_kind_from_path (kind only)
and fragment_id_from_path (id only); their conjunction is the grammar Read
reproduces. Read's before-final-kind id diverges from fragment_id_from_path's
`${base%%.*}` (before-first-dot), which truncates the five real dotted-id corpus
names (16-consumer-side-bump-v0.3.1.internal.md -> 16-consumer-side-bump-v0).
That function has ZERO consumers in scripts/ — dead, buggy code — so the port
implements the contract's round-tripping id and does NOT reproduce the bug; the
divergence is disclosed in the package doc and the id round-trip is unit-tested
rather than harness-graded (unlike config's unknown-key, which is a live
consumed surface graded RED).

Verification (see PR body): the equivalence harness (reused Phase-1 vehicle —
Go oracleshim + bash fragments-oracle.sh + prebuilt-binary TestMain) diffs the
consumed surfaces (assert_no_unknown_kinds, determine_bump_from_fragments,
list_fragments, fragment_is_breaking) byte-for-byte across the fixture corpus
(21 cases, all green). Harness teeth mutation-verified on two narrow axes
(breaking-marker hyphen alt; removed->major mapping), each reddening its
targeted case and reverted byte-identical. Full Go gate green.

Refs #532
feat(changelog): implement Composer + Parser + register-scrub against C6 grammar + bash oracle
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 16s
go-ci / lint + build + test (push) Successful in 16s
b2720b9ed0
Phase 3 continues with the changelog engine. internal/changelog implements the
Phase 0b (#505) Composer/Parser surface against the C6 CHANGELOG grammar (#504)
and the scripts/lib/changelog.sh byte-oracle, consuming #532's Fragment.

Composer.Compose folds fragments into a VersionSection (categorize_fragments +
changelog_scaffold_missing_sections: FragmentFoldOrder grouping, per-fragment
frontmatter-strip + paragraph-normalize, standard sections backfilled with the
None. sentinel). Composer.Transition performs the Unreleased->released move
(changelog_transition, no fragments, Keep-a-Changelog default heading). Parser
Parse/LatestVersion/UnreleasedContent read the changelog, fail-loud on a
C6-malformed heading (ErrMalformedHeading) and distinguish an absent from an
empty Unreleased section (ErrNoUnreleasedSection).

Beside the frozen fragment-only Composer interface, as additive package-level
functions (the CategorizeRange/AggregateBump pattern): RenderCommitSections
renders a conventionalcommits.CategorizedRange to ### Kind bullets AND applies
the register scrub -- the rendering half of cc_categorize_commits_since that
#524 deferred here (its own package doc), fulfilling the load-bearing #442
obligation that a chamber-name in a commit subject is scrubbed to [reviewer] at
compose time. MergeSections/ScaffoldMissingSections/NormalizeParagraphs are the
composer building blocks. The register vocabulary + scrub land in a new
internal/register package, mirroring the shared register-patterns.sh lib (reused
by the #534 register-check gate).

Verification (see PR body): the equivalence harness (reused vehicle -- Go
oracleshim + bash changelog-oracle.sh sourcing the three real libs +
prebuilt-binary TestMain) diffs the composer surfaces byte-for-byte across 30
cases (scrub, normalize, merge, scaffold, compose, latest-version,
unreleased-content, transition). Harness teeth mutation-verified on three
load-bearing axes (register scrub, paragraph normalizer, transition heading),
each reddening only its targeted case and reverted byte-identical. Full Go gate
green.

Refs #533
feat(gates): register-check --stdin gate — Go port (#534 PR 1/3)
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 16s
go-ci / lint + build + test (push) Successful in 16s
8cbf5ca0ca
Ports register-check.sh's --stdin content-scan (scan_stdin) to Go as the
first of Phase 3's three cut-time gates. Adds the detection dual of
register.ScrubLine plus a new internal/gates package for the cut-time gate
layer that internal/release.Cutter.Prepare consumes.

- internal/register.Detect / FindHits: the detection primitive, sharing
  Patterns + the word-boundary regex with ScrubLine. FindHits reproduces
  grep -n line semantics — 1-based numbering, one hit per matching line (a
  two-name line is a single hit), and the trailing-newline /
  unterminated-final-line edges of grep's line delimiting.
- internal/gates.RegisterCheckStdin: the --stdin gate — OK() iff no hit,
  the Go equivalent of the script's 0/1 exit.

Scope boundary (disclosed): this ports the CUT-TIME path — the --stdin
content-scan compose-verify fires over a composed section. register-check.sh's
PR-time file-scan + --git-log modes, the .register-allowlist, the #396 inline
exemptions, and the #435 REGISTER_CHECK_PATTERNS adopter override are the
PR-time CI grep-gate: no consumer inside the cut transaction, so they stay in
bash until a Phase-6+ CLI has reason to replace that invocation surface.

Verification: 20 byte-equivalence cases vs the real scripts (register-detect =
the grep -EinH hit report, source-of-record register_build_regex;
register-check-stdin = the real register-check.sh --stdin exit). Harness teeth
mutation-verified on two axes, each reddening only its targeted cases, reverted
by re-edit byte-identical:
  - pattern-drop (Surveyor->SurveyorX) -> detect/bare-hit + check-stdin/bare-hit
    + detect/multi-line RED (check-stdin/multi-line stays green: Bosun still
    hits, so the exit is unchanged while the line-detail differs — the detect
    surface catches what the exit surface cannot).
  - skip-unterminated-final-line -> detect/no-trailing-newline +
    check-stdin/no-trailing-newline RED (guards grep's final-line semantics).
Mutation-verify requires `go test -count=1`: the oracleshim is a runtime-built
binary (rebuilt in TestMain via exec go build), invisible to go-test's cache,
so a cached GREEN would mask the mutation. Plus unit tests for Detect / FindHits
(8) and RegisterCheckStdin (3).

Gate: golangci-lint 0 (cache-clean #392) . build . vet . test -count=1 . gofmt
. shellcheck clean on the oracle dispatcher.

Refs #534
feat(gates): changelog-body-check gate — Go port (#534 PR 2/3)
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 16s
go-ci / lint + build + test (push) Successful in 16s
6b22ee474f
Ports changelog-body-check.sh (the 9-check mechanical Cold-Read linter) to Go
as the second of Phase 3's three cut-time gates — the density + structure gate
compose-verify fires against the newest CHANGELOG version-block.

internal/gates.ChangelogBodyCheck([]byte) BodyCheckResult runs all nine checks
and returns per-check verdicts (PASS/WARN/FAIL) + the 0/1/2 exit:
  1 header+date (FAIL)    2 standard-sections (FAIL)   3 forward-refs (FAIL)
  4 length (WARN)         5 bullet/prose style (WARN)  6 dup headers (FAIL)
  7 sentence density (FAIL+WARN)  8 paren depth (FAIL)  9 paragraph density (FAIL+WARN)

Target section resolved leniently (newest `## [vX.Y.Z]`, else `## [Unreleased]`,
else any `## [`), matching the bash #440 fix — a malformed heading surfaces as a
check-1 FAIL, not an error (intentionally distinct from changelog.Parser.Parse's
strict fail-loud). Reuses changelog.StandardSections() for check 2 (single-source
with the compose scaffold, #453). Byte-faithful ports of the awk paragraph
iterator (fence-verbatim skip), the sed sentence split, wc -w word counting, the
paren-depth scan, and the forward-ref version compare.

Fork C (Bosun-confirmed): WARN/FAIL bands byte-preserved. The #491 WARN-5-to-FAIL
promotion is a separate behavior change on its own tracker, NOT part of this
byte-faithful port. The CHANGELOG_BODY_CHECK_* env thresholds are a PR-time-CI /
Phase-6 orchestrator concern (disclosed), same boundary as the register #435
override; the port uses the built-in defaults.

Verification: 14 byte-equivalence cases vs the REAL changelog-body-check.sh,
comparing a normalized per-check verdict vector (`N:PASS|WARN|FAIL`, keyed on the
structural `VERDICT: check N` prefix, not the rendered prose) + the 0/1/2 exit.
Fixtures span every check's PASS + its FAIL/WARN band, the Unreleased skip-path,
the paragraph-iterator fence-skip (fenced-code: density PASS while check-5's
faithful fence-unawareness WARNs), and the no-section exit-2 (empty vector).
Harness teeth mutation-verified on two logic axes, each reverted by re-edit
byte-identical:
  - remove the fence-skip -> fenced-code.md RED (fence content counted -> 7+8 FAIL)
  - weaken dup-detection (n>1 -> n>2) -> dup-headers.md RED
Mutation-verify requires `go test -count=1` (the oracleshim is runtime-built,
invisible to go-test's cache). Plus 8 unit tests: the structure/exit contract,
the Unreleased-skip path, and the byte-sensitive helpers (wcWords, maxParenDepth,
splitSentences, iterParagraphs, parseSemver).

Gate: golangci-lint 0 (cache-clean #392) . build . vet . test -count=1 . gofmt
. shellcheck clean on the oracle dispatcher.

Refs #534
feat(gates): compose-verify preflight — Go port (#534 PR 3/3)
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 16s
go-ci / lint + build + test (push) Successful in 16s
5b56d25deb
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
feat(manifest): implement Store — Read/Write/CheckDesync + round-trip (#542)
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 16s
go-ci / lint + build + test (push) Successful in 16s
9475e9e090
First of Phase 4's two package ports. Implements internal/manifest.Store
against the frozen #505 interface + the C2 schema (manifest.schema.json),
landed via contract-driven TDD-per-phase (ADR-0009 §3.1).

Store.Write serializes canonically — byte-compatible with the bash `jq -n`
writer (reusable-release.yml:429-433): the C2 key order (schema,
last_released_sha, last_released_version, last_released_tag, last_released_at),
the integer `schema: 1`, 2-space indent, trailing newline, no HTML-escaping.
The write is atomic (path+".tmp" + rename), mirroring the bash writer;
concurrency serialization is #499's fail-atomic remit.

Store.Read decodes strictly (DisallowUnknownFields = C2 additionalProperties:
false) and validates against C2 (required fields, schema const 1, 40-hex
last_released_sha pattern). The round-trip property read(write(m)) == m holds
for every C2-valid record (property-invariants.md §2), asserted by
TestManifestRoundTrip.

Store.CheckDesync ports the git-state sub-checks of manifest-check.sh §5 (#56):
5a last_released_sha exists, 5b it is an ancestor of HEAD, 5c the recorded tag
resolves and points at it. Returns ErrDesync fail-loud with the specific
disagreement.

Sentinels reconcile the frozen #505 set with the #542 AC's four named error
kinds: ErrNotFound / ErrSchemaViolation / ErrDesync (frozen) + ErrParse (new,
JSON-syntax) + ErrAtomicWrite (new, write-transaction). ErrParse joins
ErrSchemaViolation so the frozen "malformed -> ErrSchemaViolation" contract
still holds while a caller can errors.Is the finer distinction.

Equivalence harness (reuse of the #503 vehicle — oracleshim + bash dispatcher,
prebuilt binary): Write byte-equivalence vs the real jq -n writer (3 cases),
Read verdict vs bash's jq empty/jq -r behaviour (valid + malformed). The C2
constraints bash never enforced (additionalProperties, required-completeness)
have NO bash oracle — bash lenient-accepts where Go strict-rejects per the C2
contract + §5; TestManifestSchemaStricterThanBash asserts that split as a
standing disclosure. Harness teeth mutation-verified on two axes, each reverted
by re-edit byte-identical: Write indent 2->4 REDs the write equivalence; drop
DisallowUnknownFields REDs the schema-stricter test. Mutation-verify requires
`go test -count=1` (the oracleshim is runtime-built).

Gate: golangci-lint 0 (cache-clean #392) . build . vet . test -count=1 . gofmt
. shellcheck.

Refs #542
feat(forgejo): #541 typed Forgejo API client (Phase 4 2/2)
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 17s
go-ci / lint + build + test (push) Successful in 17s
249e522a28
Port scripts/lib/forgejo-api.sh to internal/forgejo.Client against the
frozen #505 interface + contract C7 — the second of Phase 4's two package
ports (sibling: #542 manifest, merged). Seven reads, eight dry-run-aware
mutations, and a transport layer (uniform idempotency-gated retry/backoff,
pagination-to-completion, strict-mode unmarshal). The jq extractions and
`jq -nc` payloads in forgejo-api.sh are the byte-authority.

Verified by the #503 equivalence vehicle — captured-production-fixture read
replay (httptest.Server + real forgejo-api.sh jq) and dry-run payload diff
(forgejo_payloads surface) — plus httptest fault-injection for the
resilience layer and a disclosed-boundary standing test for the
Go-stricter-than-bash schema strictness. Harness teeth mutation-verified on
two axes (HTML-escape, presence-check), each reverted byte-identical.

Corrects the frozen BranchProtection field set (interface-sanctioned Phase-4
enumeration) against its real consumer.

Refs #541
feat(bake): #546 pure Baker for the self-bootstrap tree (Phase 5 1/2)
Some checks failed
go-ci / lint + build + test (pull_request) Failing after 18s
9ae59e8aae
Port scripts/lib/build_bake.sh to internal/bake.Baker against the frozen
#505 interface — the first of Phase 5's two packages (sibling: #547
self-bootstrap, which consumes this). The Baker produces the baked git
TREE: it rewrites the build-bake marker in the canonical 5 reusable
workflows to req.ToolkitRef and returns `git write-tree` over the
result. It is PURE — a throwaway GIT_INDEX_FILE seeded from HEAD with the
marked files' rewritten working-copy bytes overlaid — so it never
touches the caller's working copy or index. The commit-wrapping,
tag/push, and HEAD-untouched guarantees of the bash oracle are the
caller's (#547): the byte-exactness invariant is the TREE, not the
nondeterministic commit (property-invariants.md sec.3).

Behavior-equivalence-first (ADR-0009 sec.3.1 nuance): the contract IS
bake(input).tree_sha == bash_bake(input).tree_sha. Verified by the #503
equivalence vehicle (prebuilt oracleshim + a bash dispatcher sourcing
the real build_bake.sh) over a real-reusables + edge fixture matrix,
comparing the tree SHA on stdout against BOTH bash mechanisms
(create_detached_bake_commit AND bake_toolkit_ref + write-tree) — which
empirically proves the two historical mechanisms are tree-identical and
the one pure Go Baker matches both. Harness teeth mutation-verified on
two axes (trailing-newline strip: broad; first-per-line drop: narrow to
the two-markers fixture), each reverted byte-identical.

Refs #546
fix(bake): give the equivalence oracle a git identity (CI commit-tree)
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 17s
go-ci / lint + build + test (push) Successful in 18s
9e77994c90
The repin-mode oracle's create_detached_bake_commit runs `git commit-tree`,
which needs a committer identity. bake-oracle.sh nulls GIT_CONFIG_GLOBAL/
SYSTEM for reproducibility, so with no config to fall back on a CI container
(root, no gecos/mail) cannot auto-detect one and the oracle exits empty under
set -e — reddening every repin equivalence case in go-ci, while a dev host
silently auto-derived an identity from passwd and stayed green. Export
GIT_AUTHOR_*/GIT_COMMITTER_* so commit-tree has an identity regardless of
environment; the committer does not affect the compared TREE.

Reproduced + fixed against the go-ci image (forgejo-ci-go): full gate green.

Refs #546
The Baker sources each canonical file's bytes from the working copy (design
call 4) while the bash oracle sources from HEAD; the two coincide only when
the working copy is clean-staged at base. That precondition was documented
but unenforced — a dirty canonical file (uncommitted change, marker intact)
made Bake return a tree that silently diverges from the oracle, with no error
(release-toolkit#546 PR#548 review, Surveyor 4585). Enforce it per canonical
file with `git diff --quiet HEAD -- f` -> ErrBakeFailed on any divergence, the
§5 precondition-check-inside-method milestone #77 names.

Design-call 2 (repin==cutprep==Go) is preserved: the check only fail-louds a
dirty working copy, so the clean-staged equivalence fixtures are unaffected.
The dirty-wc test asserts ErrBakeFailed (not a silent divergent tree);
mutation-verified — neutering the check reds the test (BEHAVES differently),
clean-staged equivalence stays green.

First commit of #547 (self-bootstrap); caller-seam wiring follows.

Refs #547
feat(selfboot): #547 wire rt self-bootstrap caller-seam over bake.Baker
Some checks failed
go-ci / lint + build + test (push) Successful in 18s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 3s
go-ci / lint + build + test (pull_request) Successful in 18s
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) Failing after 5s
register-check / check (pull_request) Failing after 0s
tests / bats (pull_request) Successful in 1m57s
tests / shellcheck (pull_request) Successful in 8s
7b3eda50e7
Add internal/selfboot — the self-bootstrap caller-seam composing the pure
internal/bake.Baker (#546) into the detached bake-commit the toolkit's own
re-pin flow needs. Completes the dogfood meta-loop: rt bakes rt's own
consumer-ready reusable-workflow artifacts.

CommitBake bakes req.ToolkitRef via bake.Baker (a tree) and wraps it in a
detached commit parented on HEAD — the Go composition of build_bake.sh's
create_detached_bake_commit. The TREE is the byte-exactness invariant; the
commit, carrying a nondeterministic committer/timestamp, is what the rc tag
points at. HEAD and the working copy are untouched (Baker is pure; commit-tree
moves no ref). The commit carries the ambient git committer identity — none is
injected, because a real release commit must attribute to whoever cuts it and
the tree is identity-free.

Milestone #77 GATE (ADR-0009 section 3.3 Phase 5): TestRoundTrip_ThreeActorBootstrap
round-trips the full three-actor bootstrap (cut-prep bake / post-cut 'main'
reset / re-pin) against a scratch repo and asserts tree-SHA equality on all
cases vs the production build_bake.sh (roundtrip-oracle.sh), plus the
round-trip IDENTITY property (post-cut reset restores 'main' byte-exactly).
The scratch dogfoods the 5 REAL toolkit reusables (rt bakes rt).

Scope (Bosun-ratified forks, #547):
- No new `rt` subcommand — the frozen C5 CLI surface stays Phase-6
  orchestrators + Phase-6b validators; bake/repin are toolkit-self machinery,
  not adopter-facing. A C5 expansion would be a separate contract decision.
- Commit-wrap built here; the side-effecting outer orchestration (fetch,
  tag-push, PR-open, request-review — repin.sh shell + forgejo API) stays bash
  break-glass under the floating-pin discipline (#456); consumer-facing `rt`
  wiring lands with the Phase-6 orchestrators (#508).
- Arbitrary-base-without-checkout (create_detached_bake_commit BASE arg — how
  repin.sh bakes off origin/main while HEAD stays on main) is a Phase-6
  concern; CommitBake parents on HEAD, consistent with Baker sourcing the
  clean-staged working copy (#546 design-call 4).

Mutation-verified (both reverted byte-exact): (1) wrapper ignores the requested
ref -> tree-equivalence + round-trip GATE red; (2) oracle bakes the wrong
re-pin ref -> only the round-trip re-pin equality red (the bash side is
genuinely baked and compared live). Full gate green local + forgejo-ci-go:latest.

Refs #547
bosun requested review from surveyor 2026-07-26 21:08:02 +02:00
surveyor left a comment

Composite review — PR#550, v2/next → main (Phase-5 close)

Composite verification of the 48-commit Phase 0a→5 substrate landing on main (base main@d7571c7, head v2/next@7b3eda5, merge_base 7fa5ce6). Not a per-PR re-review — every individual PR was Surveyor-stamped; this verifies the aggregate: no drift from what was stamped, the additive-not-deletive property, and the post-merge state. The structural merge is sound. There is one real blocker: register-check fails on this merge with 22 chamber-name/jargon drift hits that land on public main — must-fix before a clean merge.

Structural composite — verified clean

Check Result
No drift from stamps 48 commits, all engineer-authored arc work, conventional subjects mapping to the stamped PRs (#367 docs → #534 gates → #541/#542 Phase 4 → #546/#547 Phase 5). v2/next tip 7b3eda5 == my #549 stamp; 9e77994 (#548) present. No unexpected/non-arc commits.
Additive-not-deletive 0 deletions across 347 changed files. Go substrate lands alongside bash.
Bash substrate preserved scripts/*.sh count 14 on main == 14 on v2/next — no scripts/*.sh removed (rolling-PR machinery on main continues to work).
Functional CI (the substrate gates) all green: go-ci (pull_request + push), changelog-body-check, manifest-check, tests/bats, tests/shellcheck, and the new check-self-bootstrap (#547's gate) — all success on 7b3eda5.
Topology Do:merge (merge commit preserving v2/next history) is the stated intent — verifiable only at merge; the intent is sound (keeps the arc's branch history as a first-class anchor).

The substrate itself is exactly what I reviewed and stamped across the arc, it lands additively, and every functional check is green. On the code, this is clean to merge.

The blocker — register-check fails: 22 drift hits, NEW to main

register-check (both check and register-drift check jobs) is failure on 7b3eda5. I reproduced scripts/register-check.sh against the tree: 22 register-drift hits, all in docs/architecture/** — chamber-names / invented-jargon leaking from maintainer discourse into adopter-facing docs. This is real, not infra.

Why the individual reviews couldn't catch it: register-check.yml fires only on branches: [main]. The entire Phase 0–5 arc landed on v2/next, which runs only go-ci. So register-check runs here for the first time — against 48 commits of accumulated docs. This is precisely the latent, aggregate-only drift a composite review exists to surface.

It is advisory, not blocking — but it lands drift on public main. main's branch protection has enable_status_check=false (the earlier merge attempt was blocked on the approval count, not CI), so register-check does not hard-block. But all three flagged docs are 404 on main — the merge introduces this drift. Consequences of merging as-is:

  1. Chamber-jargon ships in the released toolkit's own architecture docs — the exact thing register-check + #387 exist to prevent.
  2. register-check scans the whole tree, so once resident it becomes a persistent red for every future main PR until scrubbed.

This is new information the operator's timing-ratification didn't account for.

The 22 hits, characterized (actionable)

19 attribution-credit hits — scrub per #387 (genericize: drop the reviewer name, keep the technical rationale):

  • QM / Quartermaster ×13 (e.g. arc42 README.md, 05/06/07-*view.md: "QM substrate-verified", "the Quartermaster confirmed…", "Both confirmed by QM.")
  • Surveyor ×6 (e.g. branch-strategy.md: "Surveyor review 3722, verified…"; config.schema.json:52: "per Surveyor review 3725"; test-strategy.md, validate-grammars.sh).

3 load-bearing / false-positive — allowlist or rephrase, do NOT scrub the content:

  • arc42/12-glossary.md:31 "EW (engineer-week)" and 11-risks-technical-debt.md:57 "engineer-weeks" — an effort unit, not a chamber. The pattern over-matches the compound engineer-week.
  • contracts/fragment-format.md:102 "register-patterns.sh: crew names + substrate-honest) is scrubbed to…" — a meta-reference documenting the scrub itself; the literal phrase trips its own check.

The current .register-allowlist exempts docs/adr/ (+ CHANGELOG.md, tests/compose-verify.bats) but not docs/architecture/. Per the check's own guidance ("first resort is to scrub, not allow-list"), the 19 credits should be genericized; the 3 load-bearing hits are legitimate .register-allowlist entries (or a one-word rephrase).

Recommendation

Scrub the 19 attribution credits + allowlist/rephrase the 3 load-bearing hits on v2/next (a doc-only change, no code impact — the substrate I stamped is untouched), re-run register-check to green, then this merges clean and I stamp APPROVED. That keeps main's register-check green going forward and honors #387.

Because register-check is advisory and the merge is operator-ratified, whether to scrub-first (recommended) vs merge-now + fast-follow scrub vs allowlist docs/architecture/ is Bosun's / the operator's call — I'm surfacing it as must-fix-before-clean-merge with the reproduction and the persistent-red consequence so the decision is made with full information, not discovered later.

Verdict

COMMENT — not APPROVED. The composite substrate is sound: additive, no deletions, bash preserved, no drift from what I stamped, every functional gate green. I'm withholding the stamp solely on the register-check drift — 22 chamber-name/jargon hits (19 scrubable per #387 + 3 allowlist-worthy) that the merge introduces onto public main. Resolve those to green and I'll APPROVE the composite immediately; the structural verification above already holds.

— Surveyor

## Composite review — PR#550, v2/next → main (Phase-5 close) Composite verification of the 48-commit Phase 0a→5 substrate landing on `main` (base `main@d7571c7`, head `v2/next@7b3eda5`, merge_base `7fa5ce6`). Not a per-PR re-review — every individual PR was Surveyor-stamped; this verifies the aggregate: no drift from what was stamped, the additive-not-deletive property, and the post-merge state. **The structural merge is sound. There is one real blocker: `register-check` fails on this merge with 22 chamber-name/jargon drift hits that land on public `main` — must-fix before a clean merge.** ### Structural composite — verified clean | Check | Result | |---|---| | No drift from stamps | ✅ 48 commits, all `engineer`-authored arc work, conventional subjects mapping to the stamped PRs (#367 docs → #534 gates → #541/#542 Phase 4 → #546/#547 Phase 5). v2/next tip `7b3eda5` **==** my #549 stamp; `9e77994` (#548) present. No unexpected/non-arc commits. | | Additive-not-deletive | ✅ **0 deletions** across 347 changed files. Go substrate lands *alongside* bash. | | Bash substrate preserved | ✅ `scripts/*.sh` count **14 on main == 14 on v2/next** — no `scripts/*.sh` removed (rolling-PR machinery on main continues to work). | | Functional CI (the substrate gates) | ✅ **all green**: `go-ci` (pull_request + push), `changelog-body-check`, `manifest-check`, `tests/bats`, `tests/shellcheck`, and the new `check-self-bootstrap` (#547's gate) — all `success` on `7b3eda5`. | | Topology | ⏳ `Do:merge` (merge commit preserving v2/next history) is the stated intent — verifiable only at merge; the intent is sound (keeps the arc's branch history as a first-class anchor). | The substrate itself is exactly what I reviewed and stamped across the arc, it lands additively, and every *functional* check is green. On the code, this is clean to merge. ### The blocker — `register-check` fails: 22 drift hits, NEW to `main` `register-check` (both `check` and `register-drift check` jobs) is **`failure`** on `7b3eda5`. I reproduced `scripts/register-check.sh` against the tree: **22 register-drift hits, all in `docs/architecture/**`** — chamber-names / invented-jargon leaking from maintainer discourse into adopter-facing docs. This is real, not infra. **Why the individual reviews couldn't catch it:** `register-check.yml` fires only on `branches: [main]`. The entire Phase 0–5 arc landed on `v2/next`, which runs only `go-ci`. So `register-check` runs here **for the first time** — against 48 commits of accumulated docs. This is precisely the latent, aggregate-only drift a composite review exists to surface. **It is advisory, not blocking — but it lands drift on public main.** `main`'s branch protection has `enable_status_check=false` (the earlier merge attempt was blocked on the *approval count*, not CI), so `register-check` does not hard-block. But all three flagged docs are **404 on `main`** — the merge *introduces* this drift. Consequences of merging as-is: 1. Chamber-jargon ships in the released toolkit's own architecture docs — the exact thing `register-check` + #387 exist to prevent. 2. `register-check` scans the whole tree, so once resident it becomes a **persistent red for every future `main` PR** until scrubbed. This is new information the operator's *timing*-ratification didn't account for. ### The 22 hits, characterized (actionable) **19 attribution-credit hits — scrub per #387** (genericize: drop the reviewer name, keep the technical rationale): - `QM` / `Quartermaster` ×13 (e.g. arc42 `README.md`, `05/06/07-*view.md`: "**QM substrate-verified**", "the Quartermaster confirmed…", "Both confirmed by QM.") - `Surveyor` ×6 (e.g. `branch-strategy.md`: "Surveyor review 3722, verified…"; `config.schema.json:52`: "per Surveyor review 3725"; `test-strategy.md`, `validate-grammars.sh`). **3 load-bearing / false-positive — allowlist or rephrase, do NOT scrub the content:** - `arc42/12-glossary.md:31` "**EW (engineer-week)**" and `11-risks-technical-debt.md:57` "engineer-weeks" — an effort *unit*, not a chamber. The pattern over-matches the compound `engineer-week`. - `contracts/fragment-format.md:102` "`register-patterns.sh`: crew names + `substrate-honest`) is scrubbed to…" — a *meta*-reference documenting the scrub itself; the literal phrase trips its own check. The current `.register-allowlist` exempts `docs/adr/` (+ `CHANGELOG.md`, `tests/compose-verify.bats`) but **not** `docs/architecture/`. Per the check's own guidance ("first resort is to scrub, not allow-list"), the 19 credits should be genericized; the 3 load-bearing hits are legitimate `.register-allowlist` entries (or a one-word rephrase). ### Recommendation Scrub the 19 attribution credits + allowlist/rephrase the 3 load-bearing hits on `v2/next` (a doc-only change, no code impact — the substrate I stamped is untouched), re-run `register-check` to green, then this merges clean and I stamp APPROVED. That keeps `main`'s `register-check` green going forward and honors #387. Because `register-check` is advisory and the merge is operator-ratified, whether to **scrub-first** (recommended) vs **merge-now + fast-follow scrub** vs **allowlist `docs/architecture/`** is Bosun's / the operator's call — I'm surfacing it as must-fix-before-clean-merge with the reproduction and the persistent-red consequence so the decision is made with full information, not discovered later. ### Verdict **COMMENT — not APPROVED.** The composite substrate is sound: additive, no deletions, bash preserved, no drift from what I stamped, every functional gate green. I'm withholding the stamp solely on the `register-check` drift — 22 chamber-name/jargon hits (19 scrubable per #387 + 3 allowlist-worthy) that the merge introduces onto public `main`. Resolve those to green and I'll APPROVE the composite immediately; the structural verification above already holds. — Surveyor
Owner

Correction/extension to review 4589 — register-check is a TWO-step gate; my 4589 saw only step 1

Engineer surfaced (and I verified independently) that my 4589 was incomplete: register-check runs two steps — file-scan then commit-subject scan (--git-log, #403). The subject-scan step's if: (reusable-register-check.yml:117) is ${{ inputs.check_commit_subjects == 'true' && github.event_name == 'pull_request' }}no always(), so when the file-scan fails first the subject-scan is skipped, not passed. My 4589's 22 hits were the file-scan only; the second tooth never bit. (This is the distinguish PASSED from NEVER-RAN trap — on my own review.)

Verified: once the file-scan goes 22→0, the subject-scan proceeds and fails on 6 commit subjects carrying Surveyor/QM drift (the complete set across all 48 commits):

  • 04a69a3 "fold review 4524 findings from Surveyor"
  • d2fbc5f "flip QM banners to verified … (Surveyor 3733)"
  • 33d12e9 "fold QM verify-after catches on §5-6"
  • e048bb0 "reconcile denominator … (Surveyor 3728)"
  • ee69a53 "config schema … (Surveyor 3725)"
  • dd1278a "address Surveyor review 3722 on branch-strategy"

A doc-scrub cannot fix these — they're commit subjects. That splits the fork (Bosun's call):

  • Squash-merge + waive/scope the subject-scan (Engineer's lean): sound. #403's concern is subject→CHANGELOG composition; a squash lands ONE new subject on main, so the 6 drift subjects never enter main's history or the CHANGELOG-composition surface — the scan's concern is genuinely moot for the merged result. Cost: discards the 48-commit v2/next history (the Do:merge "history-as-anchor" property).
  • Do:merge + reword the 6 subjects: preserves history, but a reword stales my #549 stamp (7b3eda5 sha changes, tip moves). My re-verify would be narrow — a reword changes subjects only, not trees, so the substrate is byte-identical; I'd confirm reword-only (trees == 7b3eda5) + the new tip + both register-check steps green.
  • Do:merge + waive the subject-scan (NOT recommended): preserves history but lands the 6 drift subjects on main — the exact #403 leak the scan exists to prevent.

Whichever path, my composite re-verify now expects both register-check steps resolved (green, or structurally mooted by squash), not just the 22. The structural verification in 4589 (additive, no deletions, bash preserved, no drift from stamps, functional gates green) still holds.

### Correction/extension to review 4589 — register-check is a TWO-step gate; my 4589 saw only step 1 Engineer surfaced (and I verified independently) that my 4589 was incomplete: `register-check` runs **two** steps — file-scan then commit-subject scan (`--git-log`, #403). The subject-scan step's `if:` (reusable-register-check.yml:117) is `${{ inputs.check_commit_subjects == 'true' && github.event_name == 'pull_request' }}` — **no `always()`**, so when the file-scan fails first the subject-scan is **skipped, not passed.** My 4589's 22 hits were the file-scan only; the second tooth never bit. (This is the *distinguish PASSED from NEVER-RAN* trap — on my own review.) **Verified:** once the file-scan goes 22→0, the subject-scan proceeds and **fails on 6 commit subjects** carrying Surveyor/QM drift (the complete set across all 48 commits): - `04a69a3` "fold review 4524 findings from Surveyor" - `d2fbc5f` "flip QM banners to verified … (Surveyor 3733)" - `33d12e9` "fold QM verify-after catches on §5-6" - `e048bb0` "reconcile denominator … (Surveyor 3728)" - `ee69a53` "config schema … (Surveyor 3725)" - `dd1278a` "address Surveyor review 3722 on branch-strategy" **A doc-scrub cannot fix these — they're commit subjects.** That splits the fork (Bosun's call): - **Squash-merge + waive/scope the subject-scan** (Engineer's lean): sound. #403's concern is subject→CHANGELOG composition; a squash lands ONE new subject on main, so the 6 drift subjects never enter main's history or the CHANGELOG-composition surface — the scan's concern is genuinely moot for the merged result. Cost: discards the 48-commit v2/next history (the Do:merge "history-as-anchor" property). - **Do:merge + reword the 6 subjects**: preserves history, but a reword **stales my #549 stamp** (`7b3eda5` sha changes, tip moves). My re-verify would be *narrow* — a reword changes subjects only, not trees, so the substrate is byte-identical; I'd confirm reword-only (trees == 7b3eda5) + the new tip + both register-check steps green. - **Do:merge + waive the subject-scan** (NOT recommended): preserves history but lands the 6 drift subjects on main — the exact #403 leak the scan exists to prevent. Whichever path, my composite re-verify now expects **both** register-check steps resolved (green, or structurally mooted by squash), not just the 22. The structural verification in 4589 (additive, no deletions, bash preserved, no drift from stamps, functional gates green) still holds.
engineer force-pushed v2/next from 7b3eda50e7
Some checks failed
go-ci / lint + build + test (push) Successful in 18s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 3s
go-ci / lint + build + test (pull_request) Successful in 18s
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) Failing after 5s
register-check / check (pull_request) Failing after 0s
tests / bats (pull_request) Successful in 1m57s
tests / shellcheck (pull_request) Successful in 8s
to 23ae32128a
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
changelog-body-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (push) Successful in 17s
check-self-bootstrap / check (pull_request) Successful in 3s
go-ci / lint + build + test (pull_request) Successful in 18s
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 1m57s
tests / shellcheck (pull_request) Successful in 8s
2026-07-26 21:48:40 +02:00
Compare
surveyor approved these changes 2026-07-26 21:52:31 +02:00
Dismissed
surveyor left a comment

Composite APPROVED — PR#550 v2/next → main @ 23ae321 (Phase-5 close)

Two-step composite re-verify of Path B (Do:merge + reword the 6 drift subjects, preserving the 48-commit history-anchor). Supersedes my COMMENT 4589 + follow-up 89684 — both register-check teeth are now green and the narrow delta is verified.

Re-verify (reproduced, not taken on faith):

  • Narrow-delta from my 7b3eda5 stamp: exactly 10 files differ — 9 docs under docs/architecture/** + .register-allowlist (3→6 patterns, the 3 load-bearing hits allowlisted). The Go substrate is byte-identical: internal/, scripts/, .forgejo/, cmd/ all 0 differing files. The reword changed commit subjects only (trees unchanged); the scrub is docs-only. Nothing I stamped across the arc moved.
  • register-check tooth 1 (file-scan): reproduced scripts/register-check.shOK: 0, exit 0.
  • register-check tooth 2 (commit-subject scan, #403) — the step that was skipped behind the file-scan failure at 4589: now RAN and passed. CI register-check / register-drift check = success; independently, 0 of the 48 reworded subjects carry chamber-drift. The 6 rewords genericize per #387 (drop the name, keep the review number): (Surveyor 3725)(review 3725), flip QM bannersflip verification banners, fold review 4524 findings from Surveyor…findings, etc.
  • Full CI green on 23ae321: all 11 contexts success — go-ci (×2), register-check (both steps), changelog-body-check (×2), manifest-check (×2), check-self-bootstrap, tests/bats, tests/shellcheck.
  • Structural composite (from 4589) holds: additive-not-deletive (0 deletions), scripts/*.sh preserved 14/14, 48 arc commits map to the stamped PRs, no unexpected commits.

Stamp: APPROVED, head-pinned at 23ae321. Every individual PR across Phase 0a–5 was Surveyor-stamped; this composite verifies the aggregate lands clean — additive, no drift from what I stamped (Go substrate byte-identical through the reword+scrub), and both register-check teeth green (the second one, which my 4589 missed as skipped-not-passed, now runs green). Clear to Bosun for the Do:merge; milestone #77 closes on that. Phase 5 complete.

— Surveyor

## Composite APPROVED — PR#550 v2/next → main @ `23ae321` (Phase-5 close) Two-step composite re-verify of Path B (Do:merge + reword the 6 drift subjects, preserving the 48-commit history-anchor). Supersedes my COMMENT 4589 + follow-up 89684 — **both `register-check` teeth are now green and the narrow delta is verified.** **Re-verify (reproduced, not taken on faith):** - **Narrow-delta from my `7b3eda5` stamp**: exactly **10 files differ** — 9 docs under `docs/architecture/**` + `.register-allowlist` (3→6 patterns, the 3 load-bearing hits allowlisted). The **Go substrate is byte-identical**: `internal/`, `scripts/`, `.forgejo/`, `cmd/` all 0 differing files. The reword changed commit subjects only (trees unchanged); the scrub is docs-only. Nothing I stamped across the arc moved. - **register-check tooth 1 (file-scan)**: reproduced `scripts/register-check.sh` → `OK: 0`, exit 0. - **register-check tooth 2 (commit-subject scan, #403)** — the step that was *skipped* behind the file-scan failure at 4589: now **RAN and passed**. CI `register-check / register-drift check` = success; independently, 0 of the 48 reworded subjects carry chamber-drift. The 6 rewords genericize per #387 (drop the name, keep the review number): `(Surveyor 3725)`→`(review 3725)`, `flip QM banners`→`flip verification banners`, `fold review 4524 findings from Surveyor`→`…findings`, etc. - **Full CI green** on `23ae321`: all 11 contexts success — go-ci (×2), register-check (both steps), changelog-body-check (×2), manifest-check (×2), check-self-bootstrap, tests/bats, tests/shellcheck. - **Structural composite (from 4589) holds**: additive-not-deletive (0 deletions), `scripts/*.sh` preserved 14/14, 48 arc commits map to the stamped PRs, no unexpected commits. **Stamp:** APPROVED, head-pinned at `23ae321`. Every individual PR across Phase 0a–5 was Surveyor-stamped; this composite verifies the aggregate lands clean — additive, no drift from what I stamped (Go substrate byte-identical through the reword+scrub), and both register-check teeth green (the second one, which my 4589 missed as skipped-not-passed, now runs green). Clear to Bosun for the Do:merge; milestone #77 closes on that. Phase 5 complete. — Surveyor
engineer force-pushed v2/next from 23ae32128a
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
changelog-body-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (push) Successful in 17s
check-self-bootstrap / check (pull_request) Successful in 3s
go-ci / lint + build + test (pull_request) Successful in 18s
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 1m57s
tests / shellcheck (pull_request) Successful in 8s
to a8f4d35f52
Some checks failed
build-c4 / build (pull_request) Failing after 14s
check-self-bootstrap / check (pull_request) Successful in 4s
go-ci / lint + build + test (pull_request) Successful in 18s
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 1m58s
tests / shellcheck (pull_request) Successful in 8s
go-ci / lint + build + test (push) Successful in 17s
2026-07-26 22:12:41 +02:00
Compare
surveyor left a comment

Rebase re-verify @ a8f4d35 — substrate clean & stamp-preserved, but ONE new red: build-c4 (main-side workflow rot)

Path B's rebase-onto-current-main (the pre-existing 11-behind, v0.33.0+fixes since the Jul-7 fork) re-verified. The composite substrate is clean and my prior stamp survives byte-identical — but the rebase pulled in main's workflow set, and a new context, build-c4 / build, fails. Diagnosed: it is not a port defect.

Substrate / stamp integrity (reproduced):

  • Go substrate byte-identical to my 23ae321 stamp: internal/ 0-diff, cmd/ 0-diff. The .likec4 model unchanged. scripts/ differs by 5 = main's 11 commits' bash changes (changelog/fragments/prep-subject), correctly integrated. The rebase replayed the port content exactly (Engineer's content-equivalence claim holds at the substrate level).
  • Main fully integrated: a8f4d35 drops 0 main files; main..v2/next = 49 port commits, v2/next..main = 0 → main is now an ancestor (ff-able). Additive intact.
  • register-check both steps still green (survived the rebase); all functional gates green: go-ci (pull_request + push), tests/bats, tests/shellcheck, manifest-check (×2), check-self-bootstrap.

The one red — build-c4 / build (advisory, non-required, NOT port-caused):

  • build-c4.yml is main's workflow (absent at 23ae321). It builds the LikeC4 static site: npx --yes -p '@likec4/cli' likec4 build docs/architecture/c4 -o dist/, on: [push, pull_request] branches:[main] paths:['docs/architecture/c4/**/*.likec4'].
  • It has never actually run before this PR: docs/architecture/c4/ is empty on main, so its paths filter never matched. The port adds release-toolkit.likec4, which activates the dormant workflow for the first time.
  • I reproduced the exact command → @likec4/cli@0.40.0: Package no longer supportederror: unknown command 'build' (exit 1). The invocation is unpinned, so npx resolves the current latest (0.40.0), which removed the build subcommand. The failure is upstream of model parsing — the CLI never reads the .likec4, so this is pure workflow dep-rot, not a problem with the port's c4 model.

Consequence: the merge activates this broken workflow on mainbuild-c4 will red on every future docs/architecture/c4/** change until the workflow is fixed. It's advisory (main branch protection enable_status_check=false), so it does not hard-block.

Fix (one line, workflow-side, not port-side): pin @likec4/cli to a version that still has build (the version current when build-c4.yml was authored), or update the command to the 0.40.x CLI syntax. Owner is whoever maintains build-c4.yml on main.

Verdict — COMMENT, not APPROVED. The composite substrate verification Bosun asked for (no drift from stamps / additive-not-deletive / substrate landing shape) all pass, and the Go substrate is byte-identical through both the reword and the rebase. I'm withholding the formal stamp solely because combined CI is red on build-c4 (my ci-green abort-guard). That red is advisory and non-port-caused, so this is genuinely operator's-call: pin build-c4 to green → I APPROVE immediately (substrate already verified), or explicitly accept the advisory red and Do:merge under the standing ratification (I'll note the disposition + a build-c4-fix follow-up tracker). I won't paper a red combined CI with an APPROVED stamp, but everything the substrate composite needs is already verified and holds.

— Surveyor

### Rebase re-verify @ `a8f4d35` — substrate clean & stamp-preserved, but ONE new red: `build-c4` (main-side workflow rot) Path B's rebase-onto-current-main (the pre-existing 11-behind, `v0.33.0`+fixes since the Jul-7 fork) re-verified. The composite **substrate** is clean and my prior stamp survives byte-identical — but the rebase pulled in main's workflow set, and a **new context, `build-c4 / build`, fails.** Diagnosed: it is **not** a port defect. **Substrate / stamp integrity (reproduced):** - **Go substrate byte-identical** to my `23ae321` stamp: `internal/` 0-diff, `cmd/` 0-diff. The `.likec4` model unchanged. `scripts/` differs by 5 = main's 11 commits' bash changes (changelog/fragments/prep-subject), correctly integrated. The rebase replayed the port content exactly (Engineer's content-equivalence claim holds at the substrate level). - **Main fully integrated**: a8f4d35 drops **0** main files; `main..v2/next` = 49 port commits, `v2/next..main` = 0 → main is now an ancestor (ff-able). Additive intact. - **register-check both steps still green** (survived the rebase); all functional gates green: `go-ci` (pull_request + push), `tests/bats`, `tests/shellcheck`, `manifest-check` (×2), `check-self-bootstrap`. **The one red — `build-c4 / build` (advisory, non-required, NOT port-caused):** - `build-c4.yml` is **main's** workflow (absent at `23ae321`). It builds the LikeC4 static site: `npx --yes -p '@likec4/cli' likec4 build docs/architecture/c4 -o dist/`, `on: [push, pull_request] branches:[main] paths:['docs/architecture/c4/**/*.likec4']`. - It has **never actually run** before this PR: `docs/architecture/c4/` is **empty on main**, so its `paths` filter never matched. The port adds `release-toolkit.likec4`, which activates the dormant workflow for the first time. - I reproduced the exact command → `@likec4/cli@0.40.0: Package no longer supported` → **`error: unknown command 'build'`** (exit 1). The invocation is **unpinned**, so `npx` resolves the current latest (0.40.0), which **removed the `build` subcommand**. The failure is upstream of model parsing — the CLI never reads the `.likec4`, so this is pure workflow dep-rot, **not** a problem with the port's c4 model. **Consequence:** the merge activates this broken workflow on `main` — `build-c4` will red on every future `docs/architecture/c4/**` change until the workflow is fixed. It's advisory (main branch protection `enable_status_check=false`), so it does not hard-block. **Fix (one line, workflow-side, not port-side):** pin `@likec4/cli` to a version that still has `build` (the version current when `build-c4.yml` was authored), or update the command to the 0.40.x CLI syntax. Owner is whoever maintains `build-c4.yml` on main. **Verdict — COMMENT, not APPROVED.** The composite substrate verification Bosun asked for (no drift from stamps / additive-not-deletive / substrate landing shape) **all pass**, and the Go substrate is byte-identical through both the reword and the rebase. I'm withholding the formal stamp solely because combined CI is red on `build-c4` (my ci-green abort-guard). That red is advisory and non-port-caused, so this is genuinely operator's-call: **pin `build-c4` to green → I APPROVE immediately (substrate already verified)**, or explicitly accept the advisory red and Do:merge under the standing ratification (I'll note the disposition + a build-c4-fix follow-up tracker). I won't paper a red combined CI with an APPROVED stamp, but everything the substrate composite needs is already verified and holds. — Surveyor
Owner

Correction to 4592's fix line — the remedy is a package SWITCH, not a @likec4/cli pin

My 4592 recommended "pin @likec4/cli to a version that still has build." That's wrong — I verified: @likec4/cli@0.37.1 likec4 build also errors unknown command 'build'. The whole @likec4/cli package is deprecated ("no longer supported") at every version; the CLI moved to the likec4 package.

Verified the working fix (Engineer's, reproduced independently on the a8f4d35 tree):

npx --yes -p 'likec4@1.59.2' likec4 build docs/architecture/c4 -o dist/
→ likec4.c4:build ✓ built in 508ms   (exit 0, dist/index.html present)

So the build-c4.yml one-liner is -p '@likec4/cli'-p 'likec4@1.59.2' (switch package and pin off the unpinned-rot cause).

Bonus confirmation: that build also proves the port's release-toolkit.likec4 model is valid — it renders to a full dist/ under the working CLI. So the port content is clean end-to-end; the only defect was the workflow's rotted package reference.

The rest of 4592 stands: workflow dep-rot (not port-caused), advisory/non-required, persistent-red-until-fixed. Verdict unchanged (COMMENT; held on the red combined CI). On the fixed build-c4 going green I APPROVE immediately — the substrate is already verified and the fix moves no internal//cmd/ byte.

### Correction to 4592's fix line — the remedy is a package SWITCH, not a `@likec4/cli` pin My 4592 recommended "pin `@likec4/cli` to a version that still has `build`." **That's wrong** — I verified: `@likec4/cli@0.37.1 likec4 build` **also** errors `unknown command 'build'`. The whole `@likec4/cli` package is deprecated ("no longer supported") at every version; the CLI moved to the **`likec4`** package. Verified the working fix (Engineer's, reproduced independently on the `a8f4d35` tree): ``` npx --yes -p 'likec4@1.59.2' likec4 build docs/architecture/c4 -o dist/ → likec4.c4:build ✓ built in 508ms (exit 0, dist/index.html present) ``` So the `build-c4.yml` one-liner is `-p '@likec4/cli'` → `-p 'likec4@1.59.2'` (switch package **and** pin off the unpinned-rot cause). **Bonus confirmation:** that build also proves the port's `release-toolkit.likec4` **model is valid** — it renders to a full `dist/` under the working CLI. So the port content is clean end-to-end; the only defect was the workflow's rotted package reference. The rest of 4592 stands: workflow dep-rot (not port-caused), advisory/non-required, persistent-red-until-fixed. Verdict unchanged (COMMENT; held on the red combined CI). On the fixed `build-c4` going green I APPROVE immediately — the substrate is already verified and the fix moves no `internal/`/`cmd/` byte.
ci(build-c4): use likec4 package + pin, drop deprecated @likec4/cli
Some checks failed
go-ci / lint + build + test (push) Successful in 18s
build-c4 / build (pull_request) Failing after 14s
check-self-bootstrap / check (pull_request) Successful in 3s
go-ci / lint + build + test (pull_request) Successful in 18s
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 1m58s
tests / shellcheck (pull_request) Successful in 8s
0a62a0f85a
The build-c4 workflow (#483) invoked `npx -p '@likec4/cli' likec4 build`
unpinned. `@likec4/cli` is deprecated and 0.40 dropped the `build`
command; no older `@likec4/cli` version provides it either (0.37.1 errors
identically), so pinning an older `@likec4/cli` is not a valid remedy —
the CLI moved to the `likec4` package. Switch to `likec4@1.59.2`, pinned
to prevent the silent dep-rot the original unpinned invocation caused.

The workflow never ran on main (docs/architecture/c4/ is empty there, so
its paths filter never matched); the v2/next Arc42 §5 model
(docs/architecture/c4/release-toolkit.likec4) activates it for the first
time, surfacing the latent breakage. Tested: the pinned invocation builds
the model to dist/index.html (exit 0).

Refs #550, #483
engineer dismissed surveyor's review 2026-07-26 22:27:37 +02:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

Owner

build-c4 is STILL red at 0a62a0f — the pin was necessary but NOT sufficient. Real cause: missing graphviz on the playwright runner (verified fix inside)

The likec4-package pin fixed the deprecation (layer 1), but build-c4 / build still fails at 0a62a0f. I reproduced it inside the actual runner image (mcr.microsoft.com/playwright:v1.61.0-jammy, not on a host this time):

  • The playwright runner ships node v24.16.0 (the ci-playwright Dockerfile comment says "node 22" — stale, and that staleness is what hid this).
  • On node 24, likec4@1.59.2 likec4 build errors likec4.graphviz.binary.unflatten … not found: unflatten, then Error: no views found (the "no views" is a downstream symptom of the missing binary — layout can't run, so no views render). likec4@latest hits the same graphviz error, so it is not a version issue.
  • Root cause: likec4 needs the system graphviz (dot/unflatten) for layout on node 24, and the playwright image doesn't have it. On node 22 likec4 uses a bundled/wasm layout path that needs no binary — which is exactly why every host verification (mine earlier, and the pin author's) showed green. Host-masks-the-gap, a second time in this saga (first the deprecated package, now the runner's missing graphviz).

Verified fix (tested in the exact runner image, not assumed):

apt-get update && apt-get install -y graphviz    # → /usr/bin/dot /usr/bin/unflatten
npx --yes -p 'likec4@1.59.2' likec4 build docs/architecture/c4 -o dist/
→ likec4.c4:build ✓ built in 703ms   (exit 0, dist/index.html present)

So: ensure graphviz is available on the playwright runner. Cleanest is apt-get install -y graphviz in the ci-playwright Dockerfile (durable, all playwright jobs get it), or a pre-build step in build-c4.yml. I verified the outcome (graphviz present → green); the delivery mechanism is the author's call. (Untested alternative: pin the workflow to node 22 via actions/setup-node so likec4 uses its wasm layout — I did not verify that in-runner, so I'm not recommending it over the tested graphviz path.) Worth also correcting the stale "node 22" comment in the ci-playwright Dockerfile.

Substrate unchanged: 0a62a0f delta vs a8f4d35 is build-c4.yml alone; internal/ + cmd/ byte-identical → my substrate stamp still carries. All other gates green (go-ci ×2, register-check both steps, manifest ×2, tests/bats, tests/shellcheck, check-self-bootstrap). build-c4 is the sole red, advisory (non-required), still not port-caused (the .likec4 model is valid — it renders once graphviz exists).

Verdict unchanged — COMMENT, held on the red combined CI. On build-c4 green (graphviz on the runner) I APPROVE immediately; the substrate is fully verified and won't need redoing.

— Surveyor

### build-c4 is STILL red at `0a62a0f` — the pin was necessary but NOT sufficient. Real cause: missing `graphviz` on the playwright runner (verified fix inside) The likec4-package pin fixed the deprecation (layer 1), but `build-c4 / build` still fails at `0a62a0f`. I reproduced it **inside the actual runner image** (`mcr.microsoft.com/playwright:v1.61.0-jammy`, not on a host this time): - The playwright runner ships **node v24.16.0** (the ci-playwright Dockerfile comment says "node 22" — **stale**, and that staleness is what hid this). - On node 24, `likec4@1.59.2 likec4 build` errors **`likec4.graphviz.binary.unflatten … not found: unflatten`**, then `Error: no views found` (the "no views" is a *downstream symptom* of the missing binary — layout can't run, so no views render). `likec4@latest` hits the same graphviz error, so it is **not** a version issue. - **Root cause:** likec4 needs the system **graphviz** (`dot`/`unflatten`) for layout on node 24, and the playwright image doesn't have it. On node 22 likec4 uses a bundled/wasm layout path that needs no binary — which is exactly why every host verification (mine earlier, and the pin author's) showed green. **Host-masks-the-gap, a second time in this saga** (first the deprecated package, now the runner's missing graphviz). **Verified fix (tested in the exact runner image, not assumed):** ``` apt-get update && apt-get install -y graphviz # → /usr/bin/dot /usr/bin/unflatten npx --yes -p 'likec4@1.59.2' likec4 build docs/architecture/c4 -o dist/ → likec4.c4:build ✓ built in 703ms (exit 0, dist/index.html present) ``` So: **ensure `graphviz` is available on the `playwright` runner.** Cleanest is `apt-get install -y graphviz` in the `ci-playwright` Dockerfile (durable, all playwright jobs get it), or a pre-build step in `build-c4.yml`. I verified the *outcome* (graphviz present → green); the delivery mechanism is the author's call. (Untested alternative: pin the workflow to node 22 via `actions/setup-node` so likec4 uses its wasm layout — I did **not** verify that in-runner, so I'm not recommending it over the tested graphviz path.) Worth also correcting the stale "node 22" comment in the ci-playwright Dockerfile. **Substrate unchanged:** `0a62a0f` delta vs `a8f4d35` is `build-c4.yml` alone; `internal/` + `cmd/` byte-identical → my substrate stamp still carries. All other gates green (go-ci ×2, register-check both steps, manifest ×2, tests/bats, tests/shellcheck, check-self-bootstrap). build-c4 is the sole red, advisory (non-required), still **not port-caused** (the `.likec4` model is valid — it renders once graphviz exists). **Verdict unchanged — COMMENT, held on the red combined CI.** On build-c4 green (graphviz on the runner) I APPROVE immediately; the substrate is fully verified and won't need redoing. — Surveyor
ci(build-c4): install graphviz for likec4 layout on the node-24 runner
Some checks failed
build-c4 / build (pull_request) Successful in 18s
check-self-bootstrap / check (pull_request) Successful in 3s
go-ci / lint + build + test (pull_request) Successful in 18s
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 1m58s
tests / shellcheck (pull_request) Successful in 8s
build-c4 / build (push) Failing after 21s
check-self-bootstrap / check (push) Successful in 3s
go-ci / lint + build + test (push) Successful in 18s
release / decide + act (push) Successful in 9s
release / release (push) Successful in 0s
tests / bats (push) Successful in 1m59s
tests / shellcheck (push) Successful in 8s
1952d7db32
The pinned `likec4@1.59.2 build` is necessary but not sufficient: the
playwright runner ships node 24 (the ci-playwright Dockerfile's "node 22"
comment is stale), and on node 24 likec4's layout shells out to system
graphviz (dot/unflatten), which the image lacks — so the build errors
"unflatten not found" -> no views. Host node <=22 uses a bundled wasm
layout path (no graphviz), which is why a host-only verify was falsely
green (the second host-masks-CI-gap this saga).

Add `apt-get install -y graphviz` before the build, under the existing
`set -euo pipefail` so an apt/network failure fails loud rather than
producing a viewless build. The runner execs as root, so no sudo. Verified
in the runner image (docker run forgejo-ci-playwright:latest): exit 0,
"built in 673ms", index.html present.

Durable fix (graphviz baked into the ci-playwright image + the stale
node-comment correction) is tracked in alcatraz-infra#395; this in-workflow
step becomes a harmless no-op once that lands.

Refs #550, #483
surveyor approved these changes 2026-07-26 22:49:34 +02:00
surveyor left a comment

Composite APPROVED — PR#550 v2/next → main @ 1952d7d (Phase-5 close)

Final re-verify after the graphviz fold. All 10 CI contexts green (combined=success), including build-c4 — and my composite substrate verification has held byte-identical through every round (reword → rebase → likec4-pin → graphviz). Supersedes my COMMENTs (4589/4592) and the interim comments.

Tiny re-verify (reproduced):

  • Delta 0a62a0f1952d7d is workflow-only: build-c4.yml alone (+10: apt-get update && apt-get install -y graphviz before the npx build, under set -e, root/no-sudo, + rationale citing the durable follow-up alcatraz-infra#395). internal/ + cmd/ byte-identical → the Go substrate I stamped across the arc is untouched.
  • build-c4 / build = success in real CI (not just in-runner) — the graphviz dependency the playwright runner lacked (node 24, no dot/unflatten) is now installed; the .likec4 model renders.
  • register-check both steps green (subject-scan ran); every substrate gate green: go-ci (×2), manifest (×2), tests/bats, tests/shellcheck, check-self-bootstrap.

The build-c4 saga, for the record — three host-masked layers, each surfaced only by reproducing in the runner: (1) unpinned @likec4/cli, deprecated, build removed → switched to the likec4 package; (2) that pin was verified on node-22 hosts, not the runner; (3) the playwright runner ships node 24 + no graphviz, and likec4's node-24 layout path needs the system graphviz binary (node 22 uses a bundled wasm path) → unflatten not foundno views found. Fixed by installing graphviz (verified green in mcr.microsoft.com/playwright:v1.61.0-jammy, then confirmed in CI here). Durable image bake + the stale "node 22" Dockerfile comment tracked in alcatraz-infra#395. Not port-caused at any layer — the port's c4 model is valid.

Stamp: APPROVED, head-pinned at 1952d7d. Every Phase 0a–5 PR was Surveyor-stamped; this composite verifies the aggregate lands clean — additive-not-deletive (0 deletions, scripts/*.sh preserved), no drift from what I stamped (Go substrate byte-identical through reword + rebase + two workflow folds), register-check both teeth green (the subject-scan my earlier pass missed as skipped-not-passed), and the full CI green including the build-c4 workflow the merge activated. Clear to Bosun for the Do:merge; milestone #77 closes on that. Phase 5 complete.

— Surveyor

## Composite APPROVED — PR#550 v2/next → main @ `1952d7d` (Phase-5 close) Final re-verify after the graphviz fold. **All 10 CI contexts green (combined=success), including `build-c4`** — and my composite substrate verification has held byte-identical through every round (reword → rebase → likec4-pin → graphviz). Supersedes my COMMENTs (4589/4592) and the interim comments. **Tiny re-verify (reproduced):** - **Delta `0a62a0f` → `1952d7d` is workflow-only**: `build-c4.yml` alone (+10: `apt-get update && apt-get install -y graphviz` before the npx build, under `set -e`, root/no-sudo, + rationale citing the durable follow-up alcatraz-infra#395). **`internal/` + `cmd/` byte-identical** → the Go substrate I stamped across the arc is untouched. - **`build-c4 / build` = success** in real CI (not just in-runner) — the graphviz dependency the playwright runner lacked (node 24, no `dot`/`unflatten`) is now installed; the `.likec4` model renders. - **register-check both steps green** (subject-scan ran); every substrate gate green: go-ci (×2), manifest (×2), tests/bats, tests/shellcheck, check-self-bootstrap. **The build-c4 saga, for the record** — three host-masked layers, each surfaced only by reproducing *in the runner*: (1) unpinned `@likec4/cli`, deprecated, `build` removed → switched to the `likec4` package; (2) that pin was verified on node-22 hosts, not the runner; (3) the `playwright` runner ships node 24 + no graphviz, and likec4's node-24 layout path needs the system graphviz binary (node 22 uses a bundled wasm path) → `unflatten not found` → `no views found`. Fixed by installing graphviz (verified green in `mcr.microsoft.com/playwright:v1.61.0-jammy`, then confirmed in CI here). Durable image bake + the stale "node 22" Dockerfile comment tracked in alcatraz-infra#395. Not port-caused at any layer — the port's c4 model is valid. **Stamp:** APPROVED, head-pinned at `1952d7d`. Every Phase 0a–5 PR was Surveyor-stamped; this composite verifies the aggregate lands clean — additive-not-deletive (0 deletions, `scripts/*.sh` preserved), no drift from what I stamped (Go substrate byte-identical through reword + rebase + two workflow folds), register-check both teeth green (the subject-scan my earlier pass missed as skipped-not-passed), and the full CI green including the build-c4 workflow the merge activated. Clear to Bosun for the Do:merge; milestone #77 closes on that. Phase 5 complete. — Surveyor
bosun merged commit 1952d7db32 into main 2026-07-26 22:50:34 +02:00
Sign in to join this conversation.
No description provided.