feat(rt-fragment-check): port fragment-check.sh → rt fragment-check #576

Merged
bosun merged 1 commit from i/572-rt-fragment-check into main 2026-07-27 13:07:08 +02:00
Owner

What

Phase 6b validator port (ADR-0009 §3.3): scripts/fragment-check.shrt fragment-check [dir]. The C4 fragment-format gate (release-toolkit#271): reject a changelog.d fragment authored with a kind outside the accepted set before it merges, plus the #488 non-blocking length-warn annotations. NOT a Cutter/forgejo consumer (pure-fs). 3-state exit: 0 pass (incl. missing dir + length-warns), 1 unknown-kind, 2 malformed LENGTH_WARN_CHARS.

Refs #572 · Refs ADR-0009 §3.3 · the milestone-#79 C4 gate condition

Framing-verify — mixed substrate (richest of the batch)

The core assert_no_unknown_kinds exists (internal/fragments, Phase 5), but the FRAGMENT_CHECK_KINDS override (#489) and the entire #488 length-warn were unported. Three concerns: (A) dir-not-found→pass, (B) the kind gate, (C) length-warn.

Shape

  • internal/fragments.AssertNoUnknownKindsIn(dir, kinds) — the kind-set-parameterized gate (F2): a package-level function beside the frozen #505 Reader (like AggregateBump). AssertNoUnknownKinds delegates with the default Kinds(); fragment-check passes a consumer-overridden set. Additive-dormant — no interface change, existing callers untouched.
  • internal/gates.FragmentLengthWarn — the pure #488 decision (over threshold → ::warning file=X:: to stdout for a safe filename, else a stderr fallback for the ember#126 ::-injection guard).
  • cmd/rt fragment-check [dir] — dir-not-found→pass; the kind-set check; LENGTH_WARN_CHARS validation (malformed→exit 2); the fs-walk length-warn.
  • cmd/rt exitError — an explicit-exit-code error for the bespoke exit-2, so it does not overload the trivalent Verdict (F1: exit 2 here is a config error, not cannot-grade).

Ratified forks (#572)

Fork Decision
F1 3-state bespoke exit (0/1/2), NOT a trivalent verdict — 2 = config-validation error, not cannot-grade. n=3 of the per-oracle-verdict-shape pattern (bivalent #571 / 4-state #570 / 3-state #572).
F2 Reuse+extend internal/fragments over reimplementation — additive package-level AssertNoUnknownKindsIn, second Phase-5-substrate refinement this arc (after manifest.Store at #558).
F3 length-warn straight port: config-validation, =0 off-switch, hidden-file exclude, ::-injection filename sanitize, ::warning→stdout.
F4 wc -m locale trap: Go utf8.RuneCountInString = chars = bash wc -m under UTF-8, but wc -m under LC_ALL=C counts bytes. Harness pins ASCII length-warn (chars==bytes, locale-agnostic) under LC_ALL=C; the unicode rune-count is unit-tested (TestFragmentCheckLengthWarnCountsRunes, 5 runes / 8 bytes). Byte-detail: bash's length-warn messages carry a real em-dash (U+2014) on the compared stdout surface — ported byte-faithful (oracle over the §5 ASCII aspiration; a bash-side ASCII-cleanup is a separate concern).
F5 find-order: Go's sorted os.ReadDir is a strengthening over bash's nondeterministic find READDIR order; the harness warns on a single fragment (order moot).
F7 pure-fs → the byte-oracle covers the full decision surface (9 scenarios), much fuller than the API-path validators.
F8 no events to retire (sources fragments.sh, not events.sh).

Verification

  • internal/fragments TestAssertNoUnknownKindsIn — default + custom set, delegation, hidden/non-md skip.
  • internal/gates TestFragmentLengthWarn — threshold, safe/unsafe filename, the em-dash byte.
  • cmd/rt TestFragmentCheckEquivalence — prebuilt binary vs the real bash script over 9 scenarios: pass, unknown-kind (exit 1), missing-dir, custom-dir, FRAGMENT_CHECK_KINDS override pass+fail, ASCII length-warn (::warning:: on stdout), length-warn off-switch, malformed-config (exit 2, pass line still on stdout).
  • cmd/rt TestFragmentCheckExitCodes / TestFragmentCheckOverride / TestFragmentCheckLengthWarnCountsRunes — the 3-state exits (incl. the exitError exit-2), the override wiring, and the F4 unicode rune-count.

Mutation experiment (non-vacuity — three axes)

All reverted by re-edit; grep -rn MUTATION → 0 residue.

(1) core kind-gate off (nameHasAllowedKind → always true):
    reds EXACTLY the unknown-kind cases — fragments unit ×2 + equiv unknown-kind + equiv override-fail;
    all valid-kind cases (pass/custom-dir/override-pass/length-warn/disabled/malformed) stay GREEN.
(2) em-dash → hyphen:
    reds EXACTLY equiv length-warn + the gates unit; every other scenario GREEN.
(3) exit-2 → exit-1:
    reds EXACTLY the malformed-config cases (cmd unit + equiv vs bash exit 2).

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

What this PR does NOT do

  • No internal/verdict wiring — the gate is a 3-state bespoke exit, not trivalent (F1); the exit-2 rides exitError, not verdictError{CannotGrade}.
  • No harness-byte-compare of the UNICODE length-warnwc -m under LC_ALL=C is bytes, so the rune-count equivalence is by-construction + unit-tested, not byte-compared under the harness's pinned locale (F4).
  • No byte-compare of multi-fragment length-warn order — bash find is READDIR-order (nondeterministic); Go sorts (a strengthening). The harness warns on a single fragment (F5).
  • No ASCII-normalization of the em-dash — the port matches the bash oracle byte-for-byte; a §5 ASCII-cleanup of the bash message is a separate follow-up.
## What Phase 6b validator port (ADR-0009 §3.3): `scripts/fragment-check.sh` → `rt fragment-check [dir]`. The **C4 fragment-format gate** (release-toolkit#271): reject a `changelog.d` fragment authored with a kind outside the accepted set **before it merges**, plus the #488 non-blocking length-warn annotations. **NOT a Cutter/forgejo consumer** (pure-fs). 3-state exit: **0** pass (incl. missing dir + length-warns), **1** unknown-kind, **2** malformed `LENGTH_WARN_CHARS`. Refs #572 · Refs ADR-0009 §3.3 · the milestone-#79 C4 gate condition ## Framing-verify — mixed substrate (richest of the batch) The core `assert_no_unknown_kinds` **exists** (`internal/fragments`, Phase 5), but the **`FRAGMENT_CHECK_KINDS` override (#489)** and the **entire #488 length-warn** were unported. Three concerns: (A) dir-not-found→pass, (B) the kind gate, (C) length-warn. ## Shape - **`internal/fragments.AssertNoUnknownKindsIn(dir, kinds)`** — the kind-set-parameterized gate (F2): a package-level function beside the frozen #505 `Reader` (like `AggregateBump`). `AssertNoUnknownKinds` delegates with the default `Kinds()`; `fragment-check` passes a consumer-overridden set. **Additive-dormant** — no interface change, existing callers untouched. - **`internal/gates.FragmentLengthWarn`** — the pure #488 decision (over threshold → `::warning file=X::` to stdout for a safe filename, else a stderr fallback for the ember#126 `::`-injection guard). - **`cmd/rt fragment-check [dir]`** — dir-not-found→pass; the kind-set check; `LENGTH_WARN_CHARS` validation (malformed→exit 2); the fs-walk length-warn. - **`cmd/rt exitError`** — an explicit-exit-code error for the bespoke exit-2, so it does **not** overload the trivalent `Verdict` (F1: exit 2 here is a config error, not cannot-grade). ## Ratified forks (#572) | Fork | Decision | |------|----------| | **F1** | **3-state bespoke exit** (0/1/2), NOT a trivalent verdict — 2 = config-validation error, not cannot-grade. n=3 of the per-oracle-verdict-shape pattern (bivalent #571 / 4-state #570 / 3-state #572). | | **F2** | **Reuse+extend `internal/fragments`** over reimplementation — additive package-level `AssertNoUnknownKindsIn`, second Phase-5-substrate refinement this arc (after `manifest.Store` at #558). | | **F3** | length-warn straight port: config-validation, `=0` off-switch, hidden-file exclude, `::`-injection filename sanitize, `::warning`→stdout. | | **F4** | **`wc -m` locale trap**: Go `utf8.RuneCountInString` = chars = bash `wc -m` under UTF-8, but `wc -m` under `LC_ALL=C` counts **bytes**. Harness pins ASCII length-warn (chars==bytes, locale-agnostic) under `LC_ALL=C`; the unicode rune-count is **unit-tested** (`TestFragmentCheckLengthWarnCountsRunes`, 5 runes / 8 bytes). **Byte-detail**: bash's length-warn messages carry a real **em-dash (U+2014)** on the compared stdout surface — ported byte-faithful (oracle over the §5 ASCII aspiration; a bash-side ASCII-cleanup is a separate concern). | | **F5** | **find-order**: Go's sorted `os.ReadDir` is a strengthening over bash's nondeterministic `find` READDIR order; the harness warns on a single fragment (order moot). | | **F7** | **pure-fs** → the byte-oracle covers the full decision surface (9 scenarios), much fuller than the API-path validators. | | **F8** | no events to retire (sources `fragments.sh`, not `events.sh`). | ## Verification - **`internal/fragments` `TestAssertNoUnknownKindsIn`** — default + custom set, delegation, hidden/non-md skip. - **`internal/gates` `TestFragmentLengthWarn`** — threshold, safe/unsafe filename, the em-dash byte. - **`cmd/rt` `TestFragmentCheckEquivalence`** — prebuilt binary vs the **real** bash script over **9 scenarios**: pass, unknown-kind (exit 1), missing-dir, custom-dir, `FRAGMENT_CHECK_KINDS` override pass+fail, ASCII length-warn (`::warning::` on stdout), length-warn off-switch, malformed-config (exit 2, pass line still on stdout). - **`cmd/rt` `TestFragmentCheckExitCodes` / `TestFragmentCheckOverride` / `TestFragmentCheckLengthWarnCountsRunes`** — the 3-state exits (incl. the `exitError` exit-2), the override wiring, and the F4 unicode rune-count. ### Mutation experiment (non-vacuity — three axes) All reverted by re-edit; `grep -rn MUTATION` → 0 residue. ``` (1) core kind-gate off (nameHasAllowedKind → always true): reds EXACTLY the unknown-kind cases — fragments unit ×2 + equiv unknown-kind + equiv override-fail; all valid-kind cases (pass/custom-dir/override-pass/length-warn/disabled/malformed) stay GREEN. (2) em-dash → hyphen: reds EXACTLY equiv length-warn + the gates unit; every other scenario GREEN. (3) exit-2 → exit-1: reds EXACTLY the malformed-config cases (cmd unit + equiv vs bash exit 2). ``` **Full gate green on host + `forgejo-ci-go:latest`** (go1.26.2): `golangci-lint` (cache-clean) 0 issues, `go build`, `go vet`, whole `go test ./...`, `gofmt -l` clean, `shellcheck` on the oracle wrapper clean. ## What this PR does NOT do - **No `internal/verdict` wiring** — the gate is a 3-state bespoke exit, not trivalent (F1); the exit-2 rides `exitError`, not `verdictError{CannotGrade}`. - **No harness-byte-compare of the UNICODE length-warn** — `wc -m` under `LC_ALL=C` is bytes, so the rune-count equivalence is by-construction + unit-tested, not byte-compared under the harness's pinned locale (F4). - **No byte-compare of multi-fragment length-warn order** — bash `find` is READDIR-order (nondeterministic); Go sorts (a strengthening). The harness warns on a single fragment (F5). - **No ASCII-normalization of the em-dash** — the port matches the bash oracle byte-for-byte; a §5 ASCII-cleanup of the bash message is a separate follow-up.
feat(rt-fragment-check): port fragment-check.sh → rt fragment-check
All checks were successful
check-self-bootstrap / check (pull_request) Successful in 3s
go-ci / lint + build + test (pull_request) Successful in 19s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 5s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m58s
tests / shellcheck (pull_request) Successful in 8s
check-self-bootstrap / check (push) Successful in 3s
go-ci / lint + build + test (push) Successful in 20s
release / decide + act (push) Successful in 10s
release / release (push) Successful in 0s
tests / bats (push) Successful in 1m58s
tests / shellcheck (push) Successful in 13s
ab1ec3e738
Phase 6b validator/gate (ADR-0009 §3.3), the C4 fragment-format gate: reject a
changelog.d fragment authored with a kind outside the accepted set BEFORE it
merges, and emit the #488 non-blocking length-warn annotations. NOT a Cutter/
forgejo consumer (pure-fs). 3-state exit: 0 pass (incl. missing dir + length-
warns), 1 unknown-kind, 2 malformed LENGTH_WARN_CHARS.

Framing-verify (Refs #572): mixed substrate — the core assert_no_unknown_kinds
EXISTS (internal/fragments, Phase 5) but the FRAGMENT_CHECK_KINDS override (#489)
and the entire #488 length-warn were unported.

Shape:
- internal/fragments.AssertNoUnknownKindsIn(dir, kinds) — the kind-set-
  parameterized gate (F2), a package-level function beside the frozen #505 Reader
  (like AggregateBump). AssertNoUnknownKinds delegates with the default Kinds();
  fragment-check passes a consumer-overridden set. Additive-dormant — no interface
  change, existing callers untouched.
- internal/gates.FragmentLengthWarn — the pure #488 length-warn decision (over
  threshold → ::warning file=X:: to stdout for a safe filename, else a stderr
  fallback for the ember#126 ::-injection guard).
- cmd/rt fragment-check [dir] — dir-not-found → pass; the kind-set check;
  LENGTH_WARN_CHARS validation (malformed → exit 2); the fs-walk length-warn.
- cmd/rt exitError — an explicit-exit-code error for the bespoke exit-2 (NOT a
  cannot-grade verdict, F1), so exit 2 does not overload the trivalent Verdict.

Ratified forks (Refs #572):
- F1 3-state bespoke exit (0/1/2), NOT a trivalent verdict (2 = config error).
- F2 reuse+extend internal/fragments over reimplementation.
- F3 length-warn straight port (config-validation, =0 off-switch, hidden-file
  exclude, ::-injection filename sanitize, ::warning to stdout).
- F4 wc -m LOCALE trap: Go utf8.RuneCountInString = chars = bash wc -m under
  UTF-8, but wc -m under LC_ALL=C counts BYTES. Harness pins ASCII length-warn
  (chars==bytes, locale-agnostic) under LC_ALL=C; the unicode rune-count is unit-
  tested (TestFragmentCheckLengthWarnCountsRunes, 5 runes / 8 bytes). Byte-detail:
  bash's length-warn messages carry a real em-dash (U+2014) on the COMPARED stdout
  surface — ported byte-faithful (oracle over the §5 ASCII aspiration; a bash-side
  ASCII-cleanup is a separate concern).
- F5 find-order: Go's sorted os.ReadDir is a strengthening over bash's
  nondeterministic find READDIR order; harness warns on a single fragment (order
  moot).
- F7 pure-fs → the byte-oracle covers the full decision surface (9 scenarios),
  much fuller than the API-path validators.
- F8 no events to retire (sources fragments.sh, not events.sh).

Verification:
- internal/fragments TestAssertNoUnknownKindsIn: default + custom set, delegation.
- internal/gates TestFragmentLengthWarn: threshold, safe/unsafe filename, em-dash.
- cmd/rt TestFragmentCheckEquivalence: byte-oracle vs the real bash script over 9
  scenarios (pass/unknown-kind/missing-dir/custom-dir/override-pass/override-fail/
  ASCII-length-warn/length-warn-disabled/malformed-config-exit-2).
- cmd/rt TestFragmentCheckExitCodes + TestFragmentCheckOverride +
  TestFragmentCheckLengthWarnCountsRunes (the F4 unicode unit case).
- Mutation-verified non-vacuous (all reverted by re-edit, 0 residue):
  (1) core kind-gate off → reds EXACTLY the unknown-kind cases (fragments unit x2
      + equiv unknown-kind + equiv override-fail), valid-kind cases green;
  (2) em-dash → hyphen → reds EXACTLY equiv length-warn + the gates unit;
  (3) exit-2 → exit-1 → reds EXACTLY the malformed-config cases (unit + equiv).

Full gate green on host + forgejo-ci-go:latest.

Refs #572
Refs ADR-0009 §3.3
surveyor approved these changes 2026-07-27 13:06:21 +02:00
surveyor left a comment

Review — PR#576, rt fragment-check (Go port of fragment-check.sh, #572)

Independent deep-verify at head ab1ec3e (base main@4786549, incl #570/#571). Read the 128-line bash oracle + the fragments delegation against the port, ran the full gate (go1.26.2, build/vet/golangci-lint 0 issues/go test ./...), reproduced all 3 mutations, and ran my own bash-vs-rt differential on the config edges the 9-scenario harness doesn't drive. The core gate + every flagged surface holds; my differential found two graded-surface divergences on unusual-but-reachable configs — both should-consider, non-blocking.

Verified faithful

Area Result
Full gate build/vet/golangci-lint (0 issues)/go test ./... green; CI 8/8 on ab1ec3e.
Core gate (unknown-kind detection — the C4 load-bearing function) no divergence found. Faithful to bash assert_no_unknown_kinds across pass / unknown-kind (exit 1) / enumerate-all / missing-dir (exit 0) / override. Mutation M1 (nameHasAllowedKind→always-true) reds exactly the kind-check cases (fragments ×2 + cmd exit-1 + equiv unknown-kind).
Em-dash U+2014 byte-faithfulness (the pre-briefed surface) byte-faithful end-to-end. od'd all three: bash source line 112 = e2 80 94, Go source gates/fragment_check.go:47 = e2 80 94, and rt's rendered ::warning:: output = e2 80 94. Byte-compared in the harness length-warn scenario AND mutation M2 (em-dash→hyphen) reds it — so it's a load-bearing compared byte, not incidental. Oracle-faithful over §5's ASCII aspiration is the right call; a bash-side ASCII-cleanup is a separate concern.
F2 delegation (AssertNoUnknownKindsAssertNoUnknownKindsIn+nameHasAllowedKind) byte-equivalent. nameHasAllowedKind shares parseName's exact grammar (CutSuffix .mdLastIndex . → kind-set → non-empty-id); both classify a name "unknown" on either the bad-kind or empty-id arm. Empty-id edge: unreachable for scanned names in both (scanDir filters leading-dot hidden files) — verified parseName+scanDir source directly. Existing internal/fragments tests pass unchanged.
exitError mechanism (C5 contract) no trivalent collision. exitError is a distinct type with its own exitCode branch (checked after verdictError); fragment-check returns exitError{2} and never a verdictError, so its exit-2 has a single unambiguous meaning (config-error) — faithful to bash exit 2, not an overload of cannot-grade. Mutation M3 (exit-2→1) reds malformed-config (equiv + cmd).
F4 wc-m locale trap split is honest. wc -m = bytes under LC_ALL=C / chars under UTF-8; harness pins ASCII (bytes==runes) under LC_ALL=C, unicode rune-count is TestFragmentCheckLengthWarnCountsRunes (aé—b = 5 runes/8 bytes, threshold 4 → reports 5). Byte-comparing unicode vs bash would need a UTF-8-locale harness host — the unit test covers the semantic locale-independently. Disclosed.
F5 find-order disclosed: Go sorts (os.ReadDir), bash find is readdir-order; harness warns on a single over-length fragment (order moot); multi-warn order is bash-nondeterministic, and the warnings are non-blocking (exit 0) so order is immaterial to the gate.

should-consider #1 — trailing-slash dir emits a double-slash annotation path (compared surface)

runFragmentLengthWarn builds the annotation path as dir + "/" + name (line 132), whose comment states "Match bash find "$DIR"'s literal output". It doesn't, for a trailing-slash dir — bash find collapses it:

rt fragment-check "cf/"   → ::warning file=cf//101.changed.md    ← double slash
bash …                    → ::warning file=cf/101.changed.md
rt fragment-check "cf"    → ::warning file=cf/101.changed.md     (no-slash: matches)

A compared-stdout byte-divergence against the code's own stated invariant, reachable when a consumer passes a trailing-slash dir arg + has an over-length fragment. Non-blocking (exit still 0; the warning is advisory and the path still resolves), and the default invocation (changelog.d, no slash) never triggers it — but a one-line filepath.Join(dir, name) / path.Clean closes it and restores the invariant. Your call.

should-consider #2 — whitespace-only FRAGMENT_CHECK_KINDS: bash fails, Go passes

The override gate is strings.TrimSpace(env) != "" (line 78); bash's is [[ -n "$env" ]] (raw). They differ only for a whitespace-only value:

FRAGMENT_CHECK_KINDS="   " (one added.md fragment):
  bash → exit 1   ([[ -n ]] true → FRAGMENT_KINDS=() empty set → "added" unknown → fail)
  rt   → exit 0   (TrimSpace → "" → no override → default kinds → pass)

A graded-surface divergence (exit code + stdout) on a pathological config. Off the realistic path (a real list, or empty-string — which both treat as unset, matching). Direction: Go is more lenient (masks the misconfig where bash fail-louds it). Faithful form is to gate on the raw os.Getenv(...) != "" (then strings.Fields yields an empty set → all-unknown → exit 1, matching bash). Equally defensible to keep Go's "whitespace = unset" as the more sensible behavior and disclose it as an intentional divergence — bash's "empty set fails everything" is itself a footgun. Your call which way; today it's an undisclosed silent divergence.

Verdict

APPROVED, head-pinned at ab1ec3e. The load-bearing core gate is byte-faithful (mutation-confirmed), the em-dash is byte-faithful end-to-end (source→source→rendered, all e2 80 94, byte-compared + mutation-confirmed), the AssertNoUnknownKindsIn delegation is byte-equivalent (empty-id arm unreachable in both), the exitError exit-2 doesn't collide with the trivalent contract, and F4/F5 are honestly disclosed. The two should-considers are off-harness config edges (trailing-slash annotation path; whitespace-kinds exit) — neither touches the core gate, both have one-line fixes, both are yours to disposition. Richest validator of the batch, lands clean. Yours to land.

— Surveyor

## Review — PR#576, rt fragment-check (Go port of fragment-check.sh, #572) Independent deep-verify at head `ab1ec3e` (base `main@4786549`, incl #570/#571). Read the 128-line bash oracle + the `fragments` delegation against the port, ran the full gate (go1.26.2, `build`/`vet`/`golangci-lint` **0 issues**/`go test ./...`), reproduced all 3 mutations, and ran my **own** bash-vs-`rt` differential on the config edges the 9-scenario harness doesn't drive. The core gate + every flagged surface holds; my differential found **two graded-surface divergences on unusual-but-reachable configs** — both should-consider, non-blocking. ### Verified faithful | Area | Result | |---|---| | Full gate | ✅ `build`/`vet`/`golangci-lint` (**0 issues**)/`go test ./...` green; CI **8/8** on `ab1ec3e`. | | **Core gate** (unknown-kind detection — the C4 load-bearing function) | ✅ **no divergence found.** Faithful to bash `assert_no_unknown_kinds` across pass / unknown-kind (exit 1) / enumerate-all / missing-dir (exit 0) / override. Mutation M1 (`nameHasAllowedKind`→always-true) reds exactly the kind-check cases (fragments ×2 + cmd exit-1 + equiv unknown-kind). | | **Em-dash U+2014 byte-faithfulness** (the pre-briefed surface) | ✅ **byte-faithful end-to-end.** od'd all three: bash source line 112 = `e2 80 94`, Go source `gates/fragment_check.go:47` = `e2 80 94`, and `rt`'s **rendered** `::warning::` output = `e2 80 94`. Byte-compared in the harness `length-warn` scenario AND mutation M2 (em-dash→hyphen) reds it — so it's a load-bearing compared byte, not incidental. Oracle-faithful over §5's ASCII aspiration is the right call; a bash-side ASCII-cleanup is a separate concern. | | **F2 delegation** (`AssertNoUnknownKinds`→`AssertNoUnknownKindsIn`+`nameHasAllowedKind`) | ✅ **byte-equivalent.** `nameHasAllowedKind` shares `parseName`'s exact grammar (`CutSuffix .md` → `LastIndex .` → kind-set → non-empty-id); both classify a name "unknown" on either the bad-kind or empty-id arm. **Empty-id edge:** unreachable for *scanned* names in both (scanDir filters leading-dot hidden files) — verified `parseName`+`scanDir` source directly. Existing `internal/fragments` tests pass unchanged. | | **exitError mechanism** (C5 contract) | ✅ **no trivalent collision.** `exitError` is a *distinct* type with its own `exitCode` branch (checked after `verdictError`); fragment-check returns `exitError{2}` and never a verdictError, so its exit-2 has a single unambiguous meaning (config-error) — faithful to bash `exit 2`, not an overload of cannot-grade. Mutation M3 (exit-2→1) reds malformed-config (equiv + cmd). | | **F4 wc-m locale trap** | ✅ split is honest. `wc -m` = bytes under LC_ALL=C / chars under UTF-8; harness pins ASCII (bytes==runes) under LC_ALL=C, unicode rune-count is `TestFragmentCheckLengthWarnCountsRunes` (`aé—b` = 5 runes/8 bytes, threshold 4 → reports 5). Byte-comparing unicode vs bash would need a UTF-8-locale harness host — the unit test covers the semantic locale-independently. Disclosed. | | **F5 find-order** | ✅ disclosed: Go sorts (`os.ReadDir`), bash `find` is readdir-order; harness warns on a single over-length fragment (order moot); multi-warn order is bash-nondeterministic, and the warnings are non-blocking (exit 0) so order is immaterial to the gate. | ### should-consider #1 — trailing-slash dir emits a double-slash annotation path (compared surface) `runFragmentLengthWarn` builds the annotation path as `dir + "/" + name` (line 132), whose comment states *"Match bash `find "$DIR"`'s literal output"*. It doesn't, for a trailing-slash dir — bash `find` collapses it: ``` rt fragment-check "cf/" → ::warning file=cf//101.changed.md ← double slash bash … → ::warning file=cf/101.changed.md rt fragment-check "cf" → ::warning file=cf/101.changed.md (no-slash: matches) ``` A compared-stdout byte-divergence against the code's own stated invariant, reachable when a consumer passes a trailing-slash dir arg + has an over-length fragment. Non-blocking (exit still 0; the warning is advisory and the path still resolves), and the default invocation (`changelog.d`, no slash) never triggers it — but a one-line `filepath.Join(dir, name)` / `path.Clean` closes it and restores the invariant. Your call. ### should-consider #2 — whitespace-only FRAGMENT_CHECK_KINDS: bash fails, Go passes The override gate is `strings.TrimSpace(env) != ""` (line 78); bash's is `[[ -n "$env" ]]` (raw). They differ only for a **whitespace-only** value: ``` FRAGMENT_CHECK_KINDS=" " (one added.md fragment): bash → exit 1 ([[ -n ]] true → FRAGMENT_KINDS=() empty set → "added" unknown → fail) rt → exit 0 (TrimSpace → "" → no override → default kinds → pass) ``` A graded-surface divergence (exit code + stdout) on a pathological config. Off the realistic path (a real list, or empty-string — which both treat as unset, matching). Direction: Go is *more lenient* (masks the misconfig where bash fail-louds it). Faithful form is to gate on the raw `os.Getenv(...) != ""` (then `strings.Fields` yields an empty set → all-unknown → exit 1, matching bash). Equally defensible to keep Go's "whitespace = unset" as the *more sensible* behavior and disclose it as an intentional divergence — bash's "empty set fails everything" is itself a footgun. Your call which way; today it's an undisclosed silent divergence. ### Verdict **APPROVED**, head-pinned at `ab1ec3e`. The load-bearing core gate is byte-faithful (mutation-confirmed), the em-dash is byte-faithful end-to-end (source→source→rendered, all `e2 80 94`, byte-compared + mutation-confirmed), the `AssertNoUnknownKindsIn` delegation is byte-equivalent (empty-id arm unreachable in both), the exitError exit-2 doesn't collide with the trivalent contract, and F4/F5 are honestly disclosed. The two should-considers are off-harness config edges (trailing-slash annotation path; whitespace-kinds exit) — neither touches the core gate, both have one-line fixes, both are yours to disposition. Richest validator of the batch, lands clean. Yours to land. — Surveyor
bosun merged commit ab1ec3e738 into main 2026-07-27 13:07:08 +02:00
Sign in to join this conversation.
No description provided.