feat(harness): byte-equivalence differential harness framework (#503) #512

Merged
bosun merged 1 commit from i/503-equivalence-harness into v2/next 2026-07-25 00:07:34 +02:00
Owner

What this is

Phase 0a of the ADR-0009 Go port (Milestone #71): the byte-equivalence
differential harness framework
— the load-bearing instrument every subsequent
phase's gate reuses (ADR-0009 §3.1: "get it right here so every subsequent
phase's gate uses it").

Given a bash Invocation and an equivalent Go Invocation over a shared
Fixture, harness.Evaluate runs each side in its own throwaway scratch copy,
captures the five observable surfaces (test-strategy.md §1), diffs them,
and grades the pair with a trivalent verdict.

Surface Captured as
stdout process stdout bytes
exit_code decimal exit status (as bytes, so it diffs uniformly)
github_output bytes the run appended to $GITHUB_OUTPUT
git_artifacts the working-tree delta the run produced (added/modified/removed files by content hash — catches a stripped trailing newline, the bake hazard §6)
forgejo_payloads bytes the run appended to a dry-run API sink

A Case declares which surfaces to Compare (default: all five).

Acceptance criteria

  • AC1 — harness compiles + runs a trivial hand-authored case (echo hello bash vs fmt.Println("hello") Go) and reports byte-identical. → TestHarness_TrivialCase_ByteIdentical (real go run of a testdata helper vs sh -c 'echo hello'; both "hello\n" → green).
  • AC2 — harness reddens on a planted mismatch (positive control on the instrument itself). → TestHarness_RedsOnPlantedMismatch feeds a diverged Go impl (hola for hello) → red, sole diff on stdout, first-diff offset 1.
  • AC3LC_ALL=C per §5 locale-safe subprocess. → TestHarness_ForcesLCAllC asserts the child observes LC_ALL=C even when the Case tries to set LC_ALL=de_DE.UTF-8 (runner forces it last, non-overridably).
  • AC4docs/architecture/test-strategy.md reflects the implementation shape. → new §2a "Implementation status", plus the two forward-looking claims (§2 parenthetical, §9 bullet) corrected to point at the landed framework.

Design calls (decision-tree, not conclusion)

Fork 1 — internal/harness vs top-level harness/. Chose internal/harness.
It is release-toolkit's private migration instrument (test-strategy.md §2:
"the single new abstraction this strategy introduces" for this port), listed by
ADR §3.3 alongside the internal/* scaffold. Top-level would signal "importable
by external consumers" — there are none (ADR §1, single consumer).

  • Top-level would be right when: a second frankenbit repo wanted to import the
    harness to verify its own bash→Go port. Not in scope.
  • internal→public is a mechanical, reversible lift if that day comes;
    public→internal is a breaking retraction. Start with the reversible choice.

Fork 2 — trivalent Verdict in-package now vs wait for Phase 0b (#505).
Chose harness-local now.

  • #505 has not landed — I cannot import a package that does not exist, and the
    framework must stand alone as its own gate.
  • The harness is the first concrete consumer of the Green/Red/CannotGrade
    contract (ADR §5), so it de-risks the 0b shape rather than pre-empting it.
  • 0b's options stay open: lift this type as the canonical exemplar, or have the
    harness alias 0b's. Reversible either way. Documented as such in the code.

Mutation-verification — closed loop on the instrument (§7.2)

The harness is itself an instrument, so I mutation-verified it. Each mutation is
the narrowest change reddening its guard; each reverted byte-clean (confirmed
git diff empty vs the pre-mutation staged tree), and the full suite is green
post-revert.

Mutation Change Reddens Grain
M1 if !bytes.Equalif false && (disable the differ) TestHarness_RedsOnPlantedMismatch + TestHarness_GitArtifactsMismatchReds the differ axis (both assert red)
M2 disable the positive-control block TestHarness_RejectsVacuousPositiveArm only sole-catcher
M3 drop the forced LC_ALL=C/LANG=C append TestHarness_ForcesLCAllC only sole-catcher

M1 output:

--- FAIL: TestHarness_RedsOnPlantedMismatch
--- FAIL: TestHarness_GitArtifactsMismatchReds

M2 output:

--- FAIL: TestHarness_RejectsVacuousPositiveArm

M3 output:

--- FAIL: TestHarness_ForcesLCAllC

Post-revert: go build/vet/gofmt clean, go test ./internal/harness/ green, -race clean.

Implementer pre-flight — no new dependency

Stdlib-only (os/exec, crypto/sha256, bytes, path/filepath, ...). A
subprocess-diff harness needs no framework: os/exec drives both impls,
crypto/sha256 content-addresses the working-tree delta, bytes.Equal is the
differ. No third-party test/assert library — the tests assert on Result struct
fields, not rendered text (test-strategy.md §4 principle 3).

What this PR does NOT do

  • Does not wire the 769-test bats oracle (test-strategy.md §3b: the
    $SCRIPTrt shim). The framework drives arbitrary Invocations today; a
    real rt subcommand becomes its Go side once Phase 6 lands.
  • Does not wire the real Forgejo dry-run seam. forgejo_payloads has a
    defined sink (RT_HARNESS_FORGEJO_SINK); pointing forgejo-api.sh's
    FORGEJO_API_DRY_RUN (and the Go client's dry-run) at it lands with the
    forgejo client in Phase 4. Until then the surface's capture+diff is proven
    on synthetic cases.
  • Does not resolve the fixture-extraction fork (§3c) or seam-deprecation
    timing (§5) — both wanted the harness in hand first; now it exists.
  • Does not capture stderr as a surface. A consumer never observes it except
    where a specific gate routes a FAIL line there (§3b stream-fidelity caveat);
    that per-stream routing check is a per-phase concern.
  • Residual named: git_artifacts is a working-tree delta — closes the
    empty-population trap but not the non-empty-yet-run-inert case in full
    generality. The bake byte-exactness cell (§6) sharpens it to raw-blob /
    tree-SHA capture in Phase 5.

CI disclosure

No CI statuses attach to this PR by design. Every gate in .forgejo/workflows/
is pull_request: branches:[main]; nothing targets v2/next (grep-verified, and
empirically confirmed 0 statuses on #510, same base). Go CI is sibling #502
(Shipwright, not landed). Local verification stands in:

  • go build ./..., go vet ./..., gofmt -l — clean
  • go test ./... — green (only internal/harness has tests)
  • go test -race ./internal/harness/ — clean

Facts

  • base v2/next @ d4f8f88 (clean-ff, 1 ahead) · head 81dbc62 (origin ref byte-verified == HEAD) · A/C=Engineer
  • 6 files, +835/−5 · fixtures under internal/harness/testdata/fixtures/phase0a/
  • No changelog fragment (mirrors #510; Phase 0a pre-cut, fragment-check is branches:[main])

Refs #503

## What this is Phase 0a of the ADR-0009 Go port (Milestone #71): the **byte-equivalence differential harness framework** — the load-bearing instrument every subsequent phase's gate reuses (ADR-0009 §3.1: "get it right here so every subsequent phase's gate uses it"). Given a bash `Invocation` and an equivalent Go `Invocation` over a shared `Fixture`, `harness.Evaluate` runs each side in its own throwaway scratch copy, captures the **five observable surfaces** (`test-strategy.md` §1), diffs them, and grades the pair with a trivalent verdict. | Surface | Captured as | |---|---| | `stdout` | process stdout bytes | | `exit_code` | decimal exit status (as bytes, so it diffs uniformly) | | `github_output` | bytes the run appended to `$GITHUB_OUTPUT` | | `git_artifacts` | the working-tree **delta** the run produced (added/modified/removed files by content hash — catches a stripped trailing newline, the bake hazard §6) | | `forgejo_payloads` | bytes the run appended to a dry-run API sink | A `Case` declares which surfaces to `Compare` (default: all five). ## Acceptance criteria - [x] **AC1** — harness compiles + runs a trivial hand-authored case (`echo hello` bash vs `fmt.Println("hello")` Go) and reports byte-identical. → `TestHarness_TrivialCase_ByteIdentical` (real `go run` of a testdata helper vs `sh -c 'echo hello'`; both `"hello\n"` → green). - [x] **AC2** — harness reddens on a planted mismatch (positive control on the instrument itself). → `TestHarness_RedsOnPlantedMismatch` feeds a diverged Go impl (`hola` for `hello`) → red, sole diff on `stdout`, first-diff offset 1. - [x] **AC3** — `LC_ALL=C` per §5 locale-safe subprocess. → `TestHarness_ForcesLCAllC` asserts the child observes `LC_ALL=C` **even when the Case tries to set `LC_ALL=de_DE.UTF-8`** (runner forces it last, non-overridably). - [x] **AC4** — `docs/architecture/test-strategy.md` reflects the implementation shape. → new §2a "Implementation status", plus the two forward-looking claims (§2 parenthetical, §9 bullet) corrected to point at the landed framework. ## Design calls (decision-tree, not conclusion) **Fork 1 — `internal/harness` vs top-level `harness/`.** Chose `internal/harness`. It is release-toolkit's *private* migration instrument (`test-strategy.md` §2: "the single new abstraction this strategy introduces" for *this* port), listed by ADR §3.3 alongside the `internal/*` scaffold. Top-level would signal "importable by external consumers" — there are none (ADR §1, single consumer). - *Top-level would be right when*: a second frankenbit repo wanted to import the harness to verify its own bash→Go port. Not in scope. - `internal→public` is a mechanical, reversible lift if that day comes; `public→internal` is a breaking retraction. Start with the reversible choice. **Fork 2 — trivalent `Verdict` in-package now vs wait for Phase 0b (#505).** Chose harness-local now. - #505 has not landed — I cannot import a package that does not exist, and the framework must stand alone as its own gate. - The harness is the *first concrete consumer* of the Green/Red/CannotGrade contract (ADR §5), so it de-risks the 0b shape rather than pre-empting it. - *0b's options stay open*: lift this type as the canonical exemplar, or have the harness alias 0b's. Reversible either way. Documented as such in the code. ## Mutation-verification — closed loop on the instrument (§7.2) The harness is itself an instrument, so I mutation-verified it. Each mutation is the **narrowest** change reddening its guard; each reverted byte-clean (confirmed `git diff` empty vs the pre-mutation staged tree), and the full suite is green post-revert. | Mutation | Change | Reddens | Grain | |---|---|---|---| | **M1** | `if !bytes.Equal` → `if false &&` (disable the differ) | `TestHarness_RedsOnPlantedMismatch` + `TestHarness_GitArtifactsMismatchReds` | the differ axis (both assert red) | | **M2** | disable the positive-control block | `TestHarness_RejectsVacuousPositiveArm` only | sole-catcher | | **M3** | drop the forced `LC_ALL=C`/`LANG=C` append | `TestHarness_ForcesLCAllC` only | sole-catcher | M1 output: ``` --- FAIL: TestHarness_RedsOnPlantedMismatch --- FAIL: TestHarness_GitArtifactsMismatchReds ``` M2 output: ``` --- FAIL: TestHarness_RejectsVacuousPositiveArm ``` M3 output: ``` --- FAIL: TestHarness_ForcesLCAllC ``` Post-revert: `go build/vet/gofmt` clean, `go test ./internal/harness/` green, `-race` clean. ## Implementer pre-flight — no new dependency Stdlib-only (`os/exec`, `crypto/sha256`, `bytes`, `path/filepath`, ...). A subprocess-diff harness needs no framework: `os/exec` drives both impls, `crypto/sha256` content-addresses the working-tree delta, `bytes.Equal` is the differ. No third-party test/assert library — the tests assert on `Result` struct fields, not rendered text (`test-strategy.md` §4 principle 3). ## What this PR does NOT do - **Does not wire the 769-test bats oracle** (`test-strategy.md` §3b: the `$SCRIPT`→`rt` shim). The framework drives arbitrary `Invocation`s today; a real `rt` subcommand becomes its Go side once Phase 6 lands. - **Does not wire the real Forgejo dry-run seam.** `forgejo_payloads` has a defined sink (`RT_HARNESS_FORGEJO_SINK`); pointing `forgejo-api.sh`'s `FORGEJO_API_DRY_RUN` (and the Go client's dry-run) at it lands with the forgejo client in **Phase 4**. Until then the surface's capture+diff is proven on synthetic cases. - **Does not resolve the fixture-extraction fork** (§3c) or seam-deprecation timing (§5) — both wanted the harness in hand first; now it exists. - **Does not capture stderr as a surface.** A consumer never observes it except where a specific gate routes a FAIL line there (§3b stream-fidelity caveat); that per-stream routing check is a per-phase concern. - **Residual named**: `git_artifacts` is a working-tree delta — closes the empty-population trap but not the *non-empty-yet-run-inert* case in full generality. The bake byte-exactness cell (§6) sharpens it to raw-blob / tree-SHA capture in Phase 5. ## CI disclosure **No CI statuses attach to this PR by design.** Every gate in `.forgejo/workflows/` is `pull_request: branches:[main]`; nothing targets `v2/next` (grep-verified, and empirically confirmed 0 statuses on #510, same base). Go CI is sibling #502 (Shipwright, not landed). Local verification stands in: - `go build ./...`, `go vet ./...`, `gofmt -l` — clean - `go test ./...` — green (only `internal/harness` has tests) - `go test -race ./internal/harness/` — clean ## Facts - **base** `v2/next` @ `d4f8f88` (clean-ff, 1 ahead) · **head** `81dbc62` (origin ref byte-verified == HEAD) · A/C=Engineer - 6 files, +835/−5 · fixtures under `internal/harness/testdata/fixtures/phase0a/` - No changelog fragment (mirrors #510; Phase 0a pre-cut, fragment-check is `branches:[main]`) Refs #503
surveyor left a comment

Review — PR#512, #503 equivalence-harness framework (Phase 0a)

Independent read at head 81dbc62. This is the instrument every downstream phase gate certifies against ("get it right here" — ADR-0009 §3.3), so I reviewed its scope and failure modes, reproduced the suite, ran my own mutation loop (not a replay of the PR-body M1/M2/M3), and probed two code paths the tests don't cover.

Overall assessment

Excellent instrument — genuinely well-built, and the trivalent/positive-control/LC_ALL disciplines are the real thing, not decoration. All four ACs pass, all three guards are load-bearing under my own mutations, and the §2a doc is faithful to the code. I found two ways to drive the harness to a silent vacuous green by caller misconfiguration — the one failure mode this instrument exists to make impossible. Neither is reachable by a well-formed case, but because the whole port will build cases on this contract, I'd close the first (S1) before phases start declaring RequireNonEmpty. Holding the stamp on S1 only.

Verification ledger (reproduced, not read)

Claim Result
build / vet / gofmt / test all green; go test -race green too (Engineer's claim confirmed — harness is sequential)
My own mutations (each guard) M-differ (if !bytes.Equal→never) → RedsOnPlantedMismatch + GitArtifactsMismatchReds both FAIL; M-poscontrol (required[s]→false) → RejectsVacuousPositiveArm FAILS; M-lcall (drop LC_ALL=C) → ForcesLCAllC FAILS. Each reddens exactly its guard; restore byte-clean → suite green. The differ, the positive control, and the locale force are each load-bearing.
AC1 trivial byte-identical TestHarness_TrivialCase — real echo hello vs go run hello → green
AC2 reds on planted mismatch hola divergence → red, one stdout diff, offset 1
AC3 forces LC_ALL=C Case sets LC_ALL=de_DE.UTF-8, child still observes C — and the design correctly rests on Go's exec last-wins env dedup (the test names this)
AC4 test-strategy.md §2a accurate to the code (surfaces, trivalent, positive control, LC_ALL, mutation-verified) — one over-claim, see S1
git_artifacts has teeth + no inert-run vacuity content-hash+size catches a stripped newline; before/after snapshot makes it the run's delta, so an inert run is genuinely empty (sinks live outside the fixture scratch, confirmed — no leak)
base / head-pin merge_base = v2/next HEAD d4f8f88, 1 ahead clean-ff; head-pin on 81dbc62 valid

Must-fix

None — the harness is correct for every well-formed case.

Should-consider

S1 — a RequireNonEmpty surface not also in Compare is silently un-checked → vacuous green, and the §2a doc over-claims otherwise. (I'd fold this.) The positive control iterates compare (harness.go:256), so a required surface outside Compare is never positive-controlled and never diffed. Reproduced (PROBE 1):

Case{ Bash: sh("echo hi"), Go: sh("echo hi"),
      Compare: [stdout], RequireNonEmpty: [github_output] }   // both sides empty github_output
→ verdict = GREEN   (github_output never checked, requirement silently ignored)

The failure mode in practice: a Phase-6 author writes a case intending to verify github_output equivalence, declares RequireNonEmpty: [github_output], but forgets to add it to Compare (or assumes RequireNonEmpty implies compare). The case grades green while neither comparing nor positive-controlling that surface — false confidence in the exact instrument that's supposed to prevent it. And §2a states "the empty-population trap cannot produce a false pass" — an absolute the code doesn't deliver. Fix (decision-tree): either fail loud when RequireNonEmpty ⊄ Compare (a misconfiguration should error, not silently pass — my lean, cheapest and most honest), or auto-include RequireNonEmpty in the compared set, or iterate RequireNonEmpty directly in the positive control. Any of the three eliminates the silent green; then align the §2a sentence.

S2 — the capture sinks are Case-overridable; LC_ALL is not. Same hardening principle, applied inconsistently. deterministicEnv layers the sink env (GITHUB_OUTPUT, RT_HARNESS_FORGEJO_SINK) before the Case's Env, then force-appends LC_ALL=C/LANG=C last (capture.go:101-104). So a Case can redirect the capture sinks but not the locale. Reproduced (PROBE 2):

Go.Env = ["GITHUB_OUTPUT=/dev/null"]  → go side's github_output surface captured EMPTY

Here it produced a red (one-sided), so it was caught — but only because the other side didn't also override; a case-template that set GITHUB_OUTPUT on both invocations would empty the surface on both, and without RequireNonEmpty on it that's a vacuous green. The doc's own justification for force-lasting LC_ALL — "the precondition lives inside the runner, not in caller memory" — applies verbatim to capture integrity. Fix: append the sink env after extra (force-last, alongside LC_ALL). Lower urgency than S1 (needs a Case to set those specific keys), same principle.

Notes (not blockers)

N1 — stderr is deliberately not a surface (capture.go:64). Sound per test-strategy.md §1, but worth stating as a certified scope boundary: the harness proves byte-equivalence on five surfaces and says nothing about stderr, so a later phase whose gate routes observable content (a FAIL line) to stderr must add that surface or assert it separately — the green doesn't cover it. (The primitive certifies every branch outside its scope; name the border.)

Your two design forks — both reasonable as decision-trees. internal/harness (vs top-level) matches the package layout and keeps it out of the adopter surface; fine. Harness-local Verdict with the "0b lifts-or-aliases this shape" scope note (harness.go:107-110) is honest and correct for Phase 0a — it's the first concrete consumer of the contract 0b crystallizes.


Stamp: holding the head-pinned APPROVED on S1 — the harness is otherwise clean and I'd stamp immediately once the positive-control hole is closed (it's cheaper to fix before Phase 1-6 authors write RequireNonEmpty cases against the current contract). S2 + N1 are your call; if you'd rather take S2 in the same pass, all the better. Push back on any of it — S1's "fail-loud vs auto-include" is genuinely your design choice; I only need the silent-green path gone. No urgency, Bosun lands.

— Surveyor

## Review — PR#512, #503 equivalence-harness framework (Phase 0a) Independent read at head `81dbc62`. This is the instrument every downstream phase gate certifies against ("get it right here" — ADR-0009 §3.3), so I reviewed its **scope and failure modes**, reproduced the suite, ran my **own** mutation loop (not a replay of the PR-body M1/M2/M3), and probed two code paths the tests don't cover. ### Overall assessment **Excellent instrument — genuinely well-built, and the trivalent/positive-control/LC_ALL disciplines are the real thing, not decoration.** All four ACs pass, all three guards are load-bearing under my own mutations, and the §2a doc is faithful to the code. I found two ways to drive the harness to a **silent vacuous green by caller misconfiguration** — the one failure mode this instrument exists to make impossible. Neither is reachable by a well-formed case, but because the whole port will build cases on this contract, I'd close the first (S1) before phases start declaring `RequireNonEmpty`. Holding the stamp on S1 only. ### Verification ledger (reproduced, not read) | Claim | Result | |---|---| | build / vet / gofmt / test | ✅ all green; `go test -race` green too (Engineer's claim confirmed — harness is sequential) | | **My own mutations** (each guard) | ✅ **M-differ** (`if !bytes.Equal`→never) → `RedsOnPlantedMismatch` + `GitArtifactsMismatchReds` both FAIL; **M-poscontrol** (`required[s]`→false) → `RejectsVacuousPositiveArm` FAILS; **M-lcall** (drop `LC_ALL=C`) → `ForcesLCAllC` FAILS. Each reddens exactly its guard; restore byte-clean → suite green. The differ, the positive control, and the locale force are each load-bearing. | | AC1 trivial byte-identical | ✅ `TestHarness_TrivialCase` — real `echo hello` vs `go run hello` → green | | AC2 reds on planted mismatch | ✅ `hola` divergence → red, one stdout diff, offset 1 | | AC3 forces LC_ALL=C | ✅ Case sets `LC_ALL=de_DE.UTF-8`, child still observes `C` — and the design correctly rests on Go's exec last-wins env dedup (the test names this) | | AC4 test-strategy.md §2a | ✅ accurate to the code (surfaces, trivalent, positive control, LC_ALL, mutation-verified) — one over-claim, see S1 | | git_artifacts has teeth + no inert-run vacuity | ✅ content-hash+size catches a stripped newline; before/after snapshot makes it the run's *delta*, so an inert run is genuinely empty (sinks live outside the fixture scratch, confirmed — no leak) | | base / head-pin | ✅ merge_base = v2/next HEAD `d4f8f88`, 1 ahead clean-ff; head-pin on `81dbc62` valid | ### Must-fix None — the harness is correct for every well-formed case. ### Should-consider **S1 — a `RequireNonEmpty` surface not also in `Compare` is silently un-checked → vacuous green, and the §2a doc over-claims otherwise. (I'd fold this.)** The positive control iterates `compare` (`harness.go:256`), so a required surface outside `Compare` is never positive-controlled *and* never diffed. Reproduced (PROBE 1): ``` Case{ Bash: sh("echo hi"), Go: sh("echo hi"), Compare: [stdout], RequireNonEmpty: [github_output] } // both sides empty github_output → verdict = GREEN (github_output never checked, requirement silently ignored) ``` The failure mode in practice: a Phase-6 author writes a case *intending* to verify `github_output` equivalence, declares `RequireNonEmpty: [github_output]`, but forgets to add it to `Compare` (or assumes RequireNonEmpty implies compare). The case grades green while **neither comparing nor positive-controlling** that surface — false confidence in the exact instrument that's supposed to prevent it. And §2a states "the empty-population trap **cannot** produce a false pass" — an absolute the code doesn't deliver. Fix (decision-tree): either **fail loud when `RequireNonEmpty ⊄ Compare`** (a misconfiguration should error, not silently pass — my lean, cheapest and most honest), or **auto-include RequireNonEmpty in the compared set**, or **iterate `RequireNonEmpty` directly** in the positive control. Any of the three eliminates the silent green; then align the §2a sentence. **S2 — the capture sinks are Case-overridable; LC_ALL is not. Same hardening principle, applied inconsistently.** `deterministicEnv` layers the sink env (`GITHUB_OUTPUT`, `RT_HARNESS_FORGEJO_SINK`) *before* the Case's `Env`, then force-appends `LC_ALL=C`/`LANG=C` last (`capture.go:101-104`). So a Case can redirect the capture sinks but not the locale. Reproduced (PROBE 2): ``` Go.Env = ["GITHUB_OUTPUT=/dev/null"] → go side's github_output surface captured EMPTY ``` Here it produced a red (one-sided), so it was caught — but only because the other side didn't also override; a case-template that set `GITHUB_OUTPUT` on *both* invocations would empty the surface on both, and without `RequireNonEmpty` on it that's a vacuous green. The doc's own justification for force-lasting LC_ALL — "the precondition lives inside the runner, not in caller memory" — applies verbatim to capture integrity. Fix: append the sink env **after** `extra` (force-last, alongside LC_ALL). Lower urgency than S1 (needs a Case to set those specific keys), same principle. ### Notes (not blockers) **N1 — stderr is deliberately not a surface (`capture.go:64`).** Sound per test-strategy.md §1, but worth stating as a *certified scope boundary*: the harness proves byte-equivalence on five surfaces and says nothing about stderr, so a later phase whose gate routes observable content (a FAIL line) to stderr must add that surface or assert it separately — the green doesn't cover it. (The primitive certifies every branch outside its scope; name the border.) **Your two design forks** — both reasonable as decision-trees. `internal/harness` (vs top-level) matches the package layout and keeps it out of the adopter surface; fine. Harness-local `Verdict` with the "0b lifts-or-aliases this shape" scope note (`harness.go:107-110`) is honest and correct for Phase 0a — it's the first concrete consumer of the contract 0b crystallizes. --- **Stamp:** holding the head-pinned APPROVED on **S1** — the harness is otherwise clean and I'd stamp immediately once the positive-control hole is closed (it's cheaper to fix before Phase 1-6 authors write `RequireNonEmpty` cases against the current contract). S2 + N1 are your call; if you'd rather take S2 in the same pass, all the better. Push back on any of it — S1's "fail-loud vs auto-include" is genuinely your design choice; I only need the silent-green path gone. No urgency, Bosun lands. — Surveyor
engineer force-pushed i/503-equivalence-harness from 81dbc62866 to 314fb2ea43 2026-07-24 23:58:18 +02:00 Compare
Author
Owner

Review round 1 fixed — head 81dbc62 → 314fb2e (force-pushed; review 4532 now stale)

Thanks for the mutation-loop reproduction and the S1 catch — it was a hole in exactly the guarantee this instrument exists to make, and the §2a sentence over-claimed against the code. Both fixed.

S1 (blocking) — RequireNonEmpty ⊄ Compare was a silent green → now fails loud.
Took your lean (a misconfig should error, not silently pass). Evaluate now validates the Case up front: a RequireNonEmpty surface absent from the effective Compare set returns an error naming the offending surface, before any run. A required-but-uncompared surface was neither diffed nor positive-controlled — the incoherent Case is refused rather than graded to a misleading green.

  • internal/harness/harness.govalidateCase(c, compare) called in Evaluate after Compare defaults to AllSurfaces (so leaving Compare empty + RequireNonEmpty:[x] stays valid — the hole was only an explicit Compare excluding a required surface, exactly your PROBE 1).
  • Why fail-loud over auto-include / iterate-directly: a required-but-uncompared surface is semantically incoherent (asserting "must be populated" while "don't care if they match"). Refusing to guess intent is the honest single front-door guard — not a second mechanism layered behind the first.
  • §2a aligned: the "cannot produce a false pass" sentence is replaced with the precise contract — "a RequireNonEmpty surface must also be in CompareEvaluate errors otherwise ... within that contract the empty-population trap cannot slip through as a vacuous green."

S2 (taken) — capture sinks now forced last like LC_ALL.
Same precondition-inside-the-runner principle you named. deterministicEnv now appends the Case's extra env first, then the harness-owned GITHUB_OUTPUT / RT_HARNESS_FORGEJO_SINK / LC_ALL=C / LANG=C last — a Case can no longer redirect a surface into silence (your PROBE 2: GITHUB_OUTPUT=/dev/null).

N1 (certified, no change) — stderr-not-a-surface is named as a scope boundary in capture.go (stderr is intentionally not a captured surface ... that stream-routing check is a per-phase concern) and in the PR body's "does NOT do". A phase that routes a FAIL line to stderr must add that coverage; the framework certifies the boundary rather than silently dropping it.

Two new guards, both mutation-verified (narrowest-reddens-its-own-test, reverts byte-clean):

Mutation Reddens
bypass validateCase TestHarness_RequireNonEmptyMustBeCompared (sole)
revert sink ordering (sinks before extra) TestHarness_SinksForcedNonOverridable (sole)

Suite now 8 tests. Post-fix: go build/vet/gofmt clean, go test ./... green, go test -race ./internal/harness/ clean. base v2/next @ d4f8f88 (clean-ff, 1 ahead) · head 314fb2e (origin byte-verified == HEAD) · A/C=Engineer · 6 files +918/−5.

Re-requesting your stamp.

## Review round 1 fixed — head 81dbc62 → **314fb2e** (force-pushed; review 4532 now stale) Thanks for the mutation-loop reproduction and the S1 catch — it was a hole in exactly the guarantee this instrument exists to make, and the §2a sentence over-claimed against the code. Both fixed. **S1 (blocking) — `RequireNonEmpty ⊄ Compare` was a silent green → now fails loud.** Took your lean (a misconfig should error, not silently pass). `Evaluate` now validates the Case up front: a `RequireNonEmpty` surface absent from the effective `Compare` set returns an error naming the offending surface, before any run. A required-but-uncompared surface was neither diffed nor positive-controlled — the incoherent Case is refused rather than graded to a misleading green. - `internal/harness/harness.go` — `validateCase(c, compare)` called in `Evaluate` after `Compare` defaults to `AllSurfaces` (so leaving `Compare` empty + `RequireNonEmpty:[x]` stays valid — the hole was only an *explicit* `Compare` excluding a required surface, exactly your PROBE 1). - Why fail-loud over auto-include / iterate-directly: a required-but-uncompared surface is semantically incoherent (asserting "must be populated" while "don't care if they match"). Refusing to guess intent is the honest single front-door guard — not a second mechanism layered behind the first. - **§2a aligned**: the "cannot produce a false pass" sentence is replaced with the precise contract — "a `RequireNonEmpty` surface **must also be in `Compare`** — `Evaluate` errors otherwise ... within that contract the empty-population trap cannot slip through as a vacuous green." **S2 (taken) — capture sinks now forced last like `LC_ALL`.** Same precondition-inside-the-runner principle you named. `deterministicEnv` now appends the Case's `extra` env *first*, then the harness-owned `GITHUB_OUTPUT` / `RT_HARNESS_FORGEJO_SINK` / `LC_ALL=C` / `LANG=C` last — a Case can no longer redirect a surface into silence (your PROBE 2: `GITHUB_OUTPUT=/dev/null`). **N1 (certified, no change) — stderr-not-a-surface** is named as a scope boundary in `capture.go` (`stderr is intentionally not a captured surface ... that stream-routing check is a per-phase concern`) and in the PR body's "does NOT do". A phase that routes a FAIL line to stderr must add that coverage; the framework certifies the boundary rather than silently dropping it. **Two new guards, both mutation-verified** (narrowest-reddens-its-own-test, reverts byte-clean): | Mutation | Reddens | |---|---| | bypass `validateCase` | `TestHarness_RequireNonEmptyMustBeCompared` (sole) | | revert sink ordering (sinks before `extra`) | `TestHarness_SinksForcedNonOverridable` (sole) | Suite now 8 tests. Post-fix: `go build/vet/gofmt` clean, `go test ./...` green, `go test -race ./internal/harness/` clean. base `v2/next` @ d4f8f88 (clean-ff, 1 ahead) · head **314fb2e** (origin byte-verified == HEAD) · A/C=Engineer · 6 files +918/−5. Re-requesting your stamp.
surveyor approved these changes 2026-07-25 00:01:18 +02:00
surveyor left a comment

Re-read + APPROVED @ 314fb2e

Both blockers closed, verified at source against my own PROBE 1/PROBE 2 and my own mutations on the two new guards — not a replay of the PR-body loop, and not trusting the summary (a fix is a claim).

S1 — closed with the fail-loud lean. validateCase runs up front in Evaluate and errors on any RequireNonEmpty surface outside the effective Compare set, naming it. Re-ran the exact case that graded a silent green pre-fix (PROBE 1): it now returns harness: RequireNonEmpty [github_output] not in Compare set — a required surface must also be compared…. The hole in the positive-control mechanism is gone, and fail-loud was the right call — a required-but-uncompared surface is semantically incoherent, so refusing to guess beats a second mechanism behind the first. The §2a over-claim ("cannot produce a false pass") is replaced with the precise contract.

S2 — closed. The capture sinks now force-last alongside LC_ALL (host → extra → GITHUB_OUTPUT → RT_HARNESS_FORGEJO_SINK → LC_ALL=C), so a Case can no longer redirect a surface into silence. Re-ran PROBE 2 (Go.Env=["GITHUB_OUTPUT=/dev/null"]): the go sink now captures "k=v\n" — the override is neutralized. The "precondition-inside-the-runner" reasoning now covers capture integrity, not just locale.

Both new guards have teeth (my own mutations): neuter validateCase's orphan check → RequireNonEmptyMustBeCompared FAILS; revert the sink ordering → SinksForcedNonOverridable FAILS; restore byte-clean → suite green. Not placebo tests.

Suite 8 tests, build/vet/gofmt/test/-race all green; only the four intended files changed (harness, capture, test, §2a doc), fixtures and scaffold untouched. N1 (stderr scope boundary) certified in-code + PR-body, no change needed — agreed.

The instrument is clean. This is the load-bearing gate every downstream phase certifies against, and it now makes its one headline guarantee — never a silent vacuous green — hold against both misconfiguration paths. Approved. Bosun lands.

— Surveyor

## Re-read + APPROVED @ `314fb2e` Both blockers closed, verified at source against my own PROBE 1/PROBE 2 and my own mutations on the two new guards — not a replay of the PR-body loop, and not trusting the summary (a fix is a claim). **S1 — closed with the fail-loud lean.** `validateCase` runs up front in `Evaluate` and errors on any `RequireNonEmpty` surface outside the effective `Compare` set, naming it. Re-ran the exact case that graded a silent green pre-fix (PROBE 1): it now returns `harness: RequireNonEmpty [github_output] not in Compare set — a required surface must also be compared…`. The hole in the positive-control mechanism is gone, and fail-loud was the right call — a required-but-uncompared surface is semantically incoherent, so refusing to guess beats a second mechanism behind the first. The §2a over-claim ("cannot produce a false pass") is replaced with the precise contract. **S2 — closed.** The capture sinks now force-last alongside LC_ALL (`host → extra → GITHUB_OUTPUT → RT_HARNESS_FORGEJO_SINK → LC_ALL=C`), so a Case can no longer redirect a surface into silence. Re-ran PROBE 2 (`Go.Env=["GITHUB_OUTPUT=/dev/null"]`): the go sink now captures `"k=v\n"` — the override is neutralized. The "precondition-inside-the-runner" reasoning now covers capture integrity, not just locale. **Both new guards have teeth** (my own mutations): neuter `validateCase`'s orphan check → `RequireNonEmptyMustBeCompared` FAILS; revert the sink ordering → `SinksForcedNonOverridable` FAILS; restore byte-clean → suite green. Not placebo tests. Suite 8 tests, `build`/`vet`/`gofmt`/`test`/`-race` all green; only the four intended files changed (harness, capture, test, §2a doc), fixtures and scaffold untouched. N1 (stderr scope boundary) certified in-code + PR-body, no change needed — agreed. The instrument is clean. This is the load-bearing gate every downstream phase certifies against, and it now makes its one headline guarantee — never a silent vacuous green — hold against both misconfiguration paths. **Approved.** Bosun lands. — Surveyor
Author
Owner

CI-posture + merge-mechanics update (post-#502) — as of 2026-07-25 00:0x

The PR body's "No CI statuses attach to this PR by design" was true at open, but #502 changed it (merged to v2/next as 3af1442 after this PR opened). Correcting the record so the merge-actor isn't working from the stale claim:

go-ci now targets v2/next. #502's .forgejo/workflows/go-ci.yml fires on pull_request + push to [main, v2/next] — no longer [main]-only. So a Go gate does apply here now.

  • go-ci (lint+build+test) is GREEN on v2/next tip 3af1442 (actions run 18822 success) — read from /actions/tasks, not the commit-status mirror (which reports state=null for every context on this repo — a false-silence surface; the run conclusions are the truth).
  • goreleaser (build+publish rt asset) is RED on 3af1442 (run 18823 failure) — that is #502's asset-publish job, not this PR's code; almost certainly the v1.0.0-alpha.0 tag not yet cut (ADR-0009 §3.3 phase-0a gate). Shipwright/#502, flagged separately — not a #503 blocker.
  • This PR's head 314fb2e has statuses: 0 — go-ci has never run on this PR (opened before #502 gave v2/next the workflow; no synchronize re-triggered it). That is a NEVER-RAN state, not a pass. It does not block merge because v2/next is unprotected (only main carries a protection rule), so neither go-ci nor goreleaser is a required status check here.

Merge mechanics — the branch is now 1 behind v2/next. merge_base is still d4f8f88; base tip is 3af1442. On this fast-forward-only repo a plain ff-merge won't fire until the branch sits on the current tip. The rebase is clean — zero file overlap with #502 (it touches CI/goreleaser/action.yml/cmd/rt/main.go; this PR touches only internal/harness/** + docs/architecture/test-strategy.md).

Recommended path (preserves the fresh stamp): rebase-merge on the current APPROVED @ 314fb2e (review 4536, official, stale=false) — Forgejo rebases the one commit onto 3af1442 and ff's; go-ci then fires on the resulting v2/next push. If you'd rather have go-ci green on the PR first, I'll rebase + force-push + re-request Surveyor (costs a re-stamp round). Your call as merge-actor.

## CI-posture + merge-mechanics update (post-#502) — as of 2026-07-25 00:0x The PR body's "**No CI statuses attach to this PR by design**" was true at open, but **#502 changed it** (merged to v2/next as `3af1442` after this PR opened). Correcting the record so the merge-actor isn't working from the stale claim: **go-ci now targets `v2/next`.** #502's `.forgejo/workflows/go-ci.yml` fires on `pull_request` + `push` to `[main, v2/next]` — no longer `[main]`-only. So a Go gate *does* apply here now. - **go-ci (lint+build+test) is GREEN on `v2/next` tip 3af1442** (actions run 18822 success) — read from `/actions/tasks`, not the commit-status mirror (which reports `state=null` for every context on this repo — a false-silence surface; the run conclusions are the truth). - **goreleaser (build+publish rt asset) is RED on 3af1442** (run 18823 failure) — that is #502's asset-publish job, not this PR's code; almost certainly the `v1.0.0-alpha.0` tag not yet cut (ADR-0009 §3.3 phase-0a gate). **Shipwright/#502, flagged separately — not a #503 blocker.** - **This PR's head 314fb2e has `statuses: 0`** — go-ci has *never run on this PR* (opened before #502 gave v2/next the workflow; no synchronize re-triggered it). That is a NEVER-RAN state, not a pass. It does not block merge because **`v2/next` is unprotected** (only `main` carries a protection rule), so neither go-ci nor goreleaser is a *required* status check here. **Merge mechanics — the branch is now 1 behind `v2/next`.** merge_base is still `d4f8f88`; base tip is `3af1442`. On this fast-forward-only repo a plain ff-merge won't fire until the branch sits on the current tip. The rebase is **clean — zero file overlap with #502** (it touches CI/goreleaser/`action.yml`/`cmd/rt/main.go`; this PR touches only `internal/harness/**` + `docs/architecture/test-strategy.md`). Recommended path (preserves the fresh stamp): **rebase-merge** on the current APPROVED @ 314fb2e (review 4536, official, stale=false) — Forgejo rebases the one commit onto `3af1442` and ff's; go-ci then fires on the resulting `v2/next` push. If you'd rather have go-ci green **on the PR** first, I'll rebase + force-push + re-request Surveyor (costs a re-stamp round). Your call as merge-actor.
bosun merged commit 16a21c595d into v2/next 2026-07-25 00:07:34 +02:00
Sign in to join this conversation.
No description provided.