feat(rt-fragment-check): port fragment-check.sh → rt fragment-check #576
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!576
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/572-rt-fragment-check"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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 achangelog.dfragment 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 malformedLENGTH_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_kindsexists (internal/fragments, Phase 5), but theFRAGMENT_CHECK_KINDSoverride (#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 #505Reader(likeAggregateBump).AssertNoUnknownKindsdelegates with the defaultKinds();fragment-checkpasses 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_CHARSvalidation (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 trivalentVerdict(F1: exit 2 here is a config error, not cannot-grade).Ratified forks (#572)
internal/fragmentsover reimplementation — additive package-levelAssertNoUnknownKindsIn, second Phase-5-substrate refinement this arc (aftermanifest.Storeat #558).=0off-switch, hidden-file exclude,::-injection filename sanitize,::warning→stdout.wc -mlocale trap: Goutf8.RuneCountInString= chars = bashwc -munder UTF-8, butwc -munderLC_ALL=Ccounts bytes. Harness pins ASCII length-warn (chars==bytes, locale-agnostic) underLC_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).os.ReadDiris a strengthening over bash's nondeterministicfindREADDIR order; the harness warns on a single fragment (order moot).fragments.sh, notevents.sh).Verification
internal/fragmentsTestAssertNoUnknownKindsIn— default + custom set, delegation, hidden/non-md skip.internal/gatesTestFragmentLengthWarn— threshold, safe/unsafe filename, the em-dash byte.cmd/rtTestFragmentCheckEquivalence— prebuilt binary vs the real bash script over 9 scenarios: pass, unknown-kind (exit 1), missing-dir, custom-dir,FRAGMENT_CHECK_KINDSoverride pass+fail, ASCII length-warn (::warning::on stdout), length-warn off-switch, malformed-config (exit 2, pass line still on stdout).cmd/rtTestFragmentCheckExitCodes/TestFragmentCheckOverride/TestFragmentCheckLengthWarnCountsRunes— the 3-state exits (incl. theexitErrorexit-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.Full gate green on host +
forgejo-ci-go:latest(go1.26.2):golangci-lint(cache-clean) 0 issues,go build,go vet, wholego test ./...,gofmt -lclean,shellcheckon the oracle wrapper clean.What this PR does NOT do
internal/verdictwiring — the gate is a 3-state bespoke exit, not trivalent (F1); the exit-2 ridesexitError, notverdictError{CannotGrade}.wc -munderLC_ALL=Cis bytes, so the rune-count equivalence is by-construction + unit-tested, not byte-compared under the harness's pinned locale (F4).findis READDIR-order (nondeterministic); Go sorts (a strengthening). The harness warns on a single fragment (F5).Review — PR#576, rt fragment-check (Go port of fragment-check.sh, #572)
Independent deep-verify at head
ab1ec3e(basemain@4786549, incl #570/#571). Read the 128-line bash oracle + thefragmentsdelegation against the port, ran the full gate (go1.26.2,build/vet/golangci-lint0 issues/go test ./...), reproduced all 3 mutations, and ran my own bash-vs-rtdifferential 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
build/vet/golangci-lint(0 issues)/go test ./...green; CI 8/8 onab1ec3e.assert_no_unknown_kindsacross 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).e2 80 94, Go sourcegates/fragment_check.go:47=e2 80 94, andrt's rendered::warning::output =e2 80 94. Byte-compared in the harnesslength-warnscenario 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.AssertNoUnknownKinds→AssertNoUnknownKindsIn+nameHasAllowedKind)nameHasAllowedKindsharesparseName'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) — verifiedparseName+scanDirsource directly. Existinginternal/fragmentstests pass unchanged.exitErroris a distinct type with its ownexitCodebranch (checked afterverdictError); fragment-check returnsexitError{2}and never a verdictError, so its exit-2 has a single unambiguous meaning (config-error) — faithful to bashexit 2, not an overload of cannot-grade. Mutation M3 (exit-2→1) reds malformed-config (equiv + cmd).wc -m= bytes under LC_ALL=C / chars under UTF-8; harness pins ASCII (bytes==runes) under LC_ALL=C, unicode rune-count isTestFragmentCheckLengthWarnCountsRunes(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.os.ReadDir), bashfindis 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)
runFragmentLengthWarnbuilds the annotation path asdir + "/" + name(line 132), whose comment states "Match bashfind "$DIR"'s literal output". It doesn't, for a trailing-slash dir — bashfindcollapses it: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-linefilepath.Join(dir, name)/path.Cleancloses 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: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(...) != ""(thenstrings.Fieldsyields 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, alle2 80 94, byte-compared + mutation-confirmed), theAssertNoUnknownKindsIndelegation 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