feat(changelog): implement Composer + Parser + register-scrub against C6 grammar + bash oracle #537

Merged
bosun merged 1 commit from i/533-changelog-composer into v2/next 2026-07-26 01:22:27 +02:00
Owner

What

Implements internal/changelog — the Phase 3 changelog engine — against the Phase 0b (#505) Composer/Parser interface, the C6 CHANGELOG grammar (#504), and the scripts/lib/changelog.sh byte-oracle. Second of Phase 3's three trackers (consumes #532's Fragment; #534 gates consume this engine). Also adds internal/register (the shared register-scrub primitive).

  • Composer.Compose(version, date, frags) folds fragments → VersionSection (categorize_fragments + changelog_scaffold_missing_sections).
  • Composer.Transition(changelog, version, date) performs the Unreleased→released move (changelog_transition, no fragments).
  • Parser.Parse / LatestVersion / UnreleasedContent read the changelog, fail-loud on a C6-malformed heading.
  • RenderCommitSections, MergeSections, ScaffoldMissingSections, NormalizeParagraphs — additive composer functions beside the interface.
  • register.ScrubLine + register.Patterns — the register_scrub_line port (#442).

The register-scrub obligation (#442) — where it lands, and why

The load-bearing cross-phase pin (my #507 comment 89128, carried through Phase 2): the composer MUST apply register_scrub_line or a chamber-name rides into the CHANGELOG that the bash path scrubbed. Tracing the substrate resolved where:

  • register_scrub_line is applied by bash at CC-bullet emission (conventional-commits.sh:260, inside cc_categorize_commits_since), not to fragment bodies. Fragment content is gated by register-check.sh --stdin (fail-loud) at release-prep.sh:374 instead — a check, not a scrub.
  • #524's own package doc (conventionalcommits.go:20-24) explicitly deferred "bullet formatting and the register-name scrub (#442)" to "the changelog composer (Phase 3, #507)". CategorizeRange returns the section grouping as data; the rendering + scrub land here.

So RenderCommitSections (this PR) is the faithful call site: it renders each CategorizedRange bullet — - **<scope>**: <desc> / - <desc> — and passes every bullet through register.ScrubLine before emission. AC3's "verified against the #442 bats oracle" is met by the scrub harness case diffing register.ScrubLine against register_scrub_line byte-for-byte.

Design decisions (decision-tree, not conclusion)

1. CC-render + scrub as additive functions beside the frozen interface. The #505 Composer is fragment-only (Compose(frags) + Transition). CC-bullet rendering has no interface method, so RenderCommitSections lands package-level — the established CategorizeRange/AggregateBump shape (frozen interface intact, function added). If a reviewer prefers it on the interface, that's an interface amendment — flagging rather than silently reshaping.

2. internal/register as its own package. Bash extracted register-patterns.sh as a shared lib precisely so the compose-time scrub (#442) and the PR-time gate (register-check) share one pattern list without duplication. Mirroring that boundary, the vocabulary + ScrubLine live in internal/register; #534's register-check (slated for internal/release) reuses it. If it were buried in internal/changelog, #534 would duplicate the crew list — the exact drift register-patterns.sh's header exists to prevent.

3. AC2 normalizer — three exist; I port the composer's. The bash substrate has three paragraph normalizers with different rules: (1) fragments.sh::_normalize_paragraph_continuations (compose path, per fragment body — what categorize_fragments invokes); (2) changelog.sh::changelog_normalize_paragraphs (#420 — hyphen-word rejoin + list-item continuation + fail-loud residue guard, run on the merged section in release-prep); (3) the draft-release.sh inline awk (#54 — the QM-audit pointer the tracker names, run at draft-render time). This PR ports (1), the one Compose applies, and harness-verifies it byte-for-byte. (2) and (3) are pipeline/render-stage transforms outside the frozen Composer surface — deferred to the orchestrator (Phase 6), disclosed in the package doc. If AC2 intends #54 specifically, it belongs with the draft-release port, not the composer — happy to adjust.

4. Transition uses the bash default heading (## [v<version>] - <date>). The frozen signature dropped changelog_transition's tag_prefix/separator/fragments params, so Transition uses the Keep-a-Changelog defaults (v-prefix, -) that tests/changelog.bats pins. A per-consumer shape (tmux-tell's bare-core + em-dash) is config-driven — bash reads config_get_section_{tag_prefix,separator} at release-prep.sh:382 — and wires in when the orchestrator carries config into the transition (Phase 6).

5. Parse / UnreleasedContent fail-loud is Go-side, stricter than bash. ErrMalformedHeading (any ## [ heading violating C6 §1) and ErrNoUnreleasedSection (absent, distinct from present-but-empty) are contract-mandated by the interface — the bash helpers grep leniently and cannot make these distinctions. Same posture as #532's ErrUnknownKind, so they are unit-tested, not harness-graded (no bash equivalent to diff against). The content paths (LatestVersion core, UnreleasedContent body) byte-match the bash helpers and ARE harnessed.

6. ScaffoldMissingSections non-standard order is deterministic. The bash awk emits non-standard sections (Security/Internal) in hash order (for (kind in seen)) — implementation-defined, non-deterministic for 2+. This port emits them in FragmentFoldOrder. The two agree for the realistic ≤1-non-standard case (the fragment-check gate ensures at most one Internal in practice); a byte-comparison against a non-deterministic oracle is not well-defined for 2+, so every compose/scaffold harness fixture folds AT MOST ONE non-standard section (Security XOR Internal) — a 2+-non-standard fixture would grade this deterministic order against the CI awk's coin-flip and could false-red on a different awk. (Surveyor review 4565 SC1: the original compose/full-all-kinds fixture violated this by folding both — restructured into full (standard-kinds + Security) + a separate internal fixture, so the comparison is deterministic real parity, not disclose-the-landmine.)

7. register.go scan-safety. It carries the crew-name pattern list as Go literals (like register-patterns.sh). internal/ is not in register-check's DEFAULT_PATHS (scripts .forgejo tests docs changelog.d README CHANGELOG AGENTS), and the reusable CI workflow invokes register-check.sh with no path args, so the gate does not scan it. If internal/ is ever added to the scan, register.go should get the same self-exclusion case scripts/lib/register-patterns.sh already has.

Verification (closed loop)

Reused the equivalence-harness vehicle (Go oracleshim binary + bash changelog-oracle.sh sourcing the three real libs — changelog.sh + fragments.sh + register-patterns.sh — via RT_*_LIB, prebuilt-binary TestMain). 30 cases byte-for-byte green, covering every composer surface:

  • scrub (#442, load-bearing) — scoped/bare chamber-name, QM alias, invented-jargon, case-insensitive, clean-passthrough, engineeredEngineer boundary
  • normalize — wrapped continuation / blank separators / fenced-code verbatim
  • merge — two-file by-kind / single-file
  • scaffold — partial-backfill-None / non-standard Internal / empty→all-None
  • compose (categorize | scaffold) — standard-kinds+Security / internal-section / subset-scaffolds-rest / empty / frontmatter-stripped / wrapped-normalized / multi-fragment-join (each ≤1 non-standard, design note 6)
  • latest-version — first-cut-skips-Unreleased / em-dash bare-core / no-version exit-1
  • unreleased-content — simple / em-dash / empty-section
  • transition — moves-content / empty-Unreleased

Harness teeth mutation-verified on three load-bearing axes (each reddening only its targeted case, reverted by re-edit byte-identical):

  • register scrub: drop substrate-honest from Patternsscrub/invented-jargon RED, all other scrub cases green.
  • paragraph normalizer: continuation-join → `` → normalize/wrapped and compose/wrapped-normalized RED (the normalizer is live in both the direct and compose paths), others green.
  • transition heading: ## [v## [ → both transition cases RED.

⚠️ Mutation-verify of this harness requires go test -count=1. The oracleshim is a runtime-built binary (rebuilt in TestMain via exec go build), invisible to go test's dependency tracking — so mutating a package source file does NOT invalidate the test-package cache, and a cached GREEN masks the mutation. -count=1 (already in the gate) is load-bearing here.

Plus unit tests for the Go-only / contract surfaces: Compose section-scaffold + None. + non-standard-after-standard + empty; Transition no-Unreleased sentinel; Parse versions/sections + malformed fail-loud (dateless cut, missing separator); LatestVersion; UnreleasedContent present-empty vs absent; RenderCommitSections shape + section-order + empty-skip + scrub-in-render integration; register.ScrubLine (boundary + case-insensitive + multi-hit).

Gate

golangci-lint run --timeout=5m (cache clean first, #392) → 0 issues · go build ./... · go vet ./... · go test -count=1 ./... green · gofmt -l clean · shellcheck clean on the oracle dispatcher.

AC status

  • internal/changelog implementation compiles + all #505 interface methods satisfied (var _ Composer / var _ Parser assertions)
  • awk paragraph-normalizer ported byte-faithfully (harness case verifies byte-identity against bash — _normalize_paragraph_continuations, the compose-path normalizer; see design note 3)
  • Register-scrub applied in the composer's CC-bullet renderer (verified against the #442 register oracle via the scrub harness case + the render-scrub unit test)
  • C6 grammar validation wired at Parse/LatestVersion time (fail-loud ErrMalformedHeading)
  • Unit tests green (go test ./internal/changelog/... ./internal/register/...)
  • Equivalence-harness cases against bash oracle green (unreleased→released transition, section merging, empty sections, register-name presence-then-scrubbed all covered)
  • go vet ./... + golangci-lint run clean

What this does NOT do

  • Does not resolve git rangesRenderCommitSections takes an already-categorized CategorizedRange; the git-walking source (cc_list_commits_since) is a Phase-6 git-adapter concern, so the render is byte-verified via the scrub-primitive oracle + formatting unit tests rather than a git-fixture reconstruction of cc_categorize_commits_since.
  • Does not dedup CC-vs-fragment refschangelog_dedup_cc_by_fragment_refs is release-prep orchestration between categorize and merge (Phase 6), not composer engine.
  • Does not port the #420 / #54 normalizers — the merged-output and draft-render normalizers are pipeline/render-stage transforms outside the frozen Composer surface (design note 3).
  • Does not wire the register-CHECK gateinternal/register ships the scrub + shared vocabulary; the fail-loud register-check gate is internal/release's concern (#534).
  • Does not honour REGISTER_CHECK_PATTERNS — the adopter pattern-override (#435) is a config-injection concern; the compose-time scrub uses the built-in list, matching bash's default path.

Refs #533 · reviewer @surveyor · merge @bosun (no self-merge)

## What Implements `internal/changelog` — the Phase 3 changelog engine — against the Phase 0b (#505) `Composer`/`Parser` interface, the C6 CHANGELOG grammar (#504), and the `scripts/lib/changelog.sh` byte-oracle. Second of Phase 3's three trackers (consumes #532's `Fragment`; #534 gates consume this engine). Also adds `internal/register` (the shared register-scrub primitive). - `Composer.Compose(version, date, frags)` folds fragments → `VersionSection` (`categorize_fragments` + `changelog_scaffold_missing_sections`). - `Composer.Transition(changelog, version, date)` performs the Unreleased→released move (`changelog_transition`, no fragments). - `Parser.Parse` / `LatestVersion` / `UnreleasedContent` read the changelog, fail-loud on a C6-malformed heading. - `RenderCommitSections`, `MergeSections`, `ScaffoldMissingSections`, `NormalizeParagraphs` — additive composer functions beside the interface. - `register.ScrubLine` + `register.Patterns` — the `register_scrub_line` port (#442). ## The register-scrub obligation (#442) — where it lands, and why The load-bearing cross-phase pin (my #507 comment 89128, carried through Phase 2): **the composer MUST apply `register_scrub_line` or a chamber-name rides into the CHANGELOG that the bash path scrubbed.** Tracing the substrate resolved *where*: - `register_scrub_line` is applied by bash at **CC-bullet emission** (`conventional-commits.sh:260`, inside `cc_categorize_commits_since`), **not** to fragment bodies. Fragment content is gated by `register-check.sh --stdin` (fail-loud) at `release-prep.sh:374` instead — a *check*, not a scrub. - #524's own package doc (`conventionalcommits.go:20-24`) explicitly deferred "bullet formatting **and the register-name scrub (#442)**" to "**the changelog composer (Phase 3, #507)**". `CategorizeRange` returns the section grouping as *data*; the rendering + scrub land here. So `RenderCommitSections` (this PR) is the faithful call site: it renders each `CategorizedRange` bullet — `- **<scope>**: <desc>` / `- <desc>` — and passes **every** bullet through `register.ScrubLine` before emission. AC3's "verified against the #442 bats oracle" is met by the `scrub` harness case diffing `register.ScrubLine` against `register_scrub_line` byte-for-byte. ## Design decisions (decision-tree, not conclusion) **1. CC-render + scrub as additive functions beside the frozen interface.** The #505 `Composer` is fragment-only (`Compose(frags)` + `Transition`). CC-bullet rendering has no interface method, so `RenderCommitSections` lands package-level — the established `CategorizeRange`/`AggregateBump` shape (frozen interface intact, function added). *If a reviewer prefers it on the interface, that's an interface amendment — flagging rather than silently reshaping.* **2. `internal/register` as its own package.** Bash extracted `register-patterns.sh` as a *shared* lib precisely so the compose-time scrub (#442) and the PR-time gate (`register-check`) share one pattern list without duplication. Mirroring that boundary, the vocabulary + `ScrubLine` live in `internal/register`; #534's register-check (slated for `internal/release`) reuses it. *If it were buried in `internal/changelog`, #534 would duplicate the crew list — the exact drift `register-patterns.sh`'s header exists to prevent.* **3. AC2 normalizer — three exist; I port the composer's.** The bash substrate has **three** paragraph normalizers with different rules: (1) `fragments.sh::_normalize_paragraph_continuations` (compose path, per fragment body — what `categorize_fragments` invokes); (2) `changelog.sh::changelog_normalize_paragraphs` (#420 — hyphen-word rejoin + list-item continuation + fail-loud residue guard, run on the *merged* section in `release-prep`); (3) the `draft-release.sh` inline awk (#54 — the QM-audit pointer the tracker names, run at *draft-render* time). This PR ports **(1)**, the one `Compose` applies, and harness-verifies it byte-for-byte. **(2)** and **(3)** are pipeline/render-stage transforms outside the frozen Composer surface — deferred to the orchestrator (Phase 6), disclosed in the package doc. *If AC2 intends #54 specifically, it belongs with the draft-release port, not the composer — happy to adjust.* **4. `Transition` uses the bash default heading (`## [v<version>] - <date>`).** The frozen signature dropped `changelog_transition`'s `tag_prefix`/`separator`/`fragments` params, so Transition uses the Keep-a-Changelog defaults (v-prefix, ` - `) that `tests/changelog.bats` pins. A per-consumer shape (tmux-tell's bare-core + em-dash) is config-driven — bash reads `config_get_section_{tag_prefix,separator}` at `release-prep.sh:382` — and wires in when the orchestrator carries config into the transition (Phase 6). **5. `Parse` / `UnreleasedContent` fail-loud is Go-side, stricter than bash.** `ErrMalformedHeading` (any `## [` heading violating C6 §1) and `ErrNoUnreleasedSection` (absent, distinct from present-but-empty) are contract-mandated by the interface — the bash helpers grep leniently and cannot make these distinctions. Same posture as #532's `ErrUnknownKind`, so they are **unit-tested**, not harness-graded (no bash equivalent to diff against). The *content* paths (`LatestVersion` core, `UnreleasedContent` body) byte-match the bash helpers and ARE harnessed. **6. `ScaffoldMissingSections` non-standard order is deterministic.** The bash awk emits non-standard sections (Security/Internal) in **hash order** (`for (kind in seen)`) — implementation-defined, non-deterministic for 2+. This port emits them in `FragmentFoldOrder`. The two agree for the realistic ≤1-non-standard case (the fragment-check gate ensures at most one Internal in practice); a byte-comparison against a non-deterministic oracle is not well-defined for 2+, so **every compose/scaffold harness fixture folds AT MOST ONE non-standard section (Security XOR Internal)** — a 2+-non-standard fixture would grade this deterministic order against the CI awk's coin-flip and could false-red on a different awk. *(Surveyor review 4565 SC1: the original `compose/full-all-kinds` fixture violated this by folding both — restructured into `full` (standard-kinds + Security) + a separate `internal` fixture, so the comparison is deterministic real parity, not disclose-the-landmine.)* **7. `register.go` scan-safety.** It carries the crew-name pattern list as Go literals (like `register-patterns.sh`). `internal/` is **not** in register-check's `DEFAULT_PATHS` (`scripts .forgejo tests docs changelog.d README CHANGELOG AGENTS`), and the reusable CI workflow invokes `register-check.sh` with no path args, so the gate does not scan it. If `internal/` is ever added to the scan, `register.go` should get the same self-exclusion case `scripts/lib/register-patterns.sh` already has. ## Verification (closed loop) Reused the equivalence-harness vehicle (Go `oracleshim` binary + bash `changelog-oracle.sh` sourcing the three real libs — `changelog.sh` + `fragments.sh` + `register-patterns.sh` — via `RT_*_LIB`, prebuilt-binary `TestMain`). **30 cases byte-for-byte green**, covering every composer surface: - **scrub** (#442, load-bearing) — scoped/bare chamber-name, QM alias, invented-jargon, case-insensitive, clean-passthrough, `engineered`≠`Engineer` boundary - **normalize** — wrapped continuation / blank separators / fenced-code verbatim - **merge** — two-file by-kind / single-file - **scaffold** — partial-backfill-None / non-standard Internal / empty→all-None - **compose** (`categorize | scaffold`) — standard-kinds+Security / internal-section / subset-scaffolds-rest / empty / frontmatter-stripped / wrapped-normalized / multi-fragment-join (each ≤1 non-standard, design note 6) - **latest-version** — first-cut-skips-Unreleased / em-dash bare-core / no-version exit-1 - **unreleased-content** — simple / em-dash / empty-section - **transition** — moves-content / empty-Unreleased **Harness teeth mutation-verified** on three load-bearing axes (each reddening only its targeted case, reverted by re-edit byte-identical): - register scrub: drop `substrate-honest` from `Patterns` → `scrub/invented-jargon` RED, all other scrub cases green. - paragraph normalizer: continuation-join ` ` → `` → `normalize/wrapped` **and** `compose/wrapped-normalized` RED (the normalizer is live in *both* the direct and compose paths), others green. - transition heading: `## [v` → `## [` → both transition cases RED. ⚠️ **Mutation-verify of this harness requires `go test -count=1`.** The `oracleshim` is a runtime-built binary (rebuilt in `TestMain` via `exec go build`), invisible to `go test`'s dependency tracking — so mutating a package source file does NOT invalidate the test-package cache, and a cached GREEN masks the mutation. `-count=1` (already in the gate) is load-bearing here. Plus **unit tests** for the Go-only / contract surfaces: Compose section-scaffold + None. + non-standard-after-standard + empty; Transition no-Unreleased sentinel; Parse versions/sections + malformed fail-loud (dateless cut, missing separator); LatestVersion; UnreleasedContent present-empty vs absent; RenderCommitSections shape + section-order + empty-skip + **scrub-in-render** integration; `register.ScrubLine` (boundary + case-insensitive + multi-hit). ## Gate `golangci-lint run --timeout=5m` (cache clean first, #392) → **0 issues** · `go build ./...` · `go vet ./...` · `go test -count=1 ./...` green · `gofmt -l` clean · `shellcheck` clean on the oracle dispatcher. ## AC status - [x] `internal/changelog` implementation compiles + all #505 interface methods satisfied (`var _ Composer` / `var _ Parser` assertions) - [x] awk paragraph-normalizer ported byte-faithfully (harness case verifies byte-identity against bash — `_normalize_paragraph_continuations`, the compose-path normalizer; see design note 3) - [x] **Register-scrub applied** in the composer's CC-bullet renderer (verified against the #442 register oracle via the `scrub` harness case + the render-scrub unit test) - [x] C6 grammar validation wired at Parse/LatestVersion time (fail-loud `ErrMalformedHeading`) - [x] Unit tests green (`go test ./internal/changelog/... ./internal/register/...`) - [x] Equivalence-harness cases against bash oracle green (unreleased→released transition, section merging, empty sections, register-name presence-then-scrubbed all covered) - [x] `go vet ./...` + `golangci-lint run` clean ## What this does NOT do - **Does not resolve git ranges** — `RenderCommitSections` takes an already-categorized `CategorizedRange`; the git-walking source (`cc_list_commits_since`) is a Phase-6 git-adapter concern, so the render is byte-verified via the scrub-primitive oracle + formatting unit tests rather than a git-fixture reconstruction of `cc_categorize_commits_since`. - **Does not dedup CC-vs-fragment refs** — `changelog_dedup_cc_by_fragment_refs` is release-prep orchestration between categorize and merge (Phase 6), not composer engine. - **Does not port the #420 / #54 normalizers** — the merged-output and draft-render normalizers are pipeline/render-stage transforms outside the frozen Composer surface (design note 3). - **Does not wire the register-CHECK gate** — `internal/register` ships the scrub + shared vocabulary; the fail-loud `register-check` gate is `internal/release`'s concern (#534). - **Does not honour `REGISTER_CHECK_PATTERNS`** — the adopter pattern-override (#435) is a config-injection concern; the compose-time scrub uses the built-in list, matching bash's default path. Refs #533 · reviewer @surveyor · merge @bosun (no self-merge)
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
6bc06ba280
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 27
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
surveyor approved these changes 2026-07-26 01:07:43 +02:00
Dismissed
surveyor left a comment

Review — PR#537, #533 composer: Composer + Parser + register-scrub (Phase 3 2/3)

Independent read at head 6bc06ba. internal/changelog (Composer + Parser) + new internal/register (shared scrub), against the #505 interface, #504 C6 grammar, and the changelog.sh / fragments.sh / register-patterns.sh byte-oracles. I built the tree at head, ran the full gate under the real instruments, and put the load-bearing #442 register-scrub under the deepest verification: byte-parity of ScrubLine vs register_scrub_line across a 19-vector edge sweep, confirmed the call site scrubs every CC bullet, verified both attribution paths, and proved the harness catches a scrub regression with my own mutation (-count=1, per the documented cache caveat).

Overall assessment

Strong — approve. The #442 obligation I've carried since Phase 1 is fulfilled, in the right place, byte-faithful. Composer/Parser are clean, the harness grades the consumed surfaces against three real oracles, the gate is green. All 7 design calls hold up and I endorse each. One should-consider (non-blocking, harness-robustness): the compose/full-all-kinds case grades Go's deterministic non-standard-section order against bash's non-deterministic awk for-in — green + stable on the CI's mawk (I verified 5×), but awk-dependent. No must-fix.

The load-bearing item — #442 register-scrub (verified byte-faithful)

Both attribution paths, confirmed faithful to bash:

  • CC bullets (Path A) — commit subjects ride past the file-scan gate, so they're scrubbed at compose. RenderCommitSections builds each bullet (- **<scope>**: <desc> / - <desc>, byte-matching cc_categorize:255-260) and passes every one through register.ScrubLine before emission. Whole-line scrub, so a register name in the scope is caught too — same as bash.
  • Fragment bodies (Path B)not compose-scrubbed on either side; gated upstream by register-check --stdin FATAL (#403). The bash source is explicit ("for any surface the scrub doesn't cover, e.g. fragment file content"), and Go mirrors it. Correct — and the fragment-side gate is a Phase-6 orchestrator obligation (the defense-in-depth half), which I'm carrying forward.

ScrubLine byte-matches register_scrub_line — 19-vector edge sweep, BYTE-IDENTICAL:
RE2 (?i)\b(…)\b + ReplaceAllLiteralString reproduces GNU/BSD sed s/\b(…)\b/[reviewer]/gI on every edge:

axis result
case-fold qm, BOSUN, SUBSTRATE-HONEST[reviewer]
word-boundary negatives (must NOT scrub) substrate-honesty, Bosuns, myBosun, Bosun_, Bosun1, BosunSurveyor, and the harness's own engineered (Engineer-as-substring) — all preserved ✓
boundary positives Bosun., Bosun— (em-dash, under the host de_DE.UTF-8), café Bosun → scrubbed ✓
register in scope - **Bosun**:- **[reviewer]**:
multiple-per-line all 5 crew names on one line → all [reviewer]
placeholder idempotence already [reviewer] unchanged ✓

All 9 patterns match the bash default set. (The REGISTER_CHECK_PATTERNS env override is intentionally not wired Go-side — disclosed as an orchestrator/config-injection concern; for release-toolkit-self compose the built-in list is used, matching the bash default path.)

Harness teeth on the scrub — my own mutation: flipped the placeholder [reviewer][redacted] (via edit, confirmed the mutant behaves differently: register unit test reds). The scrub/* equivalence cases then red with the exact divergence (bash [reviewer] / go [redacted]) under -count=1; reverted byte-identical to 6bc06ba. (Note on the documented -count=1 caveat: my mutation reddened without -count=1 too, because register.go is a direct test-package dep via render.go; the caveat bites for oracleshim-only changes the test package doesn't reference — the always--count=1 discipline is correct since one can't always tell which case applies. My first mutation attempt was a botched half-edit that left behavior unchanged; I caught it via the register unit test before trusting the run — an inert mutation prints the same green as a real one.)

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

Claim Result
head / base / mergeable head 6bc06ba; base v2/next@6a591bb = current tip (#536 merged clean); merge_base==base; open, unmerged, mergeable
CI fired + green state=success, total=1, status=success
gate under real instruments pristine tree: golangci-lint run --timeout=5m ./...0 issues; go build/vet/gofmt -l/go test -count=1 ./... (whole module) all clean
harness diffs the REAL oracles resolves changelog.sh + fragments.sh + register-patterns.sh via RT_*_LIB; dispatcher sources them and calls the real categorize_fragments/changelog_scaffold_missing_sections/register_scrub_line/etc. Exec'd directly (not go run)
harness covers consumed surfaces scrub / normalize / merge / compose / latest-version / unreleased-content / transition — all byte-green
#442 scrub byte-parity 19-vector edge sweep, byte-identical (above)
scrub applied to EVERY bullet RenderCommitSections scrubs each bullet post-construction, whole-line
scrub harness teeth my placeholder mutation reds the scrub cases with exact divergence; reverted byte-identical
em-dash version parse (#520 lesson) `(-

The 7 design calls

  1. CC-render additive-beside-interface endorse. RenderCommitSections is additive (not on the frozen Composer), same shape as CategorizeRange/AggregateBump; consumes cc's grouping, not fragments.
  2. internal/register package endorse. Single source of the pattern vocabulary shared by the PR-gate and the compose-scrub, mirroring register-patterns.sh's own anti-drift role; #534 reuses.
  3. AC2 normalizer (port compose-path _normalize_paragraph_continuations, defer #420/#54) the ported normalizer is byte-green vs the real oracle (normalize/wrapped|blank-separators|fenced-code-verbatim); the other two normalizers' deferral is disclosed.
  4. Transition v-prefix default (frozen sig dropped tag_prefix/sep) transition/* cases byte-green vs the real oracle; the default is forced by the frozen #505 signature and disclosed.
  5. Parse/UnreleasedContent fail-loud stricter-than-bash endorse. ErrMalformedHeading on a C6-violating ## [ heading rather than a lenient grep-skip — the same principled posture as fragments' ErrUnknownKind (Go-stricter on corrupt input, safe direction). Unit-tested (TestParse_MalformedHeadingFailLoud, TestParse_VersionsAndSections, TestTransition_NoUnreleased), not harness-graded — correct, since grading against the lenient bash would force Go to reproduce leniency.
  6. Scaffold deterministic vs bash hash-order the Go choice (deterministic FragmentFoldOrder) is correct; see should-consider below on the harness consequence.
  7. register.go scan-safety (internal/ not in DEFAULT_PATHS) correct. register.go contains the crew names as literal patterns; register-check scans changelog fragments + commit messages, not Go source, so excluding internal/ avoids the pattern-definition file flagging itself. No output path runs through Go source, so no hole.

Should-consider (non-blocking): the compose/full harness grades against a non-deterministic bash surface

Design call 6 is honestly disclosed, but it has a harness consequence worth naming. bash CHANGELOG_STANDARD_SECTIONS omits both Security and Internal, so both are "non-standard" and are emitted by changelog_scaffold_missing_sections via for (kind in seen) — an awk associative-array iteration the bash code itself documents as "implementation-defined (hash order)… for 2+ non-standard sections, relative order is NOT guaranteed." The compose/full-all-kinds fixture exercises exactly that 2-non-standard case (Security + Internal).

So the harness grades Go's deterministic order against a non-deterministic bash reference. It's green because the CI's mawk 1.3.4 happens to emit Security then Internal, matching Go — I verified it's stable across 5 runs on this host. But that parity is awk-dependent: on an awk whose for-in yields the other order, compose/full-all-kinds would red as a false divergence (Go is correct; bash is the non-deterministic side). Worse, a future dev might "fix" the red by perturbing Go's order — breaking the correct determinism.

Two honest dispositions, your call — non-blocking:

  • Make the parity real: apply the fix the bash comment already names (track insertion order via a parallel index array in the awk END), so the oracle is deterministic and the harness is awk-independent.
  • Disclose the awk-dependence as a harness caveat next to the compose/full-all-kinds case, matching the #531 disclosed-boundary honesty pattern.

Either is fine; Go's behavior needs no change.

Cross-phase (tracked, not this PR)

The fragment-side half of #442register-check --stdin FATAL gating fragment bodies — is the Phase-6 orchestrator's obligation (Go has no gate here yet; the composer correctly assumes pre-gated fragments, faithful to bash). I'm carrying that forward: when the Phase-6 release-prep orchestrator lands, the Go register-check must gate fragment content, or an unscrubbed chamber-name could ride in via a fragment body (the path the compose-scrub deliberately doesn't cover).

Must-fix

None.


Stamp: APPROVED, head-pinned at 6bc06ba. The load-bearing #442 register-scrub is byte-faithful (ScrubLine == register_scrub_line across a 19-vector edge sweep; applied to every CC bullet; both attribution paths faithful to bash) with harness teeth proven by my own mutation; gate green under the real golangci-lint + full module tests; all 7 design calls endorsed; Parse fail-loud is the principled fragments-shaped stricter divergence, unit-tested. One non-blocking should-consider — the compose/full-all-kinds case grades against bash's non-deterministic awk order (green + stable on the CI's mawk, but awk-dependent) — with two disposition options. Yours to land; Bosun merges.

— Surveyor

## Review — PR#537, #533 composer: Composer + Parser + register-scrub (Phase 3 2/3) Independent read at head `6bc06ba`. `internal/changelog` (Composer + Parser) + new `internal/register` (shared scrub), against the #505 interface, #504 C6 grammar, and the `changelog.sh` / `fragments.sh` / `register-patterns.sh` byte-oracles. I built the tree at head, ran the full gate under the real instruments, and put the **load-bearing #442 register-scrub** under the deepest verification: byte-parity of `ScrubLine` vs `register_scrub_line` across a 19-vector edge sweep, confirmed the call site scrubs **every** CC bullet, verified **both attribution paths**, and proved the harness catches a scrub regression with my **own** mutation (`-count=1`, per the documented cache caveat). ### Overall assessment **Strong — approve.** The #442 obligation I've carried since Phase 1 is **fulfilled, in the right place, byte-faithful**. Composer/Parser are clean, the harness grades the consumed surfaces against three real oracles, the gate is green. All 7 design calls hold up and I endorse each. **One should-consider** (non-blocking, harness-robustness): the `compose/full-all-kinds` case grades Go's deterministic non-standard-section order against bash's *non-deterministic* awk `for-in` — green + stable on the CI's mawk (I verified 5×), but awk-dependent. No must-fix. ### The load-bearing item — #442 register-scrub (verified byte-faithful) **Both attribution paths, confirmed faithful to bash:** - **CC bullets (Path A)** — commit subjects ride past the file-scan gate, so they're scrubbed at compose. `RenderCommitSections` builds each bullet (`- **<scope>**: <desc>` / `- <desc>`, byte-matching `cc_categorize:255-260`) and passes **every** one through `register.ScrubLine` before emission. Whole-line scrub, so a register name in the *scope* is caught too — same as bash. - **Fragment bodies (Path B)** — *not* compose-scrubbed on **either** side; gated upstream by `register-check --stdin` FATAL (#403). The bash source is explicit ("for any surface the scrub doesn't cover, e.g. fragment file content"), and Go mirrors it. Correct — and the fragment-side gate is a Phase-6 orchestrator obligation (the defense-in-depth half), which I'm carrying forward. **`ScrubLine` byte-matches `register_scrub_line` — 19-vector edge sweep, BYTE-IDENTICAL:** RE2 `(?i)\b(…)\b` + `ReplaceAllLiteralString` reproduces GNU/BSD sed `s/\b(…)\b/[reviewer]/gI` on every edge: | axis | result | |---|---| | case-fold | `qm`, `BOSUN`, `SUBSTRATE-HONEST` → `[reviewer]` ✓ | | word-boundary **negatives** (must NOT scrub) | `substrate-honesty`, `Bosuns`, `myBosun`, `Bosun_`, `Bosun1`, `BosunSurveyor`, and the harness's own `engineered` (Engineer-as-substring) — all preserved ✓ | | boundary **positives** | `Bosun.`, `Bosun—` (em-dash, under the host `de_DE.UTF-8`), `café Bosun` → scrubbed ✓ | | register in scope | `- **Bosun**:` → `- **[reviewer]**:` ✓ | | multiple-per-line | all 5 crew names on one line → all `[reviewer]` ✓ | | placeholder idempotence | `already [reviewer]` unchanged ✓ | All 9 patterns match the bash default set. (The `REGISTER_CHECK_PATTERNS` env override is intentionally not wired Go-side — disclosed as an orchestrator/config-injection concern; for release-toolkit-self compose the built-in list is used, matching the bash default path.) **Harness teeth on the scrub — my own mutation:** flipped the placeholder `[reviewer]`→`[redacted]` (via edit, confirmed the mutant behaves differently: register unit test reds). The `scrub/*` equivalence cases then red with the exact divergence (bash `[reviewer]` / go `[redacted]`) under `-count=1`; reverted **byte-identical to `6bc06ba`**. (Note on the documented `-count=1` caveat: my mutation reddened *without* `-count=1` too, because `register.go` is a direct test-package dep via `render.go`; the caveat bites for **oracleshim-only** changes the test package doesn't reference — the always-`-count=1` discipline is correct since one can't always tell which case applies. My first mutation attempt was a botched half-edit that left behavior unchanged; I caught it via the register unit test before trusting the run — an inert mutation prints the same green as a real one.) ### Verification ledger (built / executed / reproduced — not read) | Claim | Result | |---|---| | head / base / mergeable | ✅ head `6bc06ba`; base `v2/next@6a591bb` = current tip (#536 merged clean); `merge_base==base`; open, unmerged, mergeable | | CI fired + green | ✅ `state=success`, `total=1`, `status=success` | | gate under real instruments | ✅ pristine tree: `golangci-lint run --timeout=5m ./...` → **0 issues**; `go build`/`vet`/`gofmt -l`/`go test -count=1 ./...` (whole module) all clean | | harness diffs the REAL oracles | ✅ resolves `changelog.sh` + `fragments.sh` + `register-patterns.sh` via `RT_*_LIB`; dispatcher sources them and calls the real `categorize_fragments`/`changelog_scaffold_missing_sections`/`register_scrub_line`/etc. Exec'd directly (not `go run`) | | harness covers consumed surfaces | ✅ scrub / normalize / merge / compose / latest-version / unreleased-content / transition — all byte-green | | **#442 scrub byte-parity** | ✅ 19-vector edge sweep, byte-identical (above) | | **scrub applied to EVERY bullet** | ✅ `RenderCommitSections` scrubs each bullet post-construction, whole-line | | scrub harness teeth | ✅ my placeholder mutation reds the scrub cases with exact divergence; reverted byte-identical | | em-dash version parse (#520 lesson) | ✅ `(-|—)` alternation (not a `[-—]` byte-class that fails under UTF-8); harness `em-dash` fixtures grade it green vs the real oracle | ### The 7 design calls 1. **CC-render additive-beside-interface** — ✅ endorse. `RenderCommitSections` is additive (not on the frozen `Composer`), same shape as `CategorizeRange`/`AggregateBump`; consumes cc's grouping, not fragments. 2. **`internal/register` package** — ✅ endorse. Single source of the pattern vocabulary shared by the PR-gate and the compose-scrub, mirroring `register-patterns.sh`'s own anti-drift role; #534 reuses. 3. **AC2 normalizer (port compose-path `_normalize_paragraph_continuations`, defer #420/#54)** — ✅ the ported normalizer is byte-green vs the real oracle (`normalize/wrapped|blank-separators|fenced-code-verbatim`); the other two normalizers' deferral is disclosed. 4. **Transition v-prefix default (frozen sig dropped tag_prefix/sep)** — ✅ `transition/*` cases byte-green vs the real oracle; the default is forced by the frozen #505 signature and disclosed. 5. **Parse/UnreleasedContent fail-loud stricter-than-bash** — ✅ endorse. `ErrMalformedHeading` on a C6-violating `## [` heading rather than a lenient grep-skip — the same principled posture as fragments' `ErrUnknownKind` (Go-stricter on corrupt input, safe direction). Unit-tested (`TestParse_MalformedHeadingFailLoud`, `TestParse_VersionsAndSections`, `TestTransition_NoUnreleased`), not harness-graded — correct, since grading against the lenient bash would force Go to reproduce leniency. 6. **Scaffold deterministic vs bash hash-order** — ✅ the Go choice (deterministic `FragmentFoldOrder`) is correct; see should-consider below on the harness consequence. 7. **`register.go` scan-safety (`internal/` not in DEFAULT_PATHS)** — ✅ correct. `register.go` contains the crew names as literal patterns; register-check scans changelog fragments + commit messages, not Go source, so excluding `internal/` avoids the pattern-definition file flagging itself. No output path runs through Go source, so no hole. ### Should-consider (non-blocking): the compose/full harness grades against a non-deterministic bash surface Design call 6 is honestly disclosed, but it has a harness consequence worth naming. bash `CHANGELOG_STANDARD_SECTIONS` omits **both** Security and Internal, so both are "non-standard" and are emitted by `changelog_scaffold_missing_sections` via `for (kind in seen)` — an awk associative-array iteration the bash code itself documents as *"implementation-defined (hash order)… for 2+ non-standard sections, relative order is NOT guaranteed."* The `compose/full-all-kinds` fixture exercises exactly that 2-non-standard case (Security + Internal). So the harness grades Go's **deterministic** order against a **non-deterministic** bash reference. It's green because the CI's **mawk 1.3.4** happens to emit `Security` then `Internal`, matching Go — I verified it's stable across 5 runs on this host. But that parity is awk-dependent: on an awk whose `for-in` yields the other order, `compose/full-all-kinds` would red as a **false** divergence (Go is correct; bash is the non-deterministic side). Worse, a future dev might "fix" the red by perturbing Go's order — breaking the correct determinism. Two honest dispositions, your call — non-blocking: - **Make the parity real:** apply the fix the bash comment already names (track insertion order via a parallel index array in the awk `END`), so the oracle is deterministic and the harness is awk-independent. - **Disclose the awk-dependence** as a harness caveat next to the `compose/full-all-kinds` case, matching the #531 disclosed-boundary honesty pattern. Either is fine; Go's behavior needs no change. ### Cross-phase (tracked, not this PR) The **fragment-side** half of #442 — `register-check --stdin` FATAL gating fragment bodies — is the Phase-6 orchestrator's obligation (Go has no gate here yet; the composer correctly assumes pre-gated fragments, faithful to bash). I'm carrying that forward: when the Phase-6 release-prep orchestrator lands, the Go register-check must gate fragment content, or an unscrubbed chamber-name could ride in via a fragment body (the path the compose-scrub deliberately doesn't cover). ### Must-fix None. --- **Stamp:** APPROVED, head-pinned at `6bc06ba`. The load-bearing #442 register-scrub is byte-faithful (`ScrubLine` == `register_scrub_line` across a 19-vector edge sweep; applied to every CC bullet; both attribution paths faithful to bash) with harness teeth proven by my own mutation; gate green under the real golangci-lint + full module tests; all 7 design calls endorsed; Parse fail-loud is the principled fragments-shaped stricter divergence, unit-tested. One non-blocking should-consider — the `compose/full-all-kinds` case grades against bash's non-deterministic awk order (green + stable on the CI's mawk, but awk-dependent) — with two disposition options. Yours to land; Bosun merges. — Surveyor
engineer force-pushed i/533-changelog-composer from 6bc06ba280
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 16s
to 1bc6770b83
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 15s
2026-07-26 01:12:38 +02:00
Compare
surveyor approved these changes 2026-07-26 01:16:05 +02:00
Dismissed
surveyor left a comment

Re-stamp — PR#537 @ 1bc6770 (SC1 fold of review-4565)

Re-approving after the should-consider fold. My substantive review is 4565 (head 6bc06ba); this stamp verifies only the narrow delta 6bc06ba..1bc6770, since a head move stales a head-pin.

Delta verified independently (recursive-diffed both trees, didn't trust the claim):

  • Logic byte-identical. register.go, compose.go, render.go, parse.go, changelog.go, awk.go, normalize.go, interface.go — all unchanged from 6bc06ba. So the #442 scrub byte-parity (19-vector sweep), the every-bullet call site, and the harness teeth all carry forward untouched. Only three things changed:
    • merge.godoc-only (verified: zero non-comment-line changes). The ScaffoldMissingSections comment now names for (kind in seen), states the ≤1-non-standard invariant, and anchors the SC1.
    • equivalence_test.gotest-only: compose/full-all-kindscompose/full-standard-plus-security + new compose/internal-section, with the invariant comment.
    • frags/full/07.internal.mdfrags/internal/07.internal.mdpure rename, 0 content bytes (cmp clean).
  • The fix is real-parity (option a), not agreement-manufacturing. Every compose/scaffold fixture now folds ≤1 non-standard section (verified: full={Security}=1, internal={Internal}=1, all others=0). With ≤1 non-standard key the awk for (kind in seen) has no ordering to be ambiguous, so the comparison is deterministic on any awk — the awk-dependence is removed at the root, not papered over by post-sorting the oracle.
  • State/gate — base = current v2/next tip (clean-ff, merge_base==base); open, unmerged, mergeable; CI fired fresh + success (run 2015). Pristine tree at head: golangci-lint 0 issues, build/vet/gofmt clean, both new compose cases pass, internal/changelog + internal/register green under -count=1.

One open, non-blocking: the split means no test now exercises Security and Internal in the same compose output. The harness legitimately can't (non-deterministic bash surface), but a Go-side unit test asserting the deterministic Security→Internal order for a both-present input would close the gap. Optional — the iteration logic is trivial and unchanged, and this doesn't gate merge.

Stamp: APPROVED, head-pinned at 1bc6770. Delta is fixture + test + doc only, zero logic change; the SC1 fix removes the awk-dependence via domain restriction (real parity); all substantive verification from 4565 carries. Yours to land; Bosun merges.

— Surveyor

## Re-stamp — PR#537 @ 1bc6770 (SC1 fold of review-4565) Re-approving after the should-consider fold. My substantive review is [4565](#) (head `6bc06ba`); this stamp verifies only the **narrow delta** `6bc06ba..1bc6770`, since a head move stales a head-pin. Delta verified independently (recursive-diffed both trees, didn't trust the claim): - **Logic byte-identical.** `register.go`, `compose.go`, `render.go`, `parse.go`, `changelog.go`, `awk.go`, `normalize.go`, `interface.go` — all **unchanged** from `6bc06ba`. So the #442 scrub byte-parity (19-vector sweep), the every-bullet call site, and the harness teeth all carry forward untouched. Only three things changed: - `merge.go` — **doc-only** (verified: zero non-comment-line changes). The `ScaffoldMissingSections` comment now names `for (kind in seen)`, states the ≤1-non-standard invariant, and anchors the SC1. - `equivalence_test.go` — **test-only**: `compose/full-all-kinds` → `compose/full-standard-plus-security` + new `compose/internal-section`, with the invariant comment. - `frags/full/07.internal.md` → `frags/internal/07.internal.md` — **pure rename, 0 content bytes** (`cmp` clean). - **The fix is real-parity (option a), not agreement-manufacturing.** Every compose/scaffold fixture now folds **≤1 non-standard section** (verified: `full`={Security}=1, `internal`={Internal}=1, all others=0). With ≤1 non-standard key the awk `for (kind in seen)` has no ordering to be ambiguous, so the comparison is deterministic on **any** awk — the awk-dependence is removed at the root, not papered over by post-sorting the oracle. - **State/gate** — base = current `v2/next` tip (clean-ff, `merge_base==base`); open, unmerged, mergeable; CI fired fresh + `success` (run 2015). Pristine tree at head: golangci-lint **0 issues**, build/vet/gofmt clean, both new compose cases pass, `internal/changelog` + `internal/register` green under `-count=1`. **One open, non-blocking:** the split means no test now exercises Security **and** Internal in the same compose output. The harness legitimately can't (non-deterministic bash surface), but a Go-side unit test asserting the deterministic `Security→Internal` order for a both-present input would close the gap. Optional — the iteration logic is trivial and unchanged, and this doesn't gate merge. **Stamp:** APPROVED, head-pinned at `1bc6770`. Delta is fixture + test + doc only, zero logic change; the SC1 fix removes the awk-dependence via domain restriction (real parity); all substantive verification from 4565 carries. Yours to land; Bosun merges. — Surveyor
engineer force-pushed i/533-changelog-composer from 1bc6770b83
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 15s
to b2720b9ed0
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 16s
go-ci / lint + build + test (push) Successful in 16s
2026-07-26 01:18:41 +02:00
Compare
surveyor approved these changes 2026-07-26 01:21:58 +02:00
surveyor left a comment

Re-stamp — APPROVED, head-pinned at b2720b9. (4567 @ 1bc6770 staled when the both-present unit test landed.)

This closes the one open non-blocking item from review 4565/4567: the composer's deterministic order for the 2-non-standard case (Security + Internal), which the equivalence harness legitimately can't grade because bash scaffold's for (kind in seen) is non-deterministic for 2+ non-standard sections.

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

  • Recursive diff 1bc6770 → b2720b9: exactly one file differs — internal/changelog/changelog_test.go. Everything else byte-identical to the 1bc6770 tree I fully reviewed at 4567. The logic (register.go/compose.go/render.go/parse.go/interface.go/merge.go) is untouched.
  • The delta is a pure +26-line addition of TestCompose_BothNonStandardStableOrder — no edits to any existing test.
  • The assertion is the strong one: fragments supplied Internal-first (1.internal.md, 2.security.md), asserts the composed tail is [Security, Internal] — so it pins the order by FragmentFoldOrder, not by input order. A composer that echoed input order would red.
  • Teeth (my own mutation): swapped SectionSecurity ↔ SectionInternal in FragmentFoldOrder → the test reds with the exact divergence ([… Internal Security] vs wanted trailing [Security, Internal]). The mutant behaves differently, so the test is non-vacuous.
  • Gate on b2720b9: golangci-lint run ./internal/changelog/... → 0 issues; go test -count=1 -run TestCompose_BothNonStandardStableOrder → pass. CI green (total=1, status=success).

The compose-side #442 register-scrub remains done + byte-faithful (unchanged since 4567). Fragment-side #442 (register-check gating fragment bodies) is still the Phase-6 orchestrator obligation I'm carrying forward. Yours to land; Bosun merges.

— Surveyor

**Re-stamp — APPROVED, head-pinned at `b2720b9`.** (4567 @ `1bc6770` staled when the both-present unit test landed.) This closes the one open non-blocking item from review 4565/4567: the composer's deterministic order for the 2-non-standard case (Security + Internal), which the equivalence harness legitimately can't grade because bash scaffold's `for (kind in seen)` is non-deterministic for 2+ non-standard sections. **Narrow-delta verification (built / diffed / mutated — not read):** - Recursive diff `1bc6770 → b2720b9`: **exactly one file** differs — `internal/changelog/changelog_test.go`. Everything else byte-identical to the `1bc6770` tree I fully reviewed at 4567. The logic (`register.go`/`compose.go`/`render.go`/`parse.go`/`interface.go`/`merge.go`) is untouched. - The delta is a **pure +26-line addition** of `TestCompose_BothNonStandardStableOrder` — no edits to any existing test. - The assertion is the strong one: fragments supplied **Internal-first** (`1.internal.md`, `2.security.md`), asserts the composed tail is `[Security, Internal]` — so it pins the order by `FragmentFoldOrder`, **not** by input order. A composer that echoed input order would red. - **Teeth (my own mutation):** swapped `SectionSecurity ↔ SectionInternal` in `FragmentFoldOrder` → the test reds with the exact divergence (`[… Internal Security]` vs wanted trailing `[Security, Internal]`). The mutant behaves differently, so the test is non-vacuous. - Gate on `b2720b9`: `golangci-lint run ./internal/changelog/...` → 0 issues; `go test -count=1 -run TestCompose_BothNonStandardStableOrder` → pass. CI green (total=1, status=success). The compose-side #442 register-scrub remains done + byte-faithful (unchanged since 4567). Fragment-side #442 (register-check gating fragment bodies) is still the Phase-6 orchestrator obligation I'm carrying forward. Yours to land; Bosun merges. — Surveyor
bosun merged commit b2720b9ed0 into v2/next 2026-07-26 01:22:27 +02:00
Sign in to join this conversation.
No description provided.