Skip to content

fix: router warnings never reached the user without --explain (#136) - #141

Open
kiki830621 wants to merge 7 commits into
mainfrom
idd/136-router-warnings-visible
Open

fix: router warnings never reached the user without --explain (#136)#141
kiki830621 wants to merge 7 commits into
mainfrom
idd/136-router-warnings-visible

Conversation

@kiki830621

@kiki830621 kiki830621 commented Aug 2, 2026

Copy link
Copy Markdown
Member

Fixes the reported bug in #136 and four rounds of verification findings against the fix itself.

The bug

Every warning the router produced was folded into the explanation prose block, which bestasr transcribe printed only under --explain. So the entire output of a run that silently substituted a different backend was:

Wrote txt transcript to …/cv-zhtw-2.txt

Warnings now print to stderr on the default path:

warning: backend 'fluid-parakeet' model '0.6b-v3' does not list support for language 'zh' — output quality is not established
Wrote txt transcript to /tmp/pk.txt

reason explains a choice and is legitimately opt-in. A warning is what the reader needs in order to trust the file that was just written. Both arrays already existed on ASRRecommendation; only the CLI conflated them.

Also unhidden by the same change: the --backend X is unavailable; selecting automatically substitution notice (the #121 case), the cold-start memory-downgrade warnings, and the #105 declared-language gate.

What four rounds of verification found

Each round found that the previous round's fix left the next layer untested — and round 4 found something different: that round 4's own fix had removed a protection while claiming to strengthen one.

layer covered by closed in
what is rendered lines(for:explain:) round 1
where it goes emit + destination round 2
whether the CLI invokes it report + a source-level lock round 3
which stream the CLI gets when it passes none bestasr-diagnostics-probe, a real process round 4
which stream the CLI gets when it passes some the lock's argument-label pin this round

The regression, because it is the useful part

Round 3's lock matched the whole call text, so it implicitly forbade any extra argument. Round 4 replaced it with a positional anchor to stop a rename of the local result turning it red. The rename tolerance was real. The loss was not noticed, because the mutation battery only re-ran the mutations the old lock had failed to catch — never the ones it had caught:

report(result, explain: explain, err: stdout)
    466/466 green at 2bd7156
    RED, 1 issue, at 9637ed2   ← the same edit, one commit earlier

Same for out:/err: swapped, explain: false (which makes --explain a no-op) and explain: true (which drops the literal warning: token the skill templates key on). The whole argument axis, not just the streams.

When you replace a guard, re-run what the old guard passed — not only what it failed. Section B of the battery below exists for that reason, and nothing in four rounds of this PR's record had done it.

Non-vacuity — 28 mutations, measured

Each applied to a real build (--skip-build is a silent no-op for Swift sources), full suite, then reverted.

A — the five round-4 blocking findings

mutation result
call site err: stdout RED
call site out: stderr, err: stdout RED
call site explain: false RED
call site explain: true RED
probe fixture passes streams explicitly RED
probe passes streams + err: default flipped (two edits) RED
guard explain else { return } above the transcribe call RED
runMapped reverted to fputs RED
unprefixed warnings copied to stdout on the default path only RED
probe exit(70) after correct output RED
decoy string literal + the real call deleted RED

B — everything an earlier guard caught, re-checked

err: default → stdout; out: default → stderr; destinationstdout; if explain {; if (explain) {; guard explain else between the statements; hoisted let shouldReport; /* … */ wrapping the call; the call deleted; print before it; a statement after it; ConsoleLinefputs; the two statements inside report reordered; the #50 reclassification reverted — all 14 RED.

C — benign refactors

rename resultoutcome; a line comment plus a four-line reflow; a block comment sitting between the two statements — all GREEN. The last was a false failure until this round.

28 expected, 0 deviations.

What the lock now does

It runs the source through a token-hiding lexer (line comments, block comments, string literals — a lexer, not a parser), extends backwards to runMapped { requiring exactly one binding between it and the transcribe call, requires the diagnostics call immediately after that call and nothing after it, and pins the argument labels: no out:/err:, and explain:explain forwarded rather than decided. It names no local, so renames stay green. The probe fixture gets the same label pin, because the probe only measures the defaults for as long as it passes none.

What no text pin can do

Stated plainly, because three rounds of this PR's record implied otherwise: a text pin's coverage is exactly the lexical block it anchors to. Hoisting the same guard one level further out — into run(), above runMapped — is green under every version of this lock, including the current one, and no finite set of anchors changes that. Proving the line executes means running the executable, and Transcribe.run() calls CommandCore.live() unconditionally while NSHomeDirectory() ignores $HOME on Darwin, so a subprocess test aimed at a fake home reads the developer's real ~/.bestasr/engines.json.

Extraction is filed as #156. It would shrink the residue to a single delegation line, not remove it — which is why it is a separate refactor rather than part of this fix.

Other fixes

  • The NUL fix had landed on the unreachable path. emit's embedded NUL needs a library caller to construct it; the error: channel embeds an external adapter's stderr verbatim into TranscriptionError.message, and adapters are third-party programs registered from ~/.bestasr/engines.json (通用 external-process engine 協定 — 長尾 ASR 家族的掛載機制 (follow-up from #35) #51). Measured end-to-end: printf 'boom\0DETAIL' >&2 truncated the error and glued the next one to it. Both channels share ConsoleLine, and both branches are counted by a test.
  • fflush is pinned. It is the premise of this round's ordering argument and nothing asserted it — deleting it was green, because the ordering test passes one FILE* as both streams and every other test reads after fclose. A test now reads a fully-buffered stream while it is still open.
  • --explain output is not "unchanged". The 中文 high-value ASR 家族評估 — FluidAudio 已內建 Paraformer/SenseVoice(零新依賴,優先於 MLX-Swift Qwen3-ASR)(follow-up from #35) #50 notice went from - warning: '…' to ! '…' — marker and text. What holds, and is what the issue's second acceptance line is about, is that nothing is lost and nothing is duplicated.
  • Warning-first is a presentation choice, not a buffering necessity. ConsoleLine flushes every write, so both orders are stable on a pty, a pipe and a file — measured.
  • The recommend spec. profile added; measured corrected from "null otherwise" to absent (JSONEncoder omits nil optionals, so "measured" in obj and obj["measured"] is None disagree); language likewise made conditional after it turned out to have the same shape — with a test that drives the nil branch, which no fixture did before. The reason/warnings split is described as editorial rather than as a classification rule, because read as a rule it would place the quality-floor bypass notice in warnings while asr-routing/spec.md mandates it in reasons.

External surface

Moving the #50 notice from reason to warnings migrates it between fields of the recommend JSON, which BestASRMCPCore/Server.swift returns verbatim as an MCP tool result. No field was added or removed and the payload still carries the notice, but its location changed and that is observable: a client keying on reason specifically stops seeing it. A repo-wide sweep finds no such consumer — but the real consumers are agents on the far side of the MCP boundary, where a sweep cannot look. Very likely harmless, not provably harmless.

Scope

  • No --quiet opt-out. The issue floated one parenthetically; there is no verbosity surface to be consistent with. The honest consequence: with a cold WhisperKit cache the auto-detect path warns on every run, and the only way to silence it is 2>/dev/null, which also discards error:.
  • diagnose already printed warnings unconditionally; recommend already emits them in its JSON. benchmark never calls Router.recommend at all.
  • bestasr-diagnostics-probe is not a package product. release-app.sh and release-mcp.sh build per-product; install.sh copies two named binaries; CI publishes nothing. Verified four ways. (swift run bestasr-diagnostics-probe does work — swift run takes target names — but that is a developer convenience, not a distribution path.)

Not fixed here — follow-ups filed

470 tests / 93 suites green.

Refs #136

Every warning the router produced was folded into the `explanation`
prose block, which the CLI prints only under --explain. The entire
output of a run that silently substituted a different backend was
"Wrote txt transcript to …".

Warnings now print to stderr on the default path. The split is the
point: `reason` explains a choice and is legitimately opt-in, but a
warning is what the reader needs in order to trust the file that was
just written. Both arrays already existed on ASRRecommendation; only
the CLI conflated them, so this carries `warnings` through
TranscribeOutcome rather than asking the caller to parse prose.

Also unhidden: the --backend substitution notice (the #121 case), the
cold-start memory-downgrade warnings, and the unverified-model notice.

--explain output is unchanged and the warnings are not duplicated when
it is passed. diagnose and recommend were already unaffected.

Refs #136
CI caught what a developer machine could not: `a clean run carries no
warnings` failed remotely while passing locally, because these tests
used the default WhisperKitLanguageDetector — which downloads and runs
a real model. Locally the model is cached and detection succeeds; in
CI it fails, and the fallback emits a warning on every `--language
auto` run.

The failing test is the guard half of the pair added with this change,
whose whole job is to assert the default path stays quiet. It did
exactly that, just against an environment difference rather than a
production defect: the warning itself is correct and stays, since
language-agnostic ranking is what let an English-only backend win zh
audio (#105).

Stubbing the detector restores the hermetic contract this file already
claims. Side effect: the three CLI suites drop from ~5.8s to ~0.02s,
which is how much real model work was hiding in them.

Refs #136
@kiki830621

Copy link
Copy Markdown
Member Author

Verify Report — PR #141

Engine

manual fan-out (4 lens Agents + sequenced Devil's Advocate, model: opus, file-based output) + Codex (gpt-5.6-sol, effort xhigh, via codex-call HTTPS) — 6 independent verifiers, two model families.

Backend note: the canonical pai-ensemble 2.20.0 engine was available, but this session operates under a standing instruction not to launch Workflow runs unprompted. Tier 3 manual fan-out is the skill's declared quality-equivalent path (same four lenses + DA + cross-model leg), so the ensemble composition is unchanged.

Diff-freshness gate (#228): PASS — frozen 4f9f2dd == PR head at aggregate time. No mid-review commits.

Aggregate

FAIL — 2 blocking, 6 follow-up.

Both blocking findings survived a dedicated adversarial pass (see DA-9 for the attacks that were tried against them and failed).

Requirements coverage — issue #136 acceptance criteria

# Criterion Status
A1 transcribe --backend <unavailable> prints the substitution warning without --explain FULLY
A2 --explain output unchanged FULLY (untested — see F7)
A3 a test asserts the warning reaches stderr on the default path PARTIALLY → blocking

Suggested-direction items: reasons/warnings split PARTIALLY (F1); --quiet declined (defensible, but the stated rationale does not hold — F8); "check benchmark and recommend" PARTIALLY — the PR reports on diagnose and recommend, silently substituting the command the issue named. benchmark was independently verified to need no change (it never calls Router.recommend), so the conclusion is right and only the evidence is mis-stated.


Findings (merged, deduped; severity = max across sources)

# Severity Finding Source Action
F1 HIGH The #50 unverified-model notice is reasons.append at Router.swift:210-212, so it is still --explain-only — while CHANGELOG.md:131-133 and the PR body state it was unhidden. Issue #136 names it explicitly. The string literally begins with the token warning: while living in reasons — the exact conflation this issue exists to end. Reachable: ModelGrid.swift carries 9 verified: false rows. Locked in place by RouterTests.swift:112, a test named "…warns about unestablished quality" that asserts on rec.reason. requirements + logic + regression + DA (upheld) Blocking
F2 HIGH No test covers the CLI printing path. Deleting BestASRCommand.swift:156-165 outright leaves all 447 tests green — reproducing, one layer up, the exact failure mode A3's purpose clause names. Package.swift:66-77 already links the bestasr executable into BestASRKitTests, so infeasibility is not the obstacle. requirements + logic + regression + Codex + DA Blocking
F3 MEDIUM Successful runs can exit on a fatal signal. FileHandle.write(_:) raises an uncatchable ObjC exception: closed stderr (2>&-) → SIGABRT / exit 134; an early-exiting stderr reader (2> >(head -n1)) → SIGPIPE / exit 141. Both measured, with a zero-warning control run confirming attribution to the new loop. The transcript is on disk and Wrote … already printed, so $? stops describing what happened. Pre-existing API misuse promoted from --explain-only to the default path. security (LOW) + DA (upgrade + second mode) In-scope fix
F4 MEDIUM The hermeticity commit fixes three of four suites in the file it claims to have fixed. CLITests.swift:404 / :440 / :461 construct CommandCore directly with requestedLanguage: "auto", falling through to the real WhisperKitLanguageDetector. Measured independently by two reviewers: ContextCommandTests ≈ 5.3–5.9 s and is the sole determinant of total suite wall time. They pass only because the detection warning lands in warnings while their assertions read explanation/reason — coincidence, not contract. regression + Codex + DA In-scope fix
F5 MEDIUM Consumer enumeration marked "verified, not assumed" missed plugins/bestasr/skills/ — three shipped templates invoking bestasr transcribe, two gating on $?. No breakage, but all three pass --explain, so this repo's flagship programmatic consumer takes the untouched branch. Combined with the fact that --explain emits no warning:-prefixed line at all, the consumer most likely to want greppable warnings is structurally the one that cannot get them. DA (new) Follow-up
F6 MEDIUM Advisory for whoever implements the "carry enrollWarnings into TranscribeOutcome.warnings" suggestion: non-duplication today is guaranteed by construction (CommandCore.swift:459 and :470 read the same array), not by discipline. Appending a warning that is not sourced from rec.warnings without also adding it to explanation makes it visible by default and invisible under --explain — reintroducing #136 inside the flag whose purpose is disclosure. DA (new) Follow-up
F7 MEDIUM (doc) / LOW (code) The PR body's "That is a real run" sample is edited in two independent ways: reordered (warning-first only occurs when stdout is piped; on a pty the code prints Wrote … first) and hand-rewrapped with a 9-space continuation indent (the code emits one unwrapped line). Ordering itself is a reviewer-introduced standard — absent from the acceptance criteria — hence LOW as code. Also: the new comment at :153-154 says "printing them again above would duplicate"; the code prints below. all 5 lenses + DA (refined) In-scope fix
F8 MEDIUM The --quiet decline rationale ("the clean-run path emits nothing, so there is nothing to suppress") holds only under the stubbed detector this same PR introduces. Production default is WhisperKitLanguageDetector, --language defaults to auto, and any detector failure yields detectionUnavailableWarning — so on an offline or cold-cache machine the default path warns on every run. Declining the flag is still defensible; the shipped reasoning is not. requirements In-scope fix (revise text)
F9 LOW --language reaches stderr unescaped. Reproduced on the built binary: $'\x1b[31mzz\nwarning: forged line' survives with both the raw ESC[31m and the LF intact, so the second physical line reads as an independent, well-formed warning: entry — ANSI injection plus log-line forging on the tool's own warning channel. Taint source is the caller's own argv and the primitive predates this PR; this PR widens it from opt-in to always-on. security + DA Follow-up
F10 LOW warnings: [String] = [] on the public initializer is a fail-silent default: a future production construction site that omits it compiles clean and reports "no warnings" — precisely the failure class #136 exists to eliminate. One production site today, so latent. logic + security + regression Follow-up
F11 LOW No verbosity surface exists anywhere (grep -rn "quiet|verbos" → zero matches), so declining --quiet breaks no contract. But the only way to silence the now-unconditional output is 2>/dev/null, which also discards runMapped's error: … line — the sole channel for typed failures. Coupled with F8 that is every run on an offline machine. DA (new) Follow-up
F12 LOW The reason/warning split is implemented for the CLI only. MCP (Server.swift:323) and GUI (ContentView.swift:191) still receive warnings as undifferentiated prose inside explanation, the GUI styling it .secondary. Neither is a regression; recording it as a decision rather than an unnoticed gap. logic + regression Follow-up
F13 INFO Spec delta. openspec/specs/asr-routing/spec.md already assigns notices to reasons vs warnings normatively, and this PR makes that classification load-bearing for user-visible CLI output for the first time — while openspec/specs/cli/spec.md:355 contains zero occurrences of warning, stderr or explain. The missing requirement is the link, not the sentence: a future reclassification in Router.swift (exactly what F1 contemplates) would silently change CLI output with no spec statement to review it against. requirements + regression + DA Follow-up

Adversarial pass — what the Devil's Advocate changed

Recorded because a verdict that only ever accumulates findings is not adversarially tested.

Partial overturn of a 4-source consensus. Four sources listed the quality-floor bypass notice (Router.swift:112-115) alongside #50 as a misclassification to move into warnings. It belongs in reasons by spec: openspec/specs/asr-routing/spec.md:577 is normative ("…bypasses the floor with a quality warning in the reasons") and scenario :615-618 governs it. RouterTests.swift:219 is therefore a correct spec lock, not the same conflation one layer lower. For that item the only correct remedy is deleting the claim from the PR body — moving the string would require a Spectra change proposal. F1 is narrowed to the #50 half accordingly.

Both published remedies for F2 are defective, in opposite directions. The pure-function seam (warningLines(_:explain:)) tests the branch and the prefix but not the destination — a regression that writes to stdout passes it, which is a case the cross-model reviewer explicitly enumerated. The process-level test that would catch it reintroduces the model/network dependency the PR's second commit exists to remove. Neither reviewer saw the other's constraint. A remedy satisfying both: an injectable sink, or keep the seam and add one dup2-based test redirecting fd 2 to a pipe.

Two security remediations do not deliver what they claim. The proposed --language regex ^[a-z]{2,3}(-[a-z0-9]{2,8})*$ is offered partly because --language zz "currently routes and transcribes rather than failing" — but zz matches that regex; it validates shape, not ISO 639 membership. And it must sit after the auto early-return at Language.swift:10. Separately, the "strip C0/C1" one-liner strips C0 and DEL only; C1 (0x80–0x9F) passes through.

Attacks that failed (so the surviving verdict is load-bearing): a second already-correct "unverified" notice (none exists — one grep hit, in reasons, and unlike the quality-floor case no spec governs its placement); CHANGELOG entry filed under a released version (clean, sits under [Unreleased]); merging/prepending duplicating or dropping warnings (neither); --context-dir dropping warnings (explicitly preserved); stdout contract pollution (transcript always goes to a file); | head as an everyday SIGPIPE trigger (a 64 KB pipe buffer absorbs normal volumes — the real trigger is an early-exiting reader, and F3 is stated accordingly).


Scope check

Clean. The diff touches 4 files, +115/−4, all within issue #136's stated surface. The second commit (test: stub the language detector …) was examined for scope creep and is in scope: the PR's own new guard test cannot be green without it, it is confined to the shared makeCore helper plus one private struct, and it touches no production code. The objection to it is F4 (incomplete within its own file), not scope.

The stub was also checked for coverage loss and is a net improvement: TestSupport.swift:58-82 writes one second of digital silence, so the pre-stub "real detection" was whisper-tiny guessing a language out of nothing — nondeterminism, not coverage.

No downstream breakage found, each call site verified by reading rather than inference: MCP and GUI both append explanation unconditionally; TranscribeOutcome is Sendable but not Codable, so the new field cannot leak into a serialized surface; no repo script parses transcribe stderr.


Next

F1 and F2 gate the merge. F1 admits a documentation-only remedy (correct the record) or a code remedy (reclassify the #50 notice, stripping its embedded warning: prefix so the CLI does not render warning: warning: …) — the issue names #50 explicitly, so reclassifying is the closer reading, but a follow-up issue with the CHANGELOG corrected is acceptable. What is not acceptable is shipping the sentence while the notice stays hidden.

Verify was run at 4f9f2dd; re-run after the blocking findings are addressed.

…nts it (#136)

Verification found the fix had separated the CLI's rendering while leaving
the classification underneath it half-done, and had said otherwise in the
permanent record.

The unverified-model notice this issue names explicitly was still
reasons.append, so it stayed --explain-only while the CHANGELOG listed it
among the notices that had been surfaced. The tell was in the string: it
began with the literal token "warning: " while living in the reasons
array, which is the same conflation #136 exists to end, one layer below
the one it fixed. It is now a warning, without the inline prefix -- the
CLI adds its own, so a straight move would have rendered
"warning: warning: '...' is unverified".

It was not merely mis-placed, it was PROTECTED: RouterTests' "Locking an
unverified backend warns about unestablished quality" asserted on
rec.reason. Name saying one thing, assertion certifying the other. No
compiler and no test would have caught the misclassification, because a
green test vouched for it.

The quality-floor bypass notice deliberately stays in reasons, and an
earlier draft of the CHANGELOG wrongly listed it as surfaced too.
openspec/specs/asr-routing/spec.md is normative that a locked backend
"bypasses the floor with a quality warning in the reasons", with a
governing scenario; moving it would be a spec change, not a bug fix. A
test now pins each placement and the asymmetry is stated at both sites so
it does not read as an oversight.

Nothing tested the CLI's printing path at all -- deleting the branch
outright left the entire suite green, which is the failure mode this
issue's third acceptance line names, reproduced one layer up. The
rendering moved into BestASRKit as TranscribeDiagnostics, split so that
all three properties are assertable: which lines (branch), how they read
(prefix), and WHERE they go. The last is why `destination` is a named
constant rather than an inlined stderr -- a pure test of the rendered
strings stays green when they are sent to stdout, which for anyone piping
a transcript is #136 again.

Note on how NOT to test that: redirecting the process's real fd 2 around
the call is the more direct proof and is unsafe inside a test bundle.
After close(2) the next open() anywhere in the process receives fd 2, and
restoring it closes that resource out from under whoever opened it --
including the harness's own result channel, which hangs the run. Measured
the hard way.

A closed stderr also turned a successful run into a fatal signal.
FileHandle.write(_:) raises an uncatchable ObjC exception on write
failure, so 2>&- produced SIGABRT (exit 134) and an early-exiting reader
such as 2> >(head -n1) produced SIGPIPE (141) -- transcript already on
disk, "Wrote ..." already printed. Pre-existing misuse, promoted from
--explain-only to the default path by this issue, and reachable from the
skill templates in plugins/bestasr/ that gate on $?. Now fputs.

Warnings are emitted BEFORE the success line. stderr is unbuffered and
stdout is at best line-buffered, so warning-first is the only order that
holds under both a terminal and a pipe; the reverse announced the file as
written before the reason to distrust it appeared, and made the samples
in the CHANGELOG true only when stdout was redirected.

Non-vacuous, measured by reverting each guard independently: the original
#136 bug (warnings only under --explain) breaks 4 assertions where it
previously broke 0; the F1 misclassification breaks 4; sending
diagnostics to stdout breaks 2; dropping the prefix breaks 2.

456 tests / 89 suites green.

Refs #136
Round-2 verification found the previous commit had made the rendering
testable and left the WIRING uncovered -- and #136 was a wiring bug. The
reported behaviour is a call-site branch, and re-adding it to the fixed
code restored that behaviour verbatim with 456/456 green:

    if explain { TranscribeDiagnostics.emit(for: result, explain: explain) }

Under --explain that renders the explanation to stderr; on the default
path it renders nothing. Byte-for-byte the bug the issue reports, inside
the commit that claimed to have made it untestable-to-restore. Deleting
the call, overriding its stream, and moving it after the success line
were all equally green.

The previous commit's own non-vacuity claim was wrong for the same
reason: it reported that reintroducing the bug broke 4 assertions. What
it actually mutated was the new library function's branch. Measured now
across three reversions -- the literal call-site restoration breaks 0
(before this commit), the library-layer one breaks 2, the full plumbing
revert breaks 5. Never 4. The sentence that row was making -- "before
this round, reintroducing the exact bug broke nothing" -- was still true
of the round that wrote it.

Both statements the CLI used to run now live behind
TranscribeDiagnostics.report(_:explain:out:err:), so their ORDER is a
property of tested code rather than of two adjacent lines nobody asserts
on, and the success line gets its first assertion of any kind. A
source-level lock pins the one line that invokes it: measured to refuse
all four call-site regressions and to tolerate comments, reflows and
renames.

Executing the command instead was tried and abandoned. Transcribe.run()
calls CommandCore.live() unconditionally and there is no injection seam;
$HOME is not one either, because NSHomeDirectory() ignores it on Darwin
-- a subprocess test aimed at a fake home silently loads the developer's
real ~/.bestasr/engines.json and can spawn a real model load. A
subprocess CLI test on this path is non-hermetic by construction today.

runMapped's error line still used the raising API, so a FAILING run under
2>&- still aborted; it is fputs now too. Two limits stated rather than
implied: fputs fixes the uncatchable-exception class, not SIGPIPE, which
is a signal from the underlying write -- and since diagnostics now
precede the success line, an early-exiting reader takes the process down
sooner than before. And ignoring the write result trades a loud failure
for a silent one: under 2>&- warnings are discarded and the run exits 0.

Record corrections, all of which claimed more than shipped:

- The CHANGELOG said the destination was "asserted by capturing the
  process's real fd 1 and fd 2". Nothing captures either; that sentence
  survived from the dup2 approach that was abandoned after it hung the
  test harness. The type's own doc comment contradicted itself 24 lines
  apart for the same reason.
- "--explain output is unchanged" is not byte-true and cannot be:
  reasons render as "  - " and warnings as "  ! ", so reclassifying the
  #50 notice necessarily moves it between markers. Read byte-literally,
  A2 would forbid the reclassification the issue's own Suggested
  direction asks for. The reading that makes #136 coherent -- no loss, no
  duplication -- is stated instead, and is satisfied.
- RouterTests' comment still said the "reasons" must carry the notice,
  fourteen lines above the assertion corrected to read warnings. That is
  the defect this PR's thesis names, left sitting above the line that
  fixed it.
- Moving the notice migrates it between fields of the recommend JSON,
  which cli/spec.md describes normatively and BestASRMCPCore returns as
  an MCP tool result. A repo sweep sees no consumer; the consumers are
  agents past the MCP boundary, where a sweep cannot look. Now disclosed.

459 tests / 91 suites green.

Refs #136
The remaining items from verification, none of which changes the verdict
on the wiring lock but each of which was a claim outrunning the code.

fputs takes a NUL-terminated C string, so an embedded U+0000 truncated a
warning AND swallowed its terminator, gluing the next warning onto the
same physical line. Measured: warnings ["abc\0def", "second"] arrived as
one line ending mid-word. Unreachable from argv, but TranscribeOutcome is
public and the FileHandle path it replaced had no such limit. Both write
sites now put raw UTF-8 bytes through fwrite, and a test pins both halves
(content preserved, two lines not one).

Ordering had no assertion at all. Captured separately it is invisible --
two files have no relative order -- so the claim that warnings precede
the success line was untestable as written. Both streams now go to one
file in the test, which is the 2>&1 case the claim is actually about;
reordering report() turns it red.

The test named "a failed write does not turn a successful run into a
fatal signal" proved much less than that. It uses a read-only stream,
which is the EBADF class only; SIGPIPE is a signal from the underlying
write and no stdio choice suppresses it. Renamed to what it covers, with
the limit stated in the body rather than implied by the name.

openspec/specs/cli/spec.md enumerated `reason` for the recommend payload
and never mentioned `warnings`, so moving the #50 notice took it off the
only field the spec names -- on a surface BestASRMCPCore returns verbatim
as an MCP tool result, where the consumers are agents a repo sweep cannot
see. The spec now enumerates both arrays and says notices move between
them, so a consumer needing every notice reads both.

Also: the CHANGELOG sample was still hand-wrapped across two lines while
the code emits one, and its "now fputs" sentence survived the change to
fwrite. And the emit doc comment still opened "fputs rather than
FileHandle" after the code stopped using fputs -- prose drifting from
code inside the commit that fixed prose drifting from code.

461 tests / 91 suites green. Non-vacuous, measured with a real rebuild
(a --skip-build mutation run is a no-op for Swift sources and reads as
0 for every guard): reordering report() breaks 1, reverting fwrite to
fputs breaks 2.

Refs #136
@kiki830621

Copy link
Copy Markdown
Member Author

Verify Report — PR #141, Round 2

Re-verify after the round-1 blocking findings were addressed. This report describes 4bc6663, the commit the ensemble was frozen against; the fixes it prompted are in 580c4be and 9637ed2, summarised at the end.

Engine

4 lens Agents (opus) + sequenced Devil's Advocate + Codex (gpt-5.6-sol, xhigh) — 6 independent verifiers, two model families. Diff-freshness gate (#228): PASS. Working-tree discipline held: every verifier ran its mutation work in an isolated copy, and three independently recorded matching file hashes. No repeat of round 1's contention.

Aggregate

FAIL at 4bc6663 — 1 blocking, unanimous across all six verifiers.

F1 is genuinely closed. F2 is not, and the reason is sharper than "insufficient coverage": the fix moved the tested boundary to a place that excludes the thing #136 actually was.


Round-1 findings — disposition

Every R1 finding was checked for silent claiming. None was claimed fixed without being fixed; the two non-fixes that are absent from the PR's "Not in scope" list (F5, F9) are omissions from a disclosure list, not false claims.

R1 status at 4bc6663
F1 (HIGH, blocking) — the #50 notice was reasons.append while the record said it had been surfaced CLOSED. Now warnings.append with the inline prefix stripped; RouterTests asserts on rec.warnings; reverting it breaks 4 assertions across 3 tests. The DA's round-1 narrowing was honoured — the quality-floor notice stays in reasons, and the spec citation was independently verified verbatim at asr-routing/spec.md:577 with its governing scenario.
F2 (HIGH, blocking) — nothing tested the CLI's printing path NOT ADDRESSED. See the adjudication.
F3 — fatal signal on a closed fd 2 CLOSED for the diagnostics path (measured: old API → exit 134, new → exit 0). Residual on the sibling error: channel, not claimed fixed anywhere.
F7 — the "real run" sample PARTIALLY. The ordering half is closed and the misleading framing is gone from the PR body; the CHANGELOG's copy was still hand-wrapped.
F8 — the --quiet rationale CLOSED. Replaced with the honest consequence, verbatim.
F4, F12 NOT ADDRESSED — disclosed in "Not in scope". Correct handling.
F5, F9, F10 NOT ADDRESSED — undisclosed. Follow-ups, not false claims.
F6, F11, F13 Correctly deferred / recorded as decisions.

Issue #136's acceptance: A1 MET, A2 MET (see the adjudication below — the claim about it was wrong, not the code), A3 still PARTIALLY — which is the blocking item.


The adjudication: what F2 actually is

All six verifiers reached "F2 is open". That unanimity is not the interesting part; the reasoning that separated them is.

Three sources ran the wrong mutation. logic, regression and Codex each demonstrated the gap by deleting the CLI's call (M1) or redirecting its stream (M6) and observing 456/456 green. Both are real regressions — but neither is the bug #136 reports.

The Devil's Advocate ran the right one. #136's reported behaviour is a call-site branch. Re-adding it to the fixed code:

if explain { TranscribeDiagnostics.emit(for: result, explain: explain) }

Under --explain that renders the explanation to stderr; on the default path it renders nothing. Byte-for-byte the behaviour the issue reports, restored inside the commit that claimed to have made it untestable-to-restore, with 456/456 green.

The DA also put the strongest case for closure on the record before rejecting it, which is worth preserving: no architecture has a fully-tested wiring seam short of executing the entry point; round 1's mutation deleted ten lines of logic where this one deletes one line of wiring; and the rendering that line invokes is now genuinely pinned. That is real progress. It collapses on one fact — #136 was never a rendering bug. The refactor drew the tested boundary to exclude call-site wiring, which is precisely and only where the bug lived.

Two of the three published remedies are wrong, in opposite directions. This is the part that changes what someone does:

  • logic's remedy closes nothing. Its diagnosis is correct (production reads emit's default argument, and no test exercises it); its fix — drop the default and make the CLI pass to: destination explicitly — was applied and measured: the original-bug restoration and the stdout redirect both remain 456/456 green. It eliminates one mutation by deleting the construct, not by covering it, and would let the doc comment truthfully claim closure while the reported bug stays one keyword away.
  • Codex's remedy has no injection point. Its design (subprocess + dependency injection through the real Transcribe path) was attempted. Transcribe.run() calls CommandCore.live() unconditionally, which hard-codes six engines; the library has exactly one environment read (PATH). And $HOME is not a lever: NSHomeDirectory() ignores it on Darwin, so a subprocess test aimed at a fake home silently loaded the developer's real ~/.bestasr/engines.json, spawned the real mlx-audio adapter, and timed out after 120 s — confirmed with sample(1) showing the host blocked in ExternalProcessEngine.run. A subprocess CLI test on this path is non-hermetic by construction today. The remedy is not wrong in principle; it is unbuilt infrastructure, and prescribing it as a merge gate over-prices the defect by an order of magnitude.

A working remedy exists and was measured: a source-level wiring lock, 22 lines, 0.003 s, no process spawn and no new production API. Measured against seven tree states — it fails on the guarded call, the deleted call, the stream override and the reordering; it passes on an inserted comment, a reflowed call and a renamed variable. Its honest weakness is that it pins text rather than behaviour: it cannot prove a byte reached fd 2 (the constant and the stream-pair test do that), and a genuine restructuring of Transcribe.run() fails it by design, so its failure message has to say so.

The DA also corrected a framing it was handed: PR #140 did not establish a source-text assertion pattern — those tests execute the real script and byte-pin its stdout. What #140 established is #filePath-rooted access to repo artifacts from a test, which is the enabling half.


Findings (merged; ~30 filed items dedupe to these)

# Severity Finding Source
R2-A HIGH (blocking) F2 is open at the layer the issue names. The reported bug is restorable verbatim at the call site with the whole suite green; so are deletion, stream override, and reordering. The rendering is pinned; its use is not. all six
R2-B MEDIUM The warning-first ordering is stated as a contract in three places with a buffering argument for why it is the only correct order, and nothing holds it — swapping the two lines leaves 456/456 green. requirements + logic + Codex
R2-C MEDIUM The non-vacuity table's headline row is false. "The original bug breaks 4 assertions where it previously broke 0" — measured across three reversions: the literal call-site restoration breaks 0, the library-layer one 2, the full plumbing revert 5. Never 4. The mutation was applied to the new library function's branch, not to the bug. And the sentence that row was making — "before this round, reintroducing the exact bug broke nothing" — was still true of the round that wrote it. requirements
R2-D MEDIUM The CHANGELOG credits the change with "capturing the process's real fd 1 and fd 2". No test captures either; the test file's own doc comment says it is "Deliberately NOT dup2", and the commit message devotes a paragraph to why. The sentence survived from an approach that was tried, hung the test harness, and was abandoned. Same defect class as F1, in the document rewritten to correct F1. requirements
R2-E MEDIUM TranscribeDiagnostics.swift:19 said the destination "is asserted by redirecting fd 2 around it"; :42-47 in the same file said that "must not be used here". Self-contradiction 24 lines apart, same cause as R2-D. requirements
R2-F LOW (doc, adjudicated down from HIGH) "--explain output is unchanged" is not byte-true. See the A2 ruling below. Codex + logic, downgraded by DA
R2-G LOW RouterTests.swift:104 still said "the reasons must carry the unverified warning" — fourteen lines above the assertion this commit corrected to read rec.warnings. The defect the commit's own thesis names, left sitting above the line that fixed it. requirements + regression
R2-H LOW runMapped's error: line still used the raising API, so a failing run under 2>&- still aborted. Not claimed fixed — but the PR body calls this "the sole channel for typed failures", and the $?-gating skill templates reach it. logic
R2-I LOW fputs takes a NUL-terminated C string: an embedded U+0000 truncates the warning and swallows its terminator, gluing the next warning onto the same physical line. Measured. Unreachable from argv; TranscribeOutcome is public, and the Data(…utf8) path it replaced had no such limit. logic + Codex + DA
R2-J MEDIUM The recommend JSON is a specified, shipped, agent-facing surface. openspec/specs/cli/spec.md:211 enumerates reason and never mentions warnings; this change moves the #50 notice off the enumerated field. BestASRMCPCore/Server.swift:369 returns that payload verbatim as an MCP tool result — so the repo-wide sweep that found "zero consumers" was correct about the repo and structurally unable to see the actual consumers, which are agents past the MCP boundary. DA only
R2-K LOW Spec consultation is asymmetric: invoked once to decline a change (correctly), while the PR changes user-visible CLI behaviour and touches no spec. The nearest precedent (309cdcd) updated the spec in the same commit as the behaviour change. DA
R2-L INFO The fputs conversion trades a loud failure for a silent one: under 2>&- a run now discards every warning and exits 0 where before it aborted. That is the right trade; it is still a trade, and a $?-gating consumer with a closed fd 2 now trusts a transcript whose warning was destroyed without trace. DA
R2-M LOW The test named "a failed write does not turn a successful run into a fatal signal" uses a read-only stream, which is the EBADF class only. fputs does not suppress SIGPIPE — that is a signal from the underlying write — so the name claims a property the test cannot reach, and since diagnostics now precede the success line, an early-exiting reader takes the process down sooner than before. Codex + DA

A2, adjudicated

Codex rated the --explain delta HIGH; logic measured the same delta and said "the defect is the claim, not the change". The Devil's Advocate went further and resolved it:

The byte-literal reading of A2 cannot be the intended one, because under it issue #136 contradicts itself. The issue's own Suggested direction asks for exactly the reclassification that changes --explain's bytes — CommandCore renders reasons as - … and warnings as ! …, so moving a notice between the arrays necessarily moves it between markers. A2 read byte-literally forbids what the issue asks for.

The reading that makes #136 coherent — no loss, no duplication under --explain — is the one two sources converged on independently, and on that reading A2 is met and measured: the #50 notice still appears exactly once. What is wrong is the CHANGELOG's flat word "unchanged", which the same entry contradicts three paragraphs later. Severity drops HIGH → LOW/doc; the required edit is one sentence.


Negative space — attacks that failed


What changed in response (580c4be, 9637ed2)

  • report(_:explain:out:err:) moves both statements the CLI ran into the library, so ordering is a property of tested code, and the success line gets its first assertion of any kind. A source-level wiring lock pins the one line that invokes it — measured: the guarded call, the deleted call and the stream override all turn it red; comments, reflows and renames do not.
  • The ordering claim is now testable and tested by sending both streams to one file — the 2>&1 case the claim is actually about. Captured separately it is unobservable.
  • runMapped converted too, so no raising stderr write remains in the CLI. Both limits stated rather than implied: this fixes the uncatchable-exception class, not SIGPIPE; and ignoring the write result is a deliberate trade toward a silent loss.
  • Both write sites use fwrite over UTF-8 bytes rather than fputs, closing R2-I, with a test pinning both halves (content preserved, two lines not one).
  • openspec/specs/cli/spec.md now enumerates warnings alongside reason and states that notices move between them, so a consumer needing every notice reads both (R2-J).
  • Record corrections: the fd-capture claim, the self-contradicting doc comment, the A2 wording, the stale RouterTests comment, the hand-wrapped sample, the overclaiming test name — and the fputs sentence that itself went stale when the code moved to fwrite, inside the commit fixing prose-drift.

461 tests / 91 suites green.

A note on the measurement, since this report has been strict about mutation claims: the first attempt at verifying the new guards used swift test --skip-build and returned 0 for every mutation. That flag is correct for #140's harness, where the mutated artifact is a Python script read at runtime, and a silent no-op here, where the mutated artifact is Swift that must be recompiled. Re-run with a real build: reordering report breaks 1, reverting fwrite breaks 2.

Deferred to follow-ups, unchanged: the three CommandCore( constructions that still set suite wall time (F4), --language control-character handling (F9), the warnings: [String] = [] default (F10), MCP/GUI undifferentiated prose (F12), and the skill templates that pass --explain and so still receive no warning:-prefixed line (F5).

Re-verify at 9637ed2 before merge.

@kiki830621

Copy link
Copy Markdown
Member Author

Verify Report — PR #141, Round 3

Third verification after round 2's blocking finding was addressed. This report describes 9637ed2.

Engine

6 verifiers: 3 lens Agents (opus, isolated clones) + Devil's Advocate (sequenced) + Codex (gpt-5.x, cross-model) + coordinator. Manual fan-out, file-based output.

The cross-model leg is back — the round-2 quota exhaustion (HTTP 429, reported as ~5 days) cleared far earlier than stated, so round 3 ran the full engine rather than round 2's degraded four. That matters here: Codex found the blocking finding first, analytically, from the diff alone and with no repository access. Two lenses and the DA then reproduced it by measurement, independently and in separate clones.

The security lens was not dispatched separately. That was a coordinator judgment about a diff whose surface is one CLI call site, one small library type and a spec line — and it was partly wrong: the logic lens surfaced a security-class finding anyway (R3-9, a forgeable warning: line), which a dedicated lens might have found sooner and pressed harder. Recorded rather than glossed.

Gates, all re-checked at 9637ed2: diff-freshness (#228) PASS (local HEAD == headRefOid, and CI's head_sha is the same commit, not a stale run); auto-close Source 1 closingIssuesReferences = []; auto-close Source 2 five commit bodies against the trap regex, zero hits. CI green at 9637ed2. Baseline reproduced independently by three reviewers: 461 tests / 91 suites.

Aggregate

FAIL at 9637ed2 — 2 blocking, both behavioural.

Round 2's blocker was answered at the layer it named. The lock does catch outright deletion, and report() gave the success line and the ordering their first assertions ever. But the untested boundary moved rather than closed, and a second one was never closed at all.


The adjudication: where F2 actually stands

Round 1 left the behaviour layer untested — TranscribeOutcome.warnings was populated; nothing checked it was printed.
Round 2 left the binding layer untested — emit's stream default.
Round 3 leaves the default-argument layer untested — and leaves the guard axis exactly where round 2 found it.

Six mutations restore a user-visible regression with 461/461 green. Every one was applied to a real build (--skip-build is a silent no-op for Swift sources — round 2's own lesson) and reverted:

mutation where what the user sees
err: … = destination= stdout TranscribeDiagnostics.swift:104 — declaration only, call site untouched every warning lands on stdout; transcribe x.wav > out.txt puts them in the transcript and leaves stderr empty. This is #136's original scenario.
out: … = stdout= destination :103 Wrote … leaves stdout; a caller redirecting stdout gets an empty file
if (explain) { report(…) } BestASRCommand.swift:173 #136 verbatim — two extra parentheses
guard explain else { return } above the call :173 #136 verbatim — the most ordinary tidy-up of the two
let shouldReport = explain; if shouldReport { … } :173 #136 verbatim — one line of indirection
the call wrapped in /* … */ :173 transcribe prints nothing at all — no warning, no Wrote …

R3-1 — the stream defaults. Sources/bestasr/BestASRCommand.swift:173 is the only production caller and reads both defaults. All nine report/emit call sites under Tests/ pass streams explicitly. So the two values production uses are executed by nothing.

The chain the record describes, with the broken link marked:

lock pins the call ✓ → report writes to err ✓ → err defaults to destinationuntesteddestination == stderr

CHANGELOG.md:173-175 enumerates that chain and gets it wrong: "It cannot prove a byte reached fd 2 — the constant and the stream-pair test do that." They do not. Under the mutation, the constant test is green (the constant is unchanged), the stream-pair test is green (it overrides both streams), the lock is green — and the byte lands on fd 1. The one link the record does not mention is the only one that is broken.

Sharper still: the lock does not merely fail to catch this — it mandates it. Its needle requires the CLI not to override the stream, which is exactly what forces production through the untested default. And TranscribeDiagnostics.swift:44-49 justifies destination's existence as protection against "a one-word change to stdout [that] would reintroduce #136 … while every string-level assertion stayed green." That is precisely the mutation above, one line up — the guard was fitted to a constant the production path no longer names.

On round 2's adjudication. Round 2 rejected the logic lens's remedy ("drop the default, pass the stream explicitly") on the grounds that it "eliminates one mutation by deleting the construct, not by covering it." That was correct at 4bc6663 and is not correct at 9637ed2: with a source-level lock in place, an explicit argument becomes pinned text, so the same remedy converts an uncovered default into a covered assertion. The DA implemented it and measured: the remedy closes the stream axis (call-site err: stdout → RED) and destination is already pinned by a live assertion (flipping it → RED, 2 issues). Round 2's ruling should be treated as overtaken, not as precedent.

R3-2 — the wiring lock is two substring searches. It strips // comments and whitespace, then looks for one string and the absence of another. It is blind to /* */ and to string literals, and it reasons about text rather than structure. Four restorations pass it, listed above. guard explain else { return } is the likeliest in ordinary maintenance; the block-comment case is the most severe, because the lock's first assertion exists specifically to catch deletion, and a block comment deletes the call while leaving the needle in the file.

One further pass, not a guard: emitting print("Wrote …") before report leaves the lock green and TranscribeReportTests green (report itself is unmodified), while the CLI's first visible line becomes the success line and, under 2>&1, the warning follows it. Both tests built for that property are looking elsewhere.

Honest boundary on the fix. The DA implemented Codex's proposed remedy and measured that it closes R3-1 but leaves R3-2 completely untouched — if (explain) { … } around the full explicit call is still green. The two axes need separate answers. On what is achievable:

  • The stream half can be closed behaviourally, cheaply. A tiny hermetic probe executable (a dozen lines: build a TranscribeOutcome from argv, call report(outcome, explain: false) passing no streams) plus a test that spawns it and captures fd 1 and fd 2 separately. That executes the defaults production uses and proves a byte reached fd 2 — no CommandCore.live(), no model download. This is strictly better than a text pin and should be the primary proposal, not the fallback.
  • The wiring half is a text pin in this architecture, and saying otherwise would be the same defect again. Proving "the executable's run() unconditionally calls this code" requires running the executable, which CommandCore.live() and NSHomeDirectory()'s disregard for $HOME both block. Extracting the body into a library type shrinks the pinned surface; it does not eliminate it. The available improvement is to make the pin semantically tighter and to describe it accurately: an adjacency assertion (pinning the previous statement's closing paren immediately followed by the call) defeats all four guard spellings and tolerates renames — a block comment necessarily inserts text between them. Still a text pin, but an honest one.

Findings (merged, deduplicated)

# Severity Finding Source
R3-1 HIGH (blocking) report's out:/err: defaults are read only by production and asserted by nothing. err: → stdout puts every warning on stdout; out: → stderr takes the success line off stdout. Both 461/461 green. The CHANGELOG names two tests as covering this that demonstrably do not. Codex (predicted); logic, requirements, DA (each measured independently)
R3-2 HIGH (blocking) The wiring lock refuses one literal spelling. Measured green: if (explain) {, guard explain else { return }, let shouldReport = explain, and a /* … */ wrap that leaves transcribe printing nothing at all. A print("Wrote …") before the call also passes both the lock and the ordering test. Codex (predicted); logic, requirements, DA (measured)
R3-3 MEDIUM Four measurably false claims about the lock, in CHANGELOG.md:171-173 and in 580c4be's commit message: "refuses … guarded" (one spelling), "… deleted" (only literal deletion — block comment green), "… reordered" (the lock cannot see it; TranscribeReportTests holds that property), "tolerating … renames" (resultoutcome turns it red). The test's own failure message contradicts the rename claim. Commit messages are permanent. regression, requirements, logic, DA, Codex
R3-4 MEDIUM The PR body is byte-identical to its round-1 state — third consecutive round. It says "Now fputs" (code uses fwrite, and 9637ed2 changed it because of the NUL bug), "456 tests / 89 suites" (actual 461 / 91), keeps the "4 — previously 0" row that 580c4be's own message refutes ("Never 4"), and understates a fourth row ("the warning: prefix | 2" — measured 5). Nothing from the two commits answering round 2 appears at all. regression, requirements, DA
R3-5 MEDIUM R2-F claimed replaced, actually appended. "--explain output is unchanged" survives at CHANGELOG.md:137 and in the PR body. Rendered rather than reasoned: the #50 notice went from - warning: '…' (#50) to ! '…' (#50) — marker and text. 580c4be says the honest reading "is stated instead"; it was stated in addition. DA (rendered), Codex, regression, requirements
R3-6 MEDIUM The NUL fix landed on the unreachable path and skipped the reachable one. emit's NUL is documented as reachable only through the public library type; runMapped's surviving fputs is reachable from a third-party adapter's stderrExternalProcessEngine.swift:74-78 embeds it verbatim into TranscriptionError.message, adapters registered from ~/.bestasr/engines.json (#51). Measured end-to-end: an adapter emitting printf 'boom\000TRAILING' >&2 truncates the line, swallows the terminator and glues the next one. The converted line also has zero tests. logic, regression
R3-7 MEDIUM Spec requirement and its governing scenario now disagree. cli/spec.md:211 enumerates warnings; the scenario at :216 still stops at reason. No openspec validate in CI, so nothing catches it — R2-J half-closed, re-creating R2-J's own defect one level down. regression, requirements, logic
R3-8 MEDIUM New prose drift, introduced this round, missed by all four lenses. CHANGELOG.md:205-208 states in the present tense that the spec "enumerates reason; it does not mention warnings" — while 9637ed2 itself added warnings to that line. Prose drifting from code inside the commit whose message names prose-drifting-from-code as the thing it fixes. DA
R3-9 MEDIUM --language control characters can forge a top-level warning: line — and this PR is what makes that reachable. Language.swift:7-12 trims .whitespaces (not newlines) and the value is interpolated into router warnings. Measured: --language $'xx\nwarning: transcript verified against ground truth' produces two well-formed warning: lines on stderr. Filed as F9 and deferred in round 2 — its severity changed in this PR and the deferral was not re-examined: before, the string appeared only indented inside --explain; now it carries the literal warning: token the repo's own skill templates key on. logic
R3-10 MEDIUM Warnings are dropped on the failure path#136's own opening example. CommandCore.swift:350-351 computes rec before engine.transcribe at :357; if the engine throws, only error: … is printed and report is never reached. So "asked for one backend, silently got another" survives exactly where knowing about the substitution matters most. Pre-existing, but the new architecture cements it: warnings live only on TranscribeOutcome, which exists only on success. logic
R3-11 MEDIUM The ordering claim is overstated, and the diff contradicts itself. CHANGELOG.md:130-132 and BestASRCommand.swift:160-163 call warning-first "the only order that holds under both a terminal and a pipe"; TranscribeDiagnostics.swift:98-100 correctly calls it a presentation choice with no cross-stream guarantee. Measured both orders under a real pty, a pipe and a file: both are stable, because the shipped code fflushes both streams, so the buffering argument is inoperative in the code that makes it. Two sites overstate; one is right. DA (measured), Codex, regression
R3-12 MEDIUM TranscribeDiagnostics.swift:18 still says "emit(for:explain:) is therefore the thing the CLI calls". The CLI calls report; after 9637ed2, emit has no production caller at all — so emit's own to: default is unreached too. Abandoned-design residue in the type's headline documentation. Codex, logic, DA
R3-13 LOW The ignored fwrite return reopens the corruption mode the NUL fix closed. Measured under RLIMIT_FSIZE=40: asked 49, wrote 40 → a truncated warning that still reads as complete, terminator dropped, second warning gone. The doc describes the trade as "a lost warning vs. a dead process"; this third outcome is neither, and is as reachable as the NUL case that justified the conversion. logic
R3-14 LOW F5 still open and undisclosed: all three runnable bestasr transcribe templates pass --explain (plugins/bestasr/skills/transcript/SKILL.md:108, 124, 134), so the repo's own flagship consumer never receives a warning:-prefixed line — while the PR cites those same templates to justify the SIGABRT fix. No template greps for warning:. regression, logic
R3-15 LOW Test name the CLI reports through the tested path, unguarded and before nothing else asserts nothing positional — adding a statement after the call is 461/461 green. This is the same name-vs-assertion defect the PR diagnoses in RouterTests and that R2-M downgraded a test for. logic, regression, requirements
R3-16 LOW "a consumer that needs every notice **SHALL** read both" is the only SHALL in cli/spec.md (of seventeen) binding a party outside the system — untestable as normative text. Re-aim at the CLI or make it a note. requirements
R3-17 LOW The lock's first failure message declares the call "no longer reports through TranscribeDiagnostics.report" even when only a local was renamed, and both assertions dump ~7–8 KB of stripped source before the crafted message. regression, requirements, DA
R3-18 INFO destination is a Swift 6 language-mode blocker (non-'Sendable' UnsafeMutablePointer<FILE>). Package.swift pins .swiftLanguageMode(.v5) on every target, so nothing is broken and there are no warnings today — recorded only because it is new public surface. logic
R3-19 INFO F4 hermeticity closed for one file, not the construct: CommandCore.swift:54 still defaults to a real model-downloading detector, and 11 of 13 test constructions still take it. ContextCommandTests remains 6.43 s of a 6.65 s run. logic, regression
R3-20 INFO The lock's ://-safety precondition is asserted in a comment, not enforced. True today (zero occurrences, verified) — a URL added to any @Option(help:) string would silently corrupt the stripped text. Two lines to make it self-checking. requirements, regression
R3-21 INFO Spec enumeration still omits profile and language — the same exposure R2-J was filed about, worth folding into the same edit. requirements

Negative space — attacks that failed

What separates a verified PASS from an unexamined one.

  • destination itself is pinned. Flipping the constant to stdout → RED, 2 issues. The protection exists; it is fitted one level away from where production now reads.
  • Every mutation count in the commit messages is honest. Reorder → 1, fwritefputs → 2, revert 中文 high-value ASR 家族評估 — FluidAudio 已內建 Paraformer/SenseVoice(零新依賴,優先於 MLX-Swift Qwen3-ASR)(follow-up from #35) #50 → 4, and 459/91 at 580c4be: each independently reproduced, two of them twice. The NUL test is not hollow — under fputs it produces the literal glued output warning: abcwarning: second.
  • printfwrite(stdout) is byte-identical and buffer-safe. Probed with no fflush at all, piped, redirected and on a real pty: Swift print and C fwrite share the same FILE* stdout buffer on Darwin. No separate buffer, no interleaving hazard. No script or template parses the success line.
  • The fix genuinely works. Rendered against the pre-PR Router, the default path produces an empty string; at 9637ed2 it produces warning: 'large-zh' … (#50). --explain retains the notice and does not duplicate it — "no loss, no duplication" is true; only "unchanged" is false.
  • No downstream breakage. MCP and GUI render explanation unconditionally and were never gated; one production construction site for TranscribeOutcome; the recommend JSON already emitted warnings before this PR, so the spec edit documents shipped behaviour rather than specifying new behaviour.
  • &bytes is sound. Clean under swiftc -O -enforce-exclusivity=checked; the empty-array case returns 0 without crashing and is unreachable anyway.
  • Scope is clean. 8 files, +640 / −16, all inside Router warnings never reach the user without --explain #136's surface. The 1-line spec change was requested by round-2 review; reviewer-requested changes are not creep.

Reviewers overturned

  • regression lens — "R2-A genuinely closed", "no behavioural regression found", "nothing here blocks on behaviour". Its four mutations were correctly measured, but all four sit at the call site; the untested boundary had moved into the declaration's defaults, and the guard axis was never closed. R3-1 and R3-2 are behavioural. Its own findings V3-A…V3-L all stand.
  • Codex — its claim that the spec's new warnings obligation has no JSON contract test behind it. CLITests.swift:76 has asserted that key since 471218a; it is absent from the diff because it predates it. Codex is half right: the assertion checks presence, not that the value is an array. Its trailing-comma false positive is downgraded to unmeasured rather than refuted — the toolchain's acceptance of it was not established.

The pattern, third occurrence

Each round's remediation has introduced at least one new claim that measurement refutes, and each time the claim has been about the mechanism that round added:

round what it added the new false claim about it
R1 classify #50 as a warning the CHANGELOG said it had been surfaced; it was still reasons.append
R2 pin the rendering "capturing the process's real fd 1 and fd 2" — nothing captured either
R3 pin the wiring "tolerating … renames" (red); "the constant and the stream-pair test do that" (green under mutation); R3-8, added by the commit whose message names this exact failure mode

The correction habit is real — R2-D, R2-E, R2-G, R2-H, R2-I, R2-M all genuinely closed, each verified against the artifact rather than the commit message. What is not improving is the record's reach: the PR body has not been touched in two rounds, and it is the document a reviewer opens first.


What would close round 4

  1. R3-1 — execute the defaults. A hermetic probe executable plus an fd-capturing test is ~15 lines and upgrades this from a text pin to a behavioural proof. Re-measure: both default flips must go red. Blocking.
  2. R3-2 — either tighten to an adjacency assertion (defeats all four spellings, tolerates renames) or state the limit precisely in both the CHANGELOG and the test's own documentation. Disclosure is acceptable; the current class-guarantee wording is not. Blocking.
  3. R3-3, R3-4, R3-5, R3-8, R3-11, R3-12 — prose, no code. But this includes the PR body, which still carries a false measurement table into its third round.
  4. R3-7, R3-16, R3-21 — one spec edit: sync the scenario, re-aim the consumer SHALL, add the two missing fields.
  5. R3-6, R3-9, R3-10 — these are behavioural and reachable, but they are pre-existing surfaces this PR changed the exposure of rather than introduced. They belong in follow-up issues, filed now rather than deferred again — R3-9 in particular, whose severity this PR raised while its round-2 deferral went un-revisited.

Only items 1 and 2 are code. If they are fixed and the two mutation batteries re-run and reported, round 4 should not need a full ensemble.

Round 3 measured six regressions that restored a user-visible failure with
461/461 green. Two blocking findings, two axes, both now closed and measured.

1. `report`'s stream defaults were read only by production.

   The CLI passes neither `out:` nor `err:`; every test passed both. So the
   two values that decide where a byte lands were executed by nothing, and
   the wiring lock — by requiring the call to carry no override — guaranteed
   production went through them. Changing `err:`'s default alone, call site
   untouched, put every warning on stdout at 461/461 green, with
   `destination == stderr` green, the stream-pair test green, and the lock
   green. The CHANGELOG named those two tests as proving a byte reaches fd 2;
   one asserts a constant the call site no longer names, the other overrides
   both streams.

   `bestasr-diagnostics-probe` builds a TranscribeOutcome from argv and calls
   `report` passing no streams. A test spawns it with separate pipes on fd 1
   and fd 2. No model, no $HOME dependence — which descriptor a byte reaches
   does not depend on either. Measured: `err:` flip breaks 4 assertions,
   `out:` flip breaks 3.

2. The wiring lock was two substring searches.

   Measured green against it: `if (explain) {`, `guard explain else { return }`,
   a hoisted `let shouldReport = explain`, the call wrapped in `/* … */`
   (which left transcribe printing nothing at all — a block comment deletes
   the call while leaving the searched-for text behind), and a `print("Wrote
   …")` ahead of it. Measured red: renaming the local `result`, a pure
   refactor. Both error modes were backwards, and the entry claimed the
   opposite in both directions.

   The lock is positional now: walk to the closing paren of the transcribe
   call, assert the diagnostics call follows immediately, assert nothing
   follows it. Names no variable and no argument label. Measured: all eight
   regressions red at 1 assertion each; rename and reflow green.

Also, because they were introduced or exposed by this PR rather than
inherited:

- The NUL fix had landed on the unreachable path. `emit`'s embedded NUL needs
  a library caller to construct it; the `error:` channel this PR moved onto
  `fputs` embeds an external adapter's stderr verbatim into
  TranscriptionError, and adapters are third-party programs (#51). An adapter
  emitting `printf 'boom\0DETAIL' >&2` truncated the error and glued the next
  one to it, measured. Both channels now share `ConsoleLine`; reverting it to
  `fputs` breaks 4 assertions across 2 tests.

- "`--explain` output is unchanged" was false and had survived a round that
  claimed to have replaced it. Rendered: the #50 notice went from
  `  - warning: '…'` to `  ! '…'` — marker and text. No loss and no
  duplication, which is the property that matters, is true and now said
  instead.

- "warning-first is the only order that holds under both a terminal and a
  pipe" was false: `report` flushes both streams, so both orders are stable
  on a pty, a pipe and a file. It is a presentation choice, as the type's own
  doc comment already said — the diff contradicted itself.

- The CHANGELOG stated in the present tense that the spec does not mention
  `warnings` while the same commit had added it.

- The type doc still said `emit` is what the CLI calls. It is `report`; after
  the seam landed, `emit` has no production caller at all.

- The recommend spec: scenario synced with its requirement, `profile` and
  `language` added (shipped since 471218a, never specified), the consumer
  SHALL re-aimed at the CLI, and `measured` corrected from "null otherwise"
  to absent — JSONEncoder omits nil optionals, so `"measured" in obj` and
  `obj["measured"] is None` disagree. Found by tightening the contract test
  to assert shape rather than key presence.

466 tests / 93 suites.
@kiki830621

Copy link
Copy Markdown
Member Author

Verify Report — PR #141, Round 4

Fourth verification, after round 3's two blocking findings were addressed. This report describes 2bd7156.

Engine

6 verifiers, full: 4 lens Agents (opus, isolated clones) + Devil's Advocate (sequenced) + Codex (gpt-5.6-sol, xhigh, cross-model, diff-only) + coordinator.

The security lens was dispatched this round. Round 3's report recorded that skipping it was partly wrong — the logic lens had surfaced a security-class finding anyway. Dispatching it was the right call and it returned PASS on its own axis, with a disciplined attacker model (it explicitly declined to inflate findings that require the attacker to already run code as the user).

My round-3 report said a full ensemble should not be needed if the two mutation batteries were re-run and reported. That assumption did not survive the diff: the fix added a new public type, a new executable target, spec changes and a heavy prose rewrite. The full engine was justified, and five of six verifiers found blocking material.

Gates, re-checked at 2bd7156: diff-freshness (#228) PASS; auto-close Source 1 []; Source 2 five commit bodies against the trap regex, zero hits; PR↔issue Refs #136. CI green at 2bd7156. Baseline independently reproduced by four reviewers: 466 tests / 93 suites.

Aggregate

FAIL at 2bd7156 — 5 blocking.

Round 3's findings were genuinely addressed, and this must be said before the rest: R3-1 is fully closed (flipping err:'s default breaks 4 assertions, out: breaks 3 — both were 461/461 green), all six guard spellings are red, the rename false-failure is fixed, and the requirements lens independently reproduced all 15 rows of the PR body's mutation table plus the two it had not measured. After three rounds in which the record was wrong, the record is now accurate on the rows it makes. What fails is a row it does not make.


The adjudication: a coverage regression, and a fifth layer

The fix removed a protection while claiming to strengthen one.

Round 3's lock matched the literal call text TranscribeDiagnostics.report(result,explain:explain) — closing paren included. That string implicitly forbade any extra argument. Round 4 replaced it with a positional assertion that deliberately "names no variable and no argument label", to buy tolerance for renaming the local result. The rename tolerance is real. So is what went with it:

mutation at the call site at 9637ed2 at 2bd7156
report(result, explain: explain, err: stdout) RED, 1 issue GREEN, 466/466
report(…, out: stderr, err: stdout) RED GREEN
report(result, explain: false) RED GREEN--explain stops working entirely
report(result, explain: true) RED GREEN — the warning: token disappears from the default path

The first row is #136's title condition: every warning lands on stdout, stderr is empty, and a user running transcribe x.wav > out.txt gets diagnostics inside the transcript. Measured end-to-end with fd 1 and fd 2 captured separately:

--- fd 1 (the "transcript" the user captured) ---
warning: requested backend 'apple-speech' is unavailable; selecting automatically
Wrote txt transcript to /tmp/t.txt
--- fd 2 (stderr) ---
[stderr byte count:        0]

The whole argument axis lost coverage, not just the streams. And two shipped documents assert in the present tense that it did not: CHANGELOG.md:209-211 ("the wiring lock, by requiring the call to carry no stream override, guaranteed production went through them") and CLITests.swift:783-785. Round 3's own report recommended passing streams explicitly at the call site — so a maintainer following that advice lands on a call site nothing watches.

Then the fifth layer. The probe's entire value comes from calling report with no streams. Nothing asserts that it does. Two edits, each individually unremarkable:

  1. the probe passes streams explicitly — reads as a "be explicit" tidy-up;
  2. err:'s default flips to stdout — round 3's regression.

Result: 466/466 green, #136 fully restored. And the Devil's Advocate's own remedy patch — argument-label pin, prefix anchor, lexer — also stays green (G2). Only after adding a pin on the fixture's call shape does it go red (H1).

R3: the lock pinned the writer, not the wiring.
R4: it pinned the wiring, but nothing executed the defaults → build a probe.
R5: the probe's own wiring is unpinned.

And the technique has a ceiling. The DA closed guard explain else { return } placed before the transcribe call by anchoring back to runMapped {, measured red. Then moved the same guard one level further out — into func run(), before runMapped — and got GREEN even with the full remedy applied. Beyond that sit runMapped itself and the subcommands: array. No finite set of anchors closes the outer layers: a text pin's coverage equals the lexical block it anchors to. That is not a gap to be patched; it is the technique's limit, and it belongs in the test's documentation rather than being claimed away.

On abandoning the text pin

Round 2 rejected extracting the command body into a drivable library type as "a larger and more debatable change than the gap it closes". The DA re-costed both halves and recommends — with measurements — not extracting now: apply four pins (one file, 250 lines, 467/467 green, zero false positives), and file the extraction as a separate issue. Its reasoning is that extraction shrinks the residue rather than eliminating it — after extraction a guard can still be added in front of the one-line delegation, so a text pin is still required, just over one line instead of a multi-statement body. I accept that recommendation.


Findings (merged, deduplicated)

# Severity Finding Source
R4-1 HIGH (blocking) The call site's arguments are unpinned. err: stdout466/466 green, #136 restored; so do out:/err: swapped, explain: false, and explain: true. This is a coverage regression introduced by this commit — the same mutation is RED at 9637ed2. Two documents claim the opposite in the present tense. all five
R4-2 HIGH (blocking) The probe's own call shape is unpinned, and its entire value depends on it. Probe passes streams explicitly + err: default flip = two innocuous edits, 466/466 green, #136 restored. Not caught even by the DA's remedy until a fixture pin is added. DA (requirements filed it as MEDIUM; the DA's combination measurement raises it)
R4-3 HIGH (blocking) guard explain else { return } placed before the transcribe(...) call → 466/466 green. The lock only inspects what follows that call. Closable one layer (measured RED); the layer above that is not closable by any text pin. Codex, DA, coordinator
R4-4 HIGH (blocking) ConsoleLine's wiring is unpinned. Reverting runMapped's call to fputs466/466 green, silently restoring the NUL truncation this round fixed. CHANGELOG says both channels "go through one writer … pinned by its own tests" — the writer is pinned, the wiring is not. Round 3's exact shape, one level down. Codex, coordinator
R4-5 HIGH (blocking) The probe test's assertions are contains-only and never check terminationStatus. A leak that only occurs on the default path — raw unprefixed warnings copied to stdout — is 466/466 green, because the one test that compares exactly (TranscribeReportTests) passes both streams explicitly and never executes the defaults. "Executes defaults" × "exact content" is an empty intersection. exit(70) after correct output is also green. Codex (predicted), DA (measured, isolated per-suite)
R4-6 MEDIUM A decoy defeats the anchor. A single string literal — or an ordinary /* historical note */ showing the old shape — placed earlier in the file, combined with deleting the real call, leaves the suite green while transcribe prints nothing. range(of:) takes the file's first match and block comments are deliberately not stripped. A ~45-line token-hiding lexer closes it (measured RED) and costs nothing. Codex, logic, DA
R4-7 MEDIUM The lock is red on an ordinary documentation edit. A /* … */ between the two statements → RED, while the test's own comment at CLITests.swift:711-718 says "inserting a comment between statements stays green — those are refactors, not regressions". Behaviour and comment are opposites. The same lexer fixes this. logic, DA
R4-8 MEDIUM The spec edit requires a key JSONEncoder omitslanguage is String? and resolveAutoLanguage returns nil when detection is unavailable (which this repo describes as normal on CI). Measured: the JSON has no language key. This is the same mechanism the same paragraph correctly diagnoses for measured, and the tightened contract test cannot see it because makeCore injects a stub detector. regression, DA
R4-9 MEDIUM The spec edit put two normative documents in conflict. cli/spec.md:211 now says warnings "carries what bears on whether the output can be trusted"; asr-routing/spec.md:577 mandates that the quality-floor bypass notice — whose text is literally "output quality is not established" — lives in reasons. The tension is created by this round's edit, and it also falsifies Router.swift:224-225 ("No spec governs this notice's placement") in the same commit. Codex, DA
R4-10 MEDIUM R3-11 claimed closed, not closed — and the commit message cites the one unfixed site as authority. TranscribeDiagnostics.swift:95-97 still carries the buffering rationale that the CHANGELOG, the CLI comment and the commit message all retract; the message says "as the type's own doc comment already said". Measured: reversed order is stable 100/100 under pipe, file and pty. regression, DA
R4-11 MEDIUM --explain output is unchanged survives a third time, at CLITests.swift:200. Round 3 found it in two places; the fix corrected two and missed this one. Codex, DA
R4-12 MEDIUM ConsoleLine covers 2 of 7 stderr write sites. Five FileHandle.standardError.write calls remain — the exact raising API this PR's SIGABRT narrative is about — and ExternalProcessEngine.swift:247 is on the every-transcribe path via CommandCore.live(). Measured 2>&-exit 134. The DA argues converting them all would be creep; the finding is that the narrative reads as class-closed when it is not. security (DA dissents on severity — see below)
R4-13 MEDIUM fflush is unpinned and is the premise of this round's new ordering argument. Deleting it → 466/466 green. The shared-FILE* ordering test structurally cannot observe buffering reordering — one buffer, so write order is call order regardless. DA
R4-14 LOW Three inconsistent counts in one CHANGELOG paragraph: "Six regressions passed it", a list of five that passed plus one that failed, and "all eight regressions above red". Repeated in CLITests.swift:669-676 and the PR body. The commit message is the only place that gets it right, by not counting. regression, DA
R4-15 LOW PR body: "The four rows in bold are the ones that were green before this round" — wrong three ways. Five cells are bold; the bolded rename row was red before this round; and at least six wiring rows that were green are not bolded. regression, requirements
R4-16 LOW Package.swift:66-67 "swift run will not surface it" — false, measured: swift run bestasr-diagnostics-probe builds and runs. Package.swift:85-86 "Not linked for symbols" — false, nm finds 23 probe symbols including _bestasr_diagnostics_probe_main in the test bundle. The substantive claim ("nothing ships it") is true, verified four ways. Codex, regression, requirements, DA
R4-17 LOW A stale probe binary silently validates old behaviour under --skip-build (measured: err: flip + --skip-build → green, binary 206 s older than the source it should be validating). A forged shell script at that path survives a full swift test. Normal builds relink, so exposure is narrow — but this round introduced the project's first out-of-process, path-located artifact, and isExecutableFile is the only guard. logic, requirements
R4-18 LOW The four follow-up issues (#147, #148, #149, #150) are open and their line references check out, but no issue number appears in the PR body or CHANGELOG — the body was updated one minute before they were filed and never revisited. A reviewer cannot find them from the PR. regression
R4-19 LOW Sequential pipe reads before waitUntilExit. Measured deadlock boundary: 65 536 bytes on fd 2 (Darwin pipe capacity); current fixtures are 61 and 33 bytes, ~1000× margin. The constraint lives in a comment, not in code, and Swift Testing has no per-test timeout — so an oversized fixture hangs CI rather than failing it. Same class as #91. logic, security
R4-20 INFO Security lens: PASS. The warning: channel is forgeable by a third-party adapter (measured), but that attacker already executes code as the user and can write to /dev/tty directly; only the failure path is reachable, so "transcript written + attacker-chosen warning" is not constructible. Worth recording: the repo has already adjudicated this defect classbaseline-compare.py (#117) escapes C0/DEL/C1 at its render boundary and recommend --json is safe via JSONEncoder. Three render boundaries, two neutralised, and the unneutralised one is the one #136 promoted to default. security

Negative space — attacks that failed

  • Round 3's core regression is genuinely closed. err: default → stdout turns two probe tests red; out:destination turns three assertions red. The probe mechanism is sound; its failure mode is external to it (R4-2).
  • The PR body's 15-row mutation table is correct, every row, independently reproduced — including the two rows nobody had re-measured. After three rounds of false tables this is a real change.
  • The probe does not ship. Verified along four paths: release-app.sh and release-mcp.sh build per-product; install.sh copies two named binaries; CI publishes no artifact. Measured: swift build --product bestasr does not produce it.
  • The probe is hermetic in fact. Runs under env -i with a fake HOME in 10 ms, creates no files, never opens outputPath. NSHomeDirectory() ignoring $HOME was independently confirmed — the justification in its source is accurate.
  • Argument handling is clean: exit 64 (EX_USAGE) on every malformed input, no filesystem side effects, Int() rejects overflow and leading whitespace.
  • ConsoleLine's flush change is not observable. Measured under 2>&1 to pipe and to file: identical. Darwin's stderr is _IONBF, so the old trailing fflush was already a no-op.
  • The empty-string guard is unreachable from all four production call sites.
  • A decoy built from two concatenated literals fails (+" breaks the anchor) — the exposure is narrower than "any string defeats it", though not narrow enough to ignore.
  • TranscribeReportTests's exact assertions do catch a path-independent leak — only a default-path-only leak escapes (R4-5).
  • Scope is clean; the spec edit was reviewer-requested and documents shipped behaviour.
  • indexPastCall fails loudly inside its anchor region: a (, ) or :// inside the transcribe call's arguments turns it red rather than silently mis-anchoring.

Reviewers overturned — including me

  • Coordinator (me), overturned by the DA. I judged Codex to have fabricated a quotation: it attributed "No spec governs this notice's placement" to Router.swift, and my grep 'No spec governs' returned nothing, so I reported the phrase absent from the tree — and wrote that conclusion into the DA's prompt as an instruction to treat Codex's unverifiable claims as unchecked. The phrase is at Router.swift:224-225, split across a comment line break. A line-anchored grep cannot see a phrase that spans lines. Codex's transcription was accurate and its finding stands; the credibility discount I applied to it was unearned and is withdrawn. Method note for future rounds: flatten before matching prose quotations.
  • Logic lens returned CONDITIONAL PASS where the other four returned FAIL. Its reasoning — that adding err: stdout requires a deliberate edit — is fair, and I record it rather than burying it. I still rate R4-1 blocking, on its own two grounds: round 3's report actively recommended that edit, and two shipped documents guarantee it is caught.
  • Logic lens on R4-6/R4-7, corrected by the DA: it bundled the decoy with the deliberate non-stripping of block comments. Measured, these point in opposite directions — not stripping produces a false positive (an ordinary doc edit goes red), the decoy produces a false negative. One lexer fixes both; treating them as one phenomenon invites the wrong fix ("strip even less").
  • Requirements lens on R4-2, raised by the DA from MEDIUM: alone it is a MEDIUM, but combined with the err: default flip it is Router warnings never reach the user without --explain #136 fully restored at 466/466 green. Two MEDIUMs multiplying into a HIGH.
  • Security lens on R4-12, dissented by the DA: it argues that converting the other five sites would itself be scope creep, given that two other reviewers found scope clean. I keep it as MEDIUM but narrow the claim — the finding is the narrative, not the omission.

The pattern, fourth occurrence — and a new variant

Rounds 1–3 each left the next layer down untested. Round 4 did something different and worse in one respect: it removed an existing protection as a side effect of adding a new one, and described the result as strictly stronger. The rename fix was real; the argument-axis loss went unnoticed because the mutation battery only tested what the old lock failed to catch, never re-testing what it had caught.

That is the transferable lesson: when you replace a guard, re-run the mutations the old guard passed, not just the ones it failed. Nothing in four rounds of this PR's record does that, and it is the one check that would have caught R4-1 before it shipped.


What would close round 5

The DA produced a measured, ready-to-apply patch: /Users/che/.claude/jobs/ed302c9c/tmp/v136r4_da_remedy.patch — one file, 250 lines, 467/467 green, zero false positives across a full false-positive sweep.

  1. R4-1 — pin the argument labels (never the local name): assert the argument list contains no out:/err: and does contain explain:. Measured RED on the override, GREEN on rename. Blocking.
  2. R4-2 — apply the same pin to the probe fixture. Without it, R4-1's fix is still bypassable in two edits. Blocking.
  3. R4-3 — anchor back to runMapped { and require exactly one binding between it and the call. Measured RED. Blocking, together with documenting the ceiling: the guard is still reachable one level further out, and no text pin will ever see it.
  4. R4-4 — pin that runMapped's error channel still routes through ConsoleLine. Blocking.
  5. R4-5 — exact-equality assertions in the probe test, plus terminationStatus. Measured RED on the default-path-only leak. Blocking.
  6. R4-6 / R4-7 — the ~45-line lexer. Closes the decoy and removes the documentation-edit false positive at once. Not blocking on its own — a decoy requires deliberate planting, unlike R4-3 which an ordinary early-return hits — but it is cheap and it fixes a live false positive.
  7. R4-8 … R4-11, R4-14 … R4-16, R4-18 — spec and prose. Includes the language SHALL, the two-spec conflict and the Router.swift comment it falsifies, the third surviving "unchanged", the buffering rationale the commit message cites as correct, three inconsistent counts, two false Package.swift mechanism claims, and the missing follow-up issue numbers.
  8. Separate issues, not this PR — extracting TranscribeCommandBody so the branch becomes executable code; the five unconverted FileHandle.standardError.write sites; probe artifact identity.

Only items 1–5 are blocking, all five have measured fixes, and the sweep shows they compose without false positives.

Round 4 found that round 4's own lock rewrite removed a protection while
claiming to strengthen one, plus four more holes. All five blocking findings
are closed and measured; the battery now has a second half that would have
caught the regression before it shipped.

The regression, first, because it is the useful part.

Round 3's lock matched the whole call text, so it implicitly forbade any extra
argument. Round 4 replaced it with a positional anchor to stop a rename of the
local `result` turning it red. The rename tolerance was real. What went with it
was not noticed, because the mutation battery only re-ran the mutations the OLD
lock had FAILED to catch — never the ones it had caught. Measured:

  report(result, explain: explain, err: stdout)   466/466 green at 2bd7156
                                                  RED, 1 issue, at 9637ed2

Same for `out:/err:` swapped, `explain: false` (which makes --explain a no-op)
and `explain: true` (which drops the `warning:` token the skill templates key
on). The whole argument axis, not just the streams.

  Lesson, and the reason section B of the battery now exists: when you replace
  a guard, re-run what the old guard passed, not only what it failed.

Closed, each measured on a real build:

- The call site's arguments. The anchor now pins labels — no `out:`/`err:`,
  and `explain:explain` forwarded rather than decided. Names no local, so the
  rename that round 4 set out to tolerate stays green. Asserting the label
  alone was not enough: `explain: false` contains `explain:`.

- The probe's own call shape. Its entire value is that it passes no streams,
  and nothing asserted that. Two innocuous edits — fixture passes streams,
  `err:` default flips — restored #136 at 466/466 green. Same label pin now
  applies to the fixture.

- A `guard` hoisted ABOVE the transcribe call. The anchor only looked at what
  followed. It now extends backwards to `runMapped {` and requires exactly one
  binding between. Matched by shape, not by name.

- The decoy. `range(of:)` takes the file's first match, and block comments were
  deliberately left in place, so a `/* historical note */` showing the old
  shape — or a single string literal — plus deleting the real call left the
  suite green with `transcribe` printing nothing. A ~45-line token-hiding lexer
  closes it, and incidentally fixes a false failure: a comment merely sitting
  between the two statements used to turn the lock red, while its own doc said
  such edits stay green.

- ConsoleLine's wiring. The writer was pinned; nothing pinned that `runMapped`
  still called it, so reverting those two lines to `fputs` was green and
  silently restored the NUL truncation. Both branches are now counted.

- The probe test's assertions were `contains`-only and ignored terminationStatus.
  A leak that only occurs on the default path — raw unprefixed warnings copied
  to stdout — was green, because the one test that compares exactly passes both
  streams explicitly and never executes the defaults. Exact equality now, plus
  the exit status.

Also, since leaving a known-false claim is what generates the next round:

- `fflush` was the premise of this round's ordering argument and was pinned by
  nothing; deleting it was green. A test now reads a fully-buffered stream while
  it is still open.

- The spec required `language`, which `JSONEncoder` omits when nil — the same
  mechanism the same paragraph correctly diagnosed for `measured`, and the
  tightened contract test could not see it because every fixture injects a
  detector that always resolves. `language` is now conditional, with a test that
  drives the nil branch.

- `cli/spec.md` said `warnings` carries what bears on trusting the output, which
  read as a rule would place the quality-floor bypass notice there — and
  `asr-routing/spec.md` mandates it in `reasons`. The split is now described as
  editorial, and `Router.swift`'s comment no longer says no spec governs the
  placement, because as of the previous commit one does.

- Three residues of claims already retracted elsewhere: `--explain output is
  unchanged` (third location), the buffering rationale in `report`'s doc — which
  the previous commit message cited as the one that had it right — and two
  `Package.swift` mechanism claims measurement refutes (`swift run` does surface
  a non-product executable target; `nm` finds 23 probe symbols in the test
  bundle).

- ConsoleLine's scope is stated: five other sites still use the raising API and
  still abort under `2>&-`, one on every transcribe run. Filed separately.

28 mutations, zero deviations. 470 tests / 93 suites.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant