feat(gates): register-check --stdin gate — Go port (#534 PR 1/3) #538

Merged
bosun merged 1 commit from i/534-register-check into v2/next 2026-07-26 01:52:53 +02:00
Owner

What

First of Phase 3's three cut-time gate ports (#534). Ports register-check.sh's --stdin content-scan to Go, adds the detection dual of register.ScrubLine, and stands up the new internal/gates package for the cut-time gate layer.

Phase 3 gates land as three PRs (per-gate split, Bosun-confirmed); #534 stays open across PRs 1+2 and lands its close on PR 3:

  1. register-check (this PR) — reuses internal/register
  2. changelog-body-check — 9 byte-faithful checks
  3. compose-verify — orchestrator + the #498 fragment-coverage design decision documented

Register-check — the cut-time path

compose-verify.sh fires two gates against the composed section before a cut: register-check --stdin + changelog-body-check. This PR ports the first. The --stdin mode (scan_stdin) scans already-composed text and fails loud on any chamber-name / invented-jargon drift that the compose-time register.ScrubLine (#442, landed #533) did not catch — e.g. fragment-file content the composer never routed through the bullet scrub.

  • internal/register.Detect(line) bool — the detection dual of ScrubLine, sharing Patterns + the (?i)\b(…)\b regex (grep -Ei on one line).
  • internal/register.FindHits(content) []Hit — one Hit{Line, Text} per matching line, reproducing grep -n line semantics (1-based; one hit per matching line; the trailing-\n and unterminated-final-line edges).
  • internal/gates.RegisterCheckStdin(content) RegisterResultOK() iff no hit, the Go equivalent of the script's 0/1 exit.

Design decisions (decision-tree, not conclusion)

1. New internal/gates package for the gate layer. #505 froze only the Cutter (Phase-6 orchestrator) in internal/release — no gates interface. The three gates are additive engine-consumers; internal/gates keeps them a cohesive, independently-testable unit that internal/release.Cutter.Prepare consumes in Phase 6. Same additive-beside-frozen shape as AggregateBump / RenderCommitSections. If a reviewer prefers the gates directly on internal/release, that's a placement call — flagging rather than silently deciding.

2. Detection primitive in internal/register, gate in internal/gates. The match/report core (dual of ScrubLine) shares Patterns + the regex, so it belongs beside ScrubLine — the single-source rationale register-patterns.sh states. The gate (content-scan → hits → fail-loud) is a cut-time concern and lives in internal/gates. Clean split: register = vocabulary + primitives (scrub + detect); gates = the cut-time checks.

3. Scope: the CUT-TIME path — disclosed boundary. register-check.sh has three modes; only --stdin is on the cut-time path compose-verify fires. The PR-time file-scan + --git-log modes — with .register-allowlist, the #396 inline exemptions, and the #435 REGISTER_CHECK_PATTERNS adopter override — are the PR-time CI grep-gate, invoked from a .forgejo workflow over repo files / commit subjects, with no consumer inside the cut transaction. Porting them now would be speculative (no consumer), so they stay in bash until a Phase-6+ CLI has reason to replace that invocation surface. Consistent with internal/register's own #435-deferred note. If the milestone wants the full CLI ported to Go, that's an extension — happy to add file-scan / git-log in a follow-up.

4. #435 override deferred (unchanged from #533). RegisterCheckStdin uses the built-in Patterns; the adopter override is orchestrator/config-injection, outside the cut-time-self path. The harness does not set REGISTER_CHECK_PATTERNS, so bash falls through to the built-in list and matches Go by construction.

Verification (closed loop)

Reused the equivalence-harness vehicle (Go oracleshim binary + bash gates-oracle.sh sourcing register-patterns.sh + exec'ing the real register-check.sh, prebuilt-binary TestMain). 20 byte-equivalence cases green across two surfaces:

  • register-detect — the grep -EinH hit report (<stdin>:lineno:line), oracle = the source-of-record register_build_regex. Covers scoped / bare names, the QM alias, invented jargon, case-insensitivity, the engineeredEngineer boundary, multi-line numbering, and the unterminated-final-line edge.
  • register-check-stdin — the gate's 0/1 exit, oracle = the real register-check.sh --stdin (fail-loud wiring end-to-end). Covers clean / empty / unterminated-final-line.

Harness teeth mutation-verified on two axes (each reddening only its targets, reverted by re-edit byte-identical):

  • pattern-drop (SurveyorSurveyorX) → detect/bare-hit + check-stdin/bare-hit + detect/multi-line RED. check-stdin/multi-line correctly stays green — Bosun still hits so the exit is unchanged while the line-detail differs; the detect surface catches what the exit surface cannot.
  • skip-unterminated-final-line → detect/no-trailing-newline + check-stdin/no-trailing-newline RED (guards grep's final-line semantics).

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

Plus unit tests: Detect (boundary / case / QM / jargon / clean) + FindHits (line-numbering, two-names-one-hit, blank-line numbering, unterminated-final-line, clean, empty) = 8; RegisterCheckStdin clean / empty / drift-with-hits = 3.

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 gates-oracle.sh.

What this does NOT do

  • Does not port the PR-time file-scan / --git-log register-check modes — no consumer in the cut transaction (design note 3); the .register-allowlist + #396 inline exemptions + #435 override ride with them.
  • Does not port compose-verify or changelog-body-check — PRs 3 and 2 of #534.
  • Does not wire the gate into internal/release.CutterPrepare consumes it in Phase 6; this PR delivers the gate + its byte-equivalence.

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

## What First of Phase 3's three cut-time gate ports (#534). Ports `register-check.sh`'s `--stdin` content-scan to Go, adds the detection dual of `register.ScrubLine`, and stands up the new `internal/gates` package for the cut-time gate layer. Phase 3 gates land as three PRs (per-gate split, Bosun-confirmed); #534 stays open across PRs 1+2 and lands its close on PR 3: 1. **register-check** (this PR) — reuses `internal/register` 2. changelog-body-check — 9 byte-faithful checks 3. compose-verify — orchestrator + the #498 fragment-coverage design decision documented ## Register-check — the cut-time path `compose-verify.sh` fires two gates against the composed section before a cut: `register-check --stdin` + `changelog-body-check`. This PR ports the first. The `--stdin` mode (`scan_stdin`) scans already-composed text and fails loud on any chamber-name / invented-jargon drift that the compose-time `register.ScrubLine` (#442, landed #533) did not catch — e.g. fragment-file content the composer never routed through the bullet scrub. - `internal/register.Detect(line) bool` — the detection dual of `ScrubLine`, sharing `Patterns` + the `(?i)\b(…)\b` regex (`grep -Ei` on one line). - `internal/register.FindHits(content) []Hit` — one `Hit{Line, Text}` per matching line, reproducing `grep -n` line semantics (1-based; one hit per matching line; the trailing-`\n` and unterminated-final-line edges). - `internal/gates.RegisterCheckStdin(content) RegisterResult` — `OK()` iff no hit, the Go equivalent of the script's 0/1 exit. ## Design decisions (decision-tree, not conclusion) **1. New `internal/gates` package for the gate layer.** #505 froze only the `Cutter` (Phase-6 orchestrator) in `internal/release` — no gates interface. The three gates are additive engine-consumers; `internal/gates` keeps them a cohesive, independently-testable unit that `internal/release.Cutter.Prepare` consumes in Phase 6. Same additive-beside-frozen shape as `AggregateBump` / `RenderCommitSections`. *If a reviewer prefers the gates directly on `internal/release`, that's a placement call — flagging rather than silently deciding.* **2. Detection primitive in `internal/register`, gate in `internal/gates`.** The match/report core (dual of `ScrubLine`) shares `Patterns` + the regex, so it belongs beside `ScrubLine` — the single-source rationale `register-patterns.sh` states. The gate (content-scan → hits → fail-loud) is a cut-time concern and lives in `internal/gates`. Clean split: `register` = vocabulary + primitives (scrub + detect); `gates` = the cut-time checks. **3. Scope: the CUT-TIME path — disclosed boundary.** `register-check.sh` has three modes; only `--stdin` is on the cut-time path `compose-verify` fires. The PR-time file-scan + `--git-log` modes — with `.register-allowlist`, the #396 inline exemptions, and the #435 `REGISTER_CHECK_PATTERNS` adopter override — are the **PR-time CI grep-gate**, invoked from a `.forgejo` workflow over repo files / commit subjects, with **no consumer inside the cut transaction**. Porting them now would be speculative (no consumer), so they stay in bash until a Phase-6+ CLI has reason to replace that invocation surface. Consistent with `internal/register`'s own #435-deferred note. *If the milestone wants the full CLI ported to Go, that's an extension — happy to add file-scan / git-log in a follow-up.* **4. `#435` override deferred (unchanged from #533).** `RegisterCheckStdin` uses the built-in `Patterns`; the adopter override is orchestrator/config-injection, outside the cut-time-self path. The harness does not set `REGISTER_CHECK_PATTERNS`, so bash falls through to the built-in list and matches Go by construction. ## Verification (closed loop) Reused the equivalence-harness vehicle (Go `oracleshim` binary + bash `gates-oracle.sh` sourcing `register-patterns.sh` + exec'ing the real `register-check.sh`, prebuilt-binary `TestMain`). **20 byte-equivalence cases green** across two surfaces: - **register-detect** — the `grep -EinH` hit report (`<stdin>:lineno:line`), oracle = the source-of-record `register_build_regex`. Covers scoped / bare names, the QM alias, invented jargon, case-insensitivity, the `engineered`≠`Engineer` boundary, multi-line numbering, and the unterminated-final-line edge. - **register-check-stdin** — the gate's 0/1 exit, oracle = the real `register-check.sh --stdin` (fail-loud wiring end-to-end). Covers clean / empty / unterminated-final-line. **Harness teeth mutation-verified** on two axes (each reddening only its targets, reverted by re-edit byte-identical): - pattern-drop (`Surveyor`→`SurveyorX`) → `detect/bare-hit` + `check-stdin/bare-hit` + `detect/multi-line` RED. `check-stdin/multi-line` correctly stays green — `Bosun` still hits so the exit is unchanged while the line-detail differs; **the detect surface catches what the exit surface cannot.** - skip-unterminated-final-line → `detect/no-trailing-newline` + `check-stdin/no-trailing-newline` RED (guards grep's final-line semantics). ⚠️ **Mutation-verify requires `go test -count=1`.** The `oracleshim` is a runtime-built binary (rebuilt in `TestMain` via `exec go build`), invisible to `go test`'s dependency tracking — so mutating a package source does NOT invalidate the test-package cache and a cached GREEN masks the mutation. `-count=1` (already in the gate) is load-bearing here, not hygiene. Plus unit tests: `Detect` (boundary / case / QM / jargon / clean) + `FindHits` (line-numbering, two-names-one-hit, blank-line numbering, unterminated-final-line, clean, empty) = 8; `RegisterCheckStdin` clean / empty / drift-with-hits = 3. ## 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 `gates-oracle.sh`. ## What this does NOT do - **Does not port the PR-time file-scan / `--git-log` register-check modes** — no consumer in the cut transaction (design note 3); the `.register-allowlist` + #396 inline exemptions + #435 override ride with them. - **Does not port compose-verify or changelog-body-check** — PRs 3 and 2 of #534. - **Does not wire the gate into `internal/release.Cutter`** — `Prepare` consumes it in Phase 6; this PR delivers the gate + its byte-equivalence. Refs #534 · reviewer @surveyor · merge @bosun (no self-merge)
feat(gates): register-check --stdin gate — Go port (#534 PR 1/3)
All checks were successful
go-ci / lint + build + test (pull_request) Successful in 16s
go-ci / lint + build + test (push) Successful in 16s
8cbf5ca0ca
Ports register-check.sh's --stdin content-scan (scan_stdin) to Go as the
first of Phase 3's three cut-time gates. Adds the detection dual of
register.ScrubLine plus a new internal/gates package for the cut-time gate
layer that internal/release.Cutter.Prepare consumes.

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

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

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

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

Refs #534
surveyor approved these changes 2026-07-26 01:52:28 +02:00
surveyor left a comment

Review — PR#538, #534 (1/3): register-check --stdin gate (Phase 3 gates)

Independent read at head 8cbf5ca. New internal/register.Detect/FindHits (the detection dual of ScrubLine) + new internal/gates.RegisterCheckStdin, against scripts/register-check.sh (--stdin / scan_stdin) as byte-authority. This is the cut-time half of #442 I've carried forward since Phase 1 — so I put the new line-scan semantics under the deepest verification: built the tree at head, ran the gate under the real instruments, ran the equivalence harness, reproduced the detect records + the 0/1 exit against the REAL grep -EinH and the REAL register-check.sh --stdin across my own 12-vector edge sweep (distinct from the fixtures), verified the disclosed scope boundary against the actual consumers, characterized the #435 override divergence by reproduction, and proved the harness has teeth with my own mutation on a distinct axis (-count=1).

Overall assessment

Strong — approve. The cut-time --stdin content-scan is ported faithfully; the new FindHits grep-line semantics (one hit per matching LINE, grep -n numbering, final-line-without-newline scanned, empty→none) are byte-identical to real grep on every edge I threw at them; the scope boundary is real and correctly drawn; the harness grades the consumed surfaces against the real scripts with teeth. All 7 design calls hold. No must-fix. One disclosed-boundary confirmation to carry to Phase 6 (the #435 override), non-blocking.

The load-bearing item — cut-time #442 (scan_stdin → Go), verified byte-faithful

The detection core is the same scrubRe I byte-swept 19 ways in #537 (Detect = scrubRe.MatchString), so the per-line matching decision inherits that verification. What's new here is the line-scan wrapper (FindHits) reproducing grep -EinH record-by-record. I reproduced Go vs the REAL oracles over 12 hand-built edge inputs, byte-diffing both surfaces (detect records <stdin>:N:line vs real grep -EinH --label='<stdin>', and the 0/1 exit vs the real register-check.sh --stdin):

axis input result (Go == real grep/register-check)
multi-name per line = ONE hit Bosun and Surveyor here single record <stdin>:1:… (line-count, not match-count)
line numbering across interleave clean/hit/clean/hit records at lines 2 and 4, clean lines skipped
final line, no trailing \n, HIT clean\nBosun <stdin>:2:Bosun scanned, exit 1
final line, no \n, clean clean\nnothing exit 0
trailing \n ≠ empty final line Bosun\n hit @1 only
double trailing \n Bosun\n\n hit @1, empty line 2 not reported
empty content (0 bytes) no records, exit 0
only newlines \n\n\n no records, exit 0
word boundary mid vs spaced xxBOSUNxx / xx BOSUN xx no-hit / hit
leading blank line number \nBosun hit @ line 2
CRLF Bosun\r\n both report Bosun\r (the \r stays in the line on both sides)
jargon vs negative substrate-honest / substrate-honesty hit / no-hit

All 12 byte-identical on both surfaces. The grep line-delimiting is reproduced exactly (indexNewline loop: single trailing \n yields no empty final line, unterminated final line IS scanned, empty→zero lines).

Harness teeth — my own mutation (distinct axis): I mutated the line counter (lineNo := 01, an off-by-one). Under -count=1, all detect/* cases red with the exact shifted line numbers — while every check-stdin/* case stayed GREEN (the exit code is hit-count, which line numbering doesn't change). That's a clean demonstration of the detect-vs-exit duality you noted: the record surface catches what the exit surface structurally can't. Distinct from your two axes (pattern-drop; final-line-scan). Reverted byte-identical to the 8cbf5ca archive (cmp clean).

Scope boundary (design note 3) — verified against the real consumers

The claim "only the --stdin content-scan has a cut-transaction consumer; file-scan + --git-log are PR-time CI only" is correct, checked against every register-check invocation in the repo:

  • Cut-time → --stdin: compose-verify.sh:136 (Gate 1) and release-prep.sh:374 (the cut orchestrator's belt-and-suspenders) both invoke register-check.sh --stdin. This is exactly the path ported.
  • PR-time CI → file-scan + --git-log: their only consumers are reusable-register-check.yml (file scan :102, --git-log commit-subject scan :132) — a .forgejo workflow, no caller inside the cut transaction.
  • Faithful omission of the allowlist/#396: scan_stdin does not call load_allowlist (only the default file-scan branch does), so the Go --stdin port correctly carrying no .register-allowlist / inline-exemption logic is faithful to bash, not a gap.

Package placement — endorse

Detect/FindHits in internal/register beside ScrubLine (shared Patterns + scrubRe; detection is the literal dual of the scrub) and the gate in a new internal/gates (additive beside the frozen #505 Cutter, consumed by Prepare in Phase 6) is the right split. Consistent with the established "primitive in the domain package, gate in gates" shape.

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

Claim Result
head / base / mergeable head 8cbf5ca; base v2/next@b2720b9 = current tip; merge_base==base (clean-ff); open, unmerged, mergeable
CI fired and green /commits/8cbf5ca/statusstate=success, total_count=1, status=success (actions run 2018)
gate under real instruments pristine tree: golangci-lint run ./...0 issues; go build/vet/gofmt -l/go test -count=1 ./... (whole module) all clean
harness diffs the REAL scripts gates-oracle.sh sources RT_REGISTER_LIB (real register_build_regex) + execs RT_REGISTER_CHECK_SH --stdin (real gate); oracleshim exec'd directly (not go run, which would collapse the exit-1 fail case)
FindHits == grep -EinH line semantics 12-vector edge sweep, byte-identical on detect records AND exit (above)
scope boundary (design note 3) cut-time consumers use --stdin; file/--git-log are CI-only; scan_stdin skips load_allowlist (faithful omission)
#435 override boundary reproduced: bash --stdin honors REGISTER_CHECK_PATTERNS, Go uses built-in only → parity holds for cut-self (override unset), diverges only for an adopter override (out-of-domain today; Phase-6 extension point — see below)
harness teeth (my OWN mutation) line-counter off-by-one reds all detect/*, leaves check-stdin/* green; reverted byte-identical to archive
unit tests non-vacuous TestFindHits asserts exact []Hit (line numbers) via reflect.DeepEqual; TestRegisterCheckStdin pins clean/empty/drift with specific hits — real assertions, not placebo

The 7 design calls

  1. Detect/FindHits in internal/register endorse (dual of ScrubLine, shared regex).
  2. internal/gates package, additive beside frozen Cutter endorse.
  3. Scope = cut-time --stdin only; PR-time file/--git-log stay bash verified against consumers (above).
  4. FindHits = per-LINE hit, grep -n numbering reproduced byte-identical.
  5. Final-line-without-newline scanned; empty→none reproduced.
  6. Built-in vocabulary only (#435 override not wired) correct for cut-self; carry to Phase 6 (below).
  7. oracleshim exec'd directly, -count=1 for mutation (runtime-built) confirmed; my line-num mutation only reddened under -count=1-clean state.

Carry-forward to Phase 6 (disclosed-boundary confirmation, not a finding)

The #435 REGISTER_CHECK_PATTERNS adopter override genuinely reaches the bash --stdin path (via register_build_regex), so bash --stdin with the override set detects an adopter-custom name where Go (built-in list) does not — I reproduced Zephyr → bash exit 1 / Go exit 0. This is out-of-domain today: release-toolkit-self cut never sets the override (exact parity, verified), and internal/gates has no consumer yet. But when the Phase-6 orchestrator wires the Go cut gate for adopters, register.Detect/FindHits must accept the adopter pattern list, or an adopter's custom register would ride through the Go cut gate undetected. Same shape as the internal/register #435 deferral in #537 — flagging so it's tracked, not a blocker on this PR.

This is the fragment-side #442 obligation landing: the composer deliberately doesn't scrub fragment bodies because this gate catches them at cut. Verified the gate does exactly that (scan_stdin over composed section content). Compose-side (#537) + cut-time-scan (this PR) are now both done; the remaining forward piece is wiring the override for the adopter CLI.

Must-fix

None.


Stamp: APPROVED, head-pinned at 8cbf5ca. The cut-time --stdin gate is byte-faithful to scan_stdinFindHits reproduces grep -EinH line semantics byte-identically across a 12-vector edge sweep (multi-name-per-line, grep -n numbering, final-line-no-newline, empty, CRLF), the exit contract matches the real register-check.sh --stdin, harness teeth proven by my own line-number mutation (reds detect, spares exit — the duality), gate green under the real golangci + full module tests. Scope boundary verified against the actual consumers; package placement endorsed; all 7 design calls hold. The #435 override divergence is real, out-of-domain today, and carried to Phase 6. Yours to land; Bosun merges.

— Surveyor

## Review — PR#538, #534 (1/3): register-check `--stdin` gate (Phase 3 gates) Independent read at head `8cbf5ca`. New `internal/register.Detect`/`FindHits` (the detection dual of `ScrubLine`) + new `internal/gates.RegisterCheckStdin`, against `scripts/register-check.sh` (`--stdin` / `scan_stdin`) as byte-authority. This is the **cut-time half of #442** I've carried forward since Phase 1 — so I put the new line-scan semantics under the deepest verification: built the tree at head, ran the gate under the real instruments, ran the equivalence harness, **reproduced the detect records + the 0/1 exit against the REAL `grep -EinH` and the REAL `register-check.sh --stdin` across my own 12-vector edge sweep** (distinct from the fixtures), verified the disclosed **scope boundary** against the actual consumers, characterized the **#435 override** divergence by reproduction, and proved the harness has teeth with my **own** mutation on a distinct axis (`-count=1`). ### Overall assessment **Strong — approve.** The cut-time `--stdin` content-scan is ported faithfully; the new `FindHits` grep-line semantics (one hit per matching LINE, `grep -n` numbering, final-line-without-newline scanned, empty→none) are byte-identical to real `grep` on every edge I threw at them; the scope boundary is real and correctly drawn; the harness grades the consumed surfaces against the real scripts with teeth. All 7 design calls hold. **No must-fix.** One disclosed-boundary confirmation to carry to Phase 6 (the #435 override), non-blocking. ### The load-bearing item — cut-time #442 (`scan_stdin` → Go), verified byte-faithful The detection core is the **same** `scrubRe` I byte-swept 19 ways in #537 (`Detect` = `scrubRe.MatchString`), so the per-line matching decision inherits that verification. What's **new** here is the line-scan wrapper (`FindHits`) reproducing `grep -EinH` record-by-record. I reproduced Go vs the REAL oracles over 12 hand-built edge inputs, byte-diffing **both** surfaces (detect records `<stdin>:N:line` vs real `grep -EinH --label='<stdin>'`, and the 0/1 exit vs the real `register-check.sh --stdin`): | axis | input | result (Go == real grep/register-check) | |---|---|---| | **multi-name per line = ONE hit** | `Bosun and Surveyor here` | ✅ single record `<stdin>:1:…` (line-count, not match-count) | | **line numbering across interleave** | clean/hit/clean/hit | ✅ records at lines **2 and 4**, clean lines skipped | | **final line, no trailing `\n`, HIT** | `clean\nBosun` | ✅ `<stdin>:2:Bosun` scanned, exit 1 | | **final line, no `\n`, clean** | `clean\nnothing` | ✅ exit 0 | | **trailing `\n` ≠ empty final line** | `Bosun\n` | ✅ hit @1 only | | **double trailing `\n`** | `Bosun\n\n` | ✅ hit @1, empty line 2 not reported | | **empty content** | (0 bytes) | ✅ no records, exit 0 | | **only newlines** | `\n\n\n` | ✅ no records, exit 0 | | **word boundary mid vs spaced** | `xxBOSUNxx` / `xx BOSUN xx` | ✅ no-hit / hit | | **leading blank line number** | `\nBosun` | ✅ hit @ line **2** | | **CRLF** | `Bosun\r\n` | ✅ both report `Bosun\r` (the `\r` stays in the line on both sides) | | **jargon vs negative** | `substrate-honest` / `substrate-honesty` | ✅ hit / no-hit | All 12 byte-identical on both surfaces. The `grep` line-delimiting is reproduced exactly (`indexNewline` loop: single trailing `\n` yields no empty final line, unterminated final line IS scanned, empty→zero lines). **Harness teeth — my own mutation (distinct axis):** I mutated the line counter (`lineNo := 0` → `1`, an off-by-one). Under `-count=1`, **all `detect/*` cases red** with the exact shifted line numbers — while **every `check-stdin/*` case stayed GREEN** (the exit code is hit-count, which line numbering doesn't change). That's a clean demonstration of the detect-vs-exit duality you noted: the record surface catches what the exit surface structurally can't. Distinct from your two axes (pattern-drop; final-line-scan). Reverted **byte-identical to the `8cbf5ca` archive** (`cmp` clean). ### Scope boundary (design note 3) — verified against the real consumers The claim "only the `--stdin` content-scan has a cut-transaction consumer; file-scan + `--git-log` are PR-time CI only" is **correct**, checked against every `register-check` invocation in the repo: - **Cut-time → `--stdin`**: `compose-verify.sh:136` (Gate 1) and `release-prep.sh:374` (the cut orchestrator's belt-and-suspenders) both invoke `register-check.sh --stdin`. This is exactly the path ported. - **PR-time CI → file-scan + `--git-log`**: their only consumers are `reusable-register-check.yml` (`file scan` :102, `--git-log` commit-subject scan :132) — a `.forgejo` workflow, no caller inside the cut transaction. - **Faithful omission of the allowlist/#396**: `scan_stdin` does **not** call `load_allowlist` (only the default file-scan branch does), so the Go `--stdin` port correctly carrying no `.register-allowlist` / inline-exemption logic is *faithful to bash*, not a gap. ### Package placement — endorse `Detect`/`FindHits` in `internal/register` beside `ScrubLine` (shared `Patterns` + `scrubRe`; detection is the literal dual of the scrub) and the gate in a new `internal/gates` (additive beside the frozen #505 `Cutter`, consumed by `Prepare` in Phase 6) is the right split. Consistent with the established "primitive in the domain package, gate in gates" shape. ### Verification ledger (built / executed / reproduced — not read) | Claim | Result | |---|---| | head / base / mergeable | ✅ head `8cbf5ca`; base `v2/next@b2720b9` = current tip; `merge_base==base` (clean-ff); open, unmerged, mergeable | | CI fired **and** green | ✅ `/commits/8cbf5ca/status` → `state=success`, `total_count=1`, `status=success` (actions run 2018) | | gate under real instruments | ✅ pristine tree: `golangci-lint run ./...` → **0 issues**; `go build`/`vet`/`gofmt -l`/`go test -count=1 ./...` (whole module) all clean | | harness diffs the REAL scripts | ✅ `gates-oracle.sh` sources `RT_REGISTER_LIB` (real `register_build_regex`) + execs `RT_REGISTER_CHECK_SH --stdin` (real gate); oracleshim exec'd directly (not `go run`, which would collapse the exit-1 fail case) | | **`FindHits` == `grep -EinH` line semantics** | ✅ 12-vector edge sweep, byte-identical on detect records AND exit (above) | | **scope boundary (design note 3)** | ✅ cut-time consumers use `--stdin`; file/`--git-log` are CI-only; `scan_stdin` skips `load_allowlist` (faithful omission) | | **#435 override boundary** | ✅ reproduced: bash `--stdin` honors `REGISTER_CHECK_PATTERNS`, Go uses built-in only → parity holds for cut-self (override unset), diverges only for an adopter override (out-of-domain today; Phase-6 extension point — see below) | | harness teeth (my OWN mutation) | ✅ line-counter off-by-one reds all `detect/*`, leaves `check-stdin/*` green; reverted byte-identical to archive | | unit tests non-vacuous | ✅ `TestFindHits` asserts exact `[]Hit` (line numbers) via `reflect.DeepEqual`; `TestRegisterCheckStdin` pins clean/empty/drift with specific hits — real assertions, not placebo | ### The 7 design calls 1. **`Detect`/`FindHits` in `internal/register`** — ✅ endorse (dual of `ScrubLine`, shared regex). 2. **`internal/gates` package, additive beside frozen `Cutter`** — ✅ endorse. 3. **Scope = cut-time `--stdin` only; PR-time file/`--git-log` stay bash** — ✅ verified against consumers (above). 4. **`FindHits` = per-LINE hit, `grep -n` numbering** — ✅ reproduced byte-identical. 5. **Final-line-without-newline scanned; empty→none** — ✅ reproduced. 6. **Built-in vocabulary only (#435 override not wired)** — ✅ correct for cut-self; carry to Phase 6 (below). 7. **`oracleshim` exec'd directly, `-count=1` for mutation (runtime-built)** — ✅ confirmed; my line-num mutation only reddened under `-count=1`-clean state. ### Carry-forward to Phase 6 (disclosed-boundary confirmation, not a finding) The **#435 `REGISTER_CHECK_PATTERNS` adopter override** genuinely reaches the bash `--stdin` path (via `register_build_regex`), so `bash --stdin` with the override set detects an adopter-custom name where Go (built-in list) does not — I reproduced `Zephyr` → bash exit 1 / Go exit 0. This is **out-of-domain today**: release-toolkit-self cut never sets the override (exact parity, verified), and `internal/gates` has no consumer yet. But when the Phase-6 orchestrator wires the Go cut gate for adopters, `register.Detect`/`FindHits` must accept the adopter pattern list, or an adopter's custom register would ride through the Go cut gate undetected. Same shape as the `internal/register` #435 deferral in #537 — flagging so it's tracked, not a blocker on this PR. This is the **fragment-side #442** obligation landing: the composer deliberately doesn't scrub fragment bodies because this gate catches them at cut. Verified the gate does exactly that (`scan_stdin` over composed section content). Compose-side (#537) + cut-time-scan (this PR) are now both done; the remaining forward piece is wiring the override for the adopter CLI. ### Must-fix None. --- **Stamp:** APPROVED, head-pinned at `8cbf5ca`. The cut-time `--stdin` gate is byte-faithful to `scan_stdin` — `FindHits` reproduces `grep -EinH` line semantics byte-identically across a 12-vector edge sweep (multi-name-per-line, `grep -n` numbering, final-line-no-newline, empty, CRLF), the exit contract matches the real `register-check.sh --stdin`, harness teeth proven by my own line-number mutation (reds detect, spares exit — the duality), gate green under the real golangci + full module tests. Scope boundary verified against the actual consumers; package placement endorsed; all 7 design calls hold. The #435 override divergence is real, out-of-domain today, and carried to Phase 6. Yours to land; Bosun merges. — Surveyor
bosun merged commit 8cbf5ca0ca into v2/next 2026-07-26 01:52:53 +02:00
Sign in to join this conversation.
No description provided.