Skip to content

fix(report): re-derive results.vulnerable from deduped findings; emit protected as its own key - #381

Merged
gadievron merged 1 commit into
masterfrom
fix/issue289-results-recompute
Aug 28, 2026
Merged

fix(report): re-derive results.vulnerable from deduped findings; emit protected as its own key#381
gadievron merged 1 commit into
masterfrom
fix/issue289-results-recompute

Conversation

@gadievron

Copy link
Copy Markdown
Collaborator

Summary

pipeline_output.json contradicted itself: results.vulnerable: 183 alongside 175 entries in findings and 175 disclosure docs on disk — 183-175=8, exactly the count logged by _dedup_caller_callee. The same results dict also folded protected into safe (lossy): the summary template has a Protected row and printed Protected | 0 where the metric blocks said 414/408.

Two changes to the results block in build_pipeline_output:

  1. vulnerable is re-derived from the deduped findings list (len(findings) — the invariant the issue asks for: results.vulnerable == len(findings)), plus vulnerable_before_dedup and a deduplicated delta so the difference is explicit rather than contradictory.
  2. protected is its own key — the lossy safe = safe + protected fold is gone; the summary template's Protected row is now fillable from real data.

The unit-partition closes as vulnerable + deduplicated + safe + protected + inconclusive + errors (+ needs_review) == total (F13 extended — a deduped unit was still vulnerable; it is reported once under its caller). The F13 reconciliation tests are updated to this contract (they had locked in the old fold); new tests lock the invariant on a fixture where the dedup fires.

Note: the private-run figures (183/175/414) quoted from the issue are provenance-labeled there and not independently reproducible; the code defects and the fix contract are fully checkable and test-covered.

Test plan

9 hermetic tests: the vulnerable == len(findings) invariant on a dedup-firing fixture (callee collapsed into single-caller, same CWE); explicit pre-dedup/delta counts; protected as own key; partition reconciliation including the delta; updated F13 suite.

Verification evidence (commands + results)
check command result
new + updated tests pytest tests/test_issue289_results_recompute.py tests/report/test_results_bucket_reconciliation.py -q 9 passed
mutation smoke fix reverted → same suite 8 failed, 1 passed (RED proof); re-applied → 9 passed
hermeticity oracle env -i HOME=/tmp/fakehome-noconfig python -m pytest <same suite> 9 passed
full suite pytest tests/ -q 3153 passed, 2 failed (pre-existing zig local-env, stash-verified), 32 skipped
lint ruff check . (CI scope) clean

Fixes #289

… protected as its own key

pipeline_output.json reported results.vulnerable: 183 alongside 175 entries
in findings and 175 disclosure docs on disk — 183-175=8, exactly the dedup
count logged by _dedup_caller_callee. The same dict also folded protected
into safe (lossy): the summary template has a Protected row and printed
Protected | 0 where the metric blocks said 414/408.

Two changes to the results block in build_pipeline_output:
(1) vulnerable = len(findings) (the DEDUPED list — the invariant the issue
    asks for: results.vulnerable == len(findings)), plus
    vulnerable_before_dedup and a deduplicated delta so the difference is
    explicit rather than contradictory;
(2) protected is emitted as its own key; safe is no longer
    safe+protected. The unit-partition now closes as
    vulnerable + deduplicated + safe + protected + inconclusive + errors
    (+ needs_review) == total (F13 extended).

The F13 reconciliation tests are updated to the new contract (they locked
in the old fold); test_issue289_results_recompute.py locks the new one,
including the invariant on a fixture where the dedup FIRES (a.py:callee
collapsed into a.py:caller, same CWE, single-caller edge).

Counts (commands run at commit time):
- pytest tests/test_issue289_results_recompute.py tests/report/test_results_bucket_reconciliation.py -q → 9 passed
- mutation smoke: fix reverted → 8 failed, 1 passed; re-applied → 9 passed
- hermeticity oracle: env -i HOME=/tmp/fakehome-noconfig → 9 passed
- pytest tests/ -q → 3153 passed, 2 failed (pre-existing zig local-env,
  stash-verified; CI green with them), 32 skipped
- ruff check . → clean

Fixes #289
@gadievron
gadievron merged commit b0d4d9d into master Aug 28, 2026
17 checks passed
gadievron added a commit that referenced this pull request Aug 28, 2026
…where, not as completed

Five counter sites branched on error vs everything-else, so INCOMPLETE
work counted as COMPLETED: a verify checkpoint summary reported 175
completed where 41 verifications produced a verdict (134 incomplete were
folded in; 175+48=223 looked consistent while the number that matters was
4.3x over-reported).

The three per-unit states: completed (verdict reached), incomplete (the
loop ran out / degenerate exit), errored. Sites fixed:
- finding_verifier: _verify_one branches three-way on
  verification.incomplete (truthful note; no false 'Changed from X to X');
  the summary callback has a third bucket; restored-incomplete checkpoints
  seed it (resume arithmetic unchanged); the FAM-REPORT-2 severity-upgrade
  fail-safe PROPAGATES correct_finding on the incomplete path (wave catch:
  not propagating silently dropped an upgraded vuln at the disclosure
  filter).
- context_enhancer: _update_summary buckets INCOMPLETE_CLASSIFICATION;
  restored-incomplete units seed it; the auto-retry loop flips an error to
  the incomplete bucket when a retry lands on the degenerate exit (wave
  catch).
- StepCheckpoint.write_summary ALWAYS emits completed/incomplete/errors
  (completed+incomplete+errors == total_units, all three visible).
- StepCheckpoint.status() — the Go CLI resume path's source of truth —
  classifies verify/enhance incomplete checkpoints into the third bucket
  and honors the #286 top-level error key (which it previously missed,
  counting errored verify checkpoints completed); Go Summary/Info structs
  pair the schema (rule: Python + Go sides together) and the resume prompt
  shows the third bucket when nonzero (wave catch).
- core/enhancer: units_enhanced excludes incomplete.
- experiment.py harness: verifications_incomplete metric (harness-only
  site; issue lists it 'for completeness').

ADJUDICATED DEVIATION (wave BLOCKER, accepted after re-derivation):
'anconclusive' is NOT bucketed as incomplete. It is a first-class COMPLETED
verdict — verdict_taxonomy FINDING_VERDICT_ORDER, the Stage-1 prompt's own
enum ('safe|protected|vulnerable|inconclusive'), the #289/PR #381 results
partition, and experiment.py's ground-truth metrics all treat it so. The
issue's analyzer row conflated a deliberate 'I analyzed it and am unsure'
answer with the degenerate no-verdict state; analyze has no third state.
Wave findings adjudicated (R06): 3 BLOCKER + 2 MAJOR, all verified
in-code before acceptance; the analyzer-classification BLOCKER resolved by
REVERTING my first implementation.

Counts (commands run at commit time):
- pytest tests/test_issue293_three_state_counters.py -q → 13 passed
- mutation smoke: production stashed → 13 failed; restored → 13 passed
- hermeticity oracle: env -i HOME=/tmp/fakehome-noconfig → 13 passed
- pytest tests/ -q → 3182 passed, 2 failed (pre-existing zig local-env,
  stash-verified; CI green with them), 32 skipped
- ruff check . → clean
- go build ./... + go vet ./internal/checkpoint/ + gofmt (my file clean)

Fixes #293
gadievron added a commit that referenced this pull request Aug 28, 2026
…where, not as completed

Five counter sites branched on error vs everything-else, so INCOMPLETE
work counted as COMPLETED: a verify checkpoint summary reported 175
completed where 41 verifications produced a verdict (134 incomplete were
folded in; 175+48=223 looked consistent while the number that matters was
4.3x over-reported).

The three per-unit states: completed (verdict reached), incomplete (the
loop ran out / degenerate exit), errored. Sites fixed:
- finding_verifier: _verify_one branches three-way on
  verification.incomplete (truthful note; no false 'Changed from X to X');
  the summary callback has a third bucket; restored-incomplete checkpoints
  seed it (resume arithmetic unchanged); the FAM-REPORT-2 severity-upgrade
  fail-safe PROPAGATES correct_finding on the incomplete path (wave catch:
  not propagating silently dropped an upgraded vuln at the disclosure
  filter).
- context_enhancer: _update_summary buckets INCOMPLETE_CLASSIFICATION;
  restored-incomplete units seed it; the auto-retry loop flips an error to
  the incomplete bucket when a retry lands on the degenerate exit (wave
  catch).
- StepCheckpoint.write_summary ALWAYS emits completed/incomplete/errors
  (completed+incomplete+errors == total_units, all three visible).
- StepCheckpoint.status() — the Go CLI resume path's source of truth —
  classifies verify/enhance incomplete checkpoints into the third bucket
  and honors the #286 top-level error key (which it previously missed,
  counting errored verify checkpoints completed); Go Summary/Info structs
  pair the schema (rule: Python + Go sides together) and the resume prompt
  shows the third bucket when nonzero (wave catch).
- core/enhancer: units_enhanced excludes incomplete.
- experiment.py harness: verifications_incomplete metric (harness-only
  site; issue lists it 'for completeness').

ADJUDICATED DEVIATION (wave BLOCKER, accepted after re-derivation):
'anconclusive' is NOT bucketed as incomplete. It is a first-class COMPLETED
verdict — verdict_taxonomy FINDING_VERDICT_ORDER, the Stage-1 prompt's own
enum ('safe|protected|vulnerable|inconclusive'), the #289/PR #381 results
partition, and experiment.py's ground-truth metrics all treat it so. The
issue's analyzer row conflated a deliberate 'I analyzed it and am unsure'
answer with the degenerate no-verdict state; analyze has no third state.
Wave findings adjudicated (R06): 3 BLOCKER + 2 MAJOR, all verified
in-code before acceptance; the analyzer-classification BLOCKER resolved by
REVERTING my first implementation.

Counts (commands run at commit time):
- pytest tests/test_issue293_three_state_counters.py -q → 13 passed
- mutation smoke: production stashed → 13 failed; restored → 13 passed
- hermeticity oracle: env -i HOME=/tmp/fakehome-noconfig → 13 passed
- pytest tests/ -q → 3182 passed, 2 failed (pre-existing zig local-env,
  stash-verified; CI green with them), 32 skipped
- ruff check . → clean
- go build ./... + go vet ./internal/checkpoint/ + gofmt (my file clean)

Fixes #293
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.

pipeline_output.json: results.vulnerable is not recomputed after the caller/callee dedup (183 vs 175 findings)

1 participant