Skip to content

feat(pipeline): verification cannot lie — a blind empty diff never reads as "no changes" - #352

Merged
macanderson merged 2 commits into
mainfrom
feat/verification-cannot-lie
Jul 23, 2026
Merged

feat(pipeline): verification cannot lie — a blind empty diff never reads as "no changes"#352
macanderson merged 2 commits into
mainfrom
feat/verification-cannot-lie

Conversation

@macanderson

Copy link
Copy Markdown
Owner

Principle #4 of the loop-hardening thread — the scariest one, because a wrong verification signal doesn't just fail, it misdirects.

The lie

On Terminal-Bench the judge was handed an empty diff and concluded "no changes were made to the repository" — while the agent's files sat on disk. The agent's rational response to a false signal was to reinitialize git to make the signal non-empty. Verification didn't just miss; it actively steered the agent wrong.

Why an empty diff is a lie waiting to happen

Empty is ambiguous: the agent genuinely changed nothing, or the diff machinery is blind (work committed, a baseline miss, an uncaptured file). Handed a bare empty string, a judge reads the second case as the first.

The pipeline already carries the signal that resolves the ambiguity: file_changes, the count of FileChange events the turn emitted. verification_honest_diff uses it — when file_changes > 0 but the gathered diff is empty, it substitutes:

[N file-change event(s) were observed this turn, but the diff could not be
 captured — the change is real; the diff is blind to it. This is NOT evidence
 that nothing changed; verify the result on its own merits.]

Applied at both diff-gather points (initial verify + post-revise), so every downstream consumer — the model judge, the distress-guidance call, the recorded evidence — sees a "couldn't verify", never a false "verified nothing". The judge's evidence summary now states the file-change count explicitly too.

That's the confidence notion the principle calls for: distinguish "verified false" from "couldn't verify" so a blind signal can never be read as a negative one. (The deterministic paths were already honest — the zero-diff guard keyed on file_changes, not the diff.)

Tests

Pin the pure guard: a blind empty diff with file changes reports "uncaptured" and forecloses the "no changes" reading; a truly empty diff with zero file changes stays empty (no invented changes); a real diff passes through untouched. cargo test -p stella-pipeline (156) + clippy -D warnings green; the chaos harness still passes.

Note

The root-cause diff-blindness (bare git diff blind to committed work) was fixed earlier by diffing against the session baseline commit. This is the structural guard on top: even if some future diff path goes blind again, it can no longer be read as "nothing changed".

…ound

The harness enumerates the provider x isolation x class x model x witness x
budget x bypass cross-product (288 scenarios) and asserts two invariants on
EVERY one: (1) named termination — a run ends only by Ok(outcome) or a typed
Err, an Ok(Aborted) carries a non-empty reason, and a terminal Complete/Error
reaches the stream; (2) never choose nothing — a run that executed zero
worker turns and did not complete may only be a RECOGNIZED cause (budget,
scope-user-abort, unresolvable provider, or a deliberate witness
artifact-integrity fail-closed), never a silent setup death. Deterministic,
milliseconds, and a break names the exact scenario.

Building it earned its keep immediately. My first cut was vacuous — it
inferred "worker executed" from transcript length, but run() appends the user
message before execution, so the check never fired. Fixed to key on the
worker's StepUsage event, the harness went red on a 7th zero-work death that
#345 had missed:

  witness AUTHORING failures (no TEST_COMMAND, an unusable command, a test
  that proves nothing, the author engine getting stuck) aborted the whole
  task. The task needs no witness to proceed — so these now degrade to a bare
  worker run. Only witness artifact-INTEGRITY violations (the author modified
  tracked files, produced a non-single-file/symlink artifact, or a
  runner/identity mismatch) stay fail-closed, surfacing the problem instead
  of silently completing unverified. witness_stage now returns a typed
  WitnessAbort { reason, degradable } to draw that line.

Tests reconciled: the still-passes and metering cases degrade (author+repair
still individually metered); the production-edit security case is scripted to
reach the tracked-mutation check and stays fail-closed.

Signed-off-by: macanderson <mac@oxagen.sh>
…ads as "no changes"

Principle #4 of the loop-hardening thread, and the scariest failure this
codebase hit: a wrong verification signal does not merely fail, it
MISDIRECTS. On Terminal-Bench the judge was handed an empty diff and
concluded "no changes were made to the repository" while the agent's files
sat on disk — the agent's rational response was to reinitialize git to make
the signal non-empty.

An empty diff is ambiguous: the agent genuinely changed nothing, OR the diff
machinery is blind (work committed, a baseline miss, an uncaptured file). The
pipeline already carries the signal that resolves it — file_changes, the
count of FileChange events the turn emitted. When that is positive but the
gathered diff is empty, verification_honest_diff replaces the bare empty
string with an explicit "the tree changed but the diff could not be captured
— this is NOT evidence that nothing changed" marker. Applied at both
diff-gather points (initial verify and post-revise), so every downstream
consumer — the model judge, the distress-guidance call, the recorded
evidence — sees a "couldn't verify", never a false "verified nothing". The
judge's evidence summary now also states the file-change count explicitly.

This is the confidence notion the principle calls for: distinguish "verified
false" from "couldn't verify" so a blind signal can never be read as a
negative one. The deterministic paths were already honest here (the
zero-diff guard keyed on file_changes, not the diff).

Tests pin the pure guard: a blind empty diff with file changes reports
"uncaptured" and forecloses the "no changes" reading; a truly empty diff with
zero file changes stays empty (no invented changes); a real diff passes
through untouched.

Signed-off-by: macanderson <mac@oxagen.sh>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @macanderson, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
stella-cli-docs Ignored Ignored Jul 23, 2026 6:43am

@macanderson
macanderson marked this pull request as ready for review July 23, 2026 06:58
@macanderson
macanderson merged commit 8291eb5 into main Jul 23, 2026
6 of 7 checks passed
@macanderson
macanderson deleted the feat/verification-cannot-lie branch July 23, 2026 06:59
macanderson added a commit that referenced this pull request Jul 23, 2026
* fix(cgp): declare §B3-honest frame token_cost (budget_tokens)

The CGP pin rebase (#354) claimed to adopt "canonical token accounting" but
kept the old estimate: the store declared
`estimate_tokens(content) + estimate_tokens(title)` and the graph declared
`estimate_tokens(content).max(1)`. Under the pinned protocol, §B3 requires
`token_cost == budget_tokens(content)` — the canonical inline count
(ceil(bytes/4)), exact, no tolerance. Neither builder was honest: the store
added the title's tokens (always non-zero) and used chars not bytes; the
graph's `.max(1)` diverges on empty content.

The existing conformance gate didn't catch it: its probe query returns zero
frames from the mismatched seed, so §B3 never fired on a real frame.

Fix: both production builders now declare `budget_tokens(content)`.
`pack_to_budget` already packs against each frame's own token_cost, so the
summed-budget invariant is preserved. New `recalled_frames_declare_honest_token_cost`
drives the real `recall` builder with a query that provably surfaces a frame
and asserts `declares_honest_token_cost()` — it fails on the pre-fix code
(token_cost 18 vs canonical 15) and passes after. A matching assertion guards
the graph builder. Dead `estimate_tokens` helpers removed.

* fix(ci): clear two pre-existing clippy -D-warnings blocking CI

`main` (b0117f3) fails `cargo clippy --workspace --all-targets -- -D warnings`
— the exact CI gate — on two warnings that landed unprotected:

- `run_shared_candidates` (pipeline.rs, from #352) lacks the
  `#[allow(clippy::too_many_arguments)]` its five sibling functions in the
  same file already carry.
- `use contextgraph_types::Representation` (contextgraph.rs, from #354) is
  used only in a `#[cfg(test)]` helper, so it is unused in the bin build.
  Import removed; the one test use is fully-qualified.

Both unrelated to the token_cost fix but required for this PR's CI to be green.
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