Skip to content

feat(audit): account for rows dropped by the search window scan - #42

Merged
plusky merged 1 commit into
mainfrom
search-scan-accounting-29
Aug 2, 2026
Merged

feat(audit): account for rows dropped by the search window scan#42
plusky merged 1 commit into
mainfrom
search-scan-accounting-29

Conversation

@plusky

@plusky plusky commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Closes #29.

What

bugs_quicksearch audit records now account for what the window scan withheld. Guard::quicksearch_window returns a SearchWindow { bugs, scanned, dropped } — the served window plus the scan's accounting — and Guard::filter_bug_list reports the verdict-dropped ids instead of a bare count. The server records the numbers as guard.scan = {scanned, dropped} on the tool-call record and feeds the dropped ids through the existing suppressed-ids machinery, so they obey the existing suppressed_ids config switch with no second knob, while the counts are recorded regardless of it.

Pinned semantics (documented on SearchWindow and in DESIGN.md):

  • scanned counts every upstream row examined — duplicate and id-less rows included, because each was fetched and looked at.
  • dropped is exactly the ids the verdict withheld, in scan order: overshoot-region denials (past the requested window but inside the quantised chunk) included; id-less rows and deduped repeats excluded (they never reach a verdict).
  • scan is present on every served bugs_quicksearch call — zero-limit records {scanned: 0, dropped: 0}, a failed search records no scan — so on a served search, dropped: 0 is a statement, not an omission.
  • Deliberate verdict change: a search whose scan dropped rows now records served_filtered (previously served) — a search that withheld rows is a filtered serve. A clean scan leaves the verdict untouched.
  • Client-invisible by construction (I3): the response is built from the window's bugs alone; a byte-identity test (and a dedicated mutation, below) enforces it.
  • Records written before the field existed still deserialize (scan optional-and-absent-when-unset, schema stays v1; the pre-Audit records do not account for rows dropped by the search window scan #29 golden bytes are pinned as GOLDEN_TOOL_CALL_PRE_SCAN with an old-line deserialization test, and the schema-goldens discipline comment now states this additive exception explicitly).

Acceptance criteria from the issue, each pinned by a test:

  • a search whose window drops denied rows records a non-zero drop count → quicksearch_scan_accounting_reaches_the_record_never_the_envelope;
  • the client-visible response is byte-identical → quicksearch_response_is_byte_identical_whether_or_not_rows_were_dropped (mixed-vs-clean upstream, auditing on);
  • with suppressed-id recording off, the count is still recorded but the ids are not → quicksearch_drop_count_survives_the_suppressed_ids_knob.

examples/audit.toml documents that the scan-dropped ids follow the suppressed_ids switch (counts always recorded); DESIGN.md gains the audit-stream bullet and extended testing bars.

Adversarial review record

Implemented via fan-out (implementer → security-bypass / correctness-fail-closed / docs-vs-code lenses + mutation verifier → fixer), findings addressed before this PR was opened. 4 findings, all minor, all fixed:

  1. Schema-goldens comment contradicted the in-place golden update (security + docs lenses, independently). The stability-gate comment forbade updating GOLDEN_TOOL_CALL without a version bump, while the change did exactly that (legitimately — additive optional field, blessed by DESIGN.md). Fixed: the comment now states the practiced rule — an optional, absent-when-unset field stays within v1, updates the golden, and must pin the prior bytes as a GOLDEN_*_PRE_* constant with an old-line deserialization test; any other byte change still requires a version bump.
  2. DESIGN.md overclaimed "present exactly when a window scan ran" (docs lens) — false in both directions: a failed search discards its partial accounting with the error, and a zero-limit call records {0, 0} without touching upstream. Fixed: DESIGN.md now states the exact presence rule.
  3. No test covered accounting accumulation across scan chunks (correctness lens) — all initial fixtures fit in one 200-row chunk, so a per-chunk reset of either counter would have survived the suite. Fixed: quicksearch_window_accounting_accumulates_across_chunks (250-row corpus, one hidden id in each chunk) pins the per-chunk extend/+=; both reset-mutants re-proved killed in a throwaway worktree.

Mutation verification

16 mutants, 16 killed, 0 survivors: ids never collected; scanned counting kept instead of examined rows; scanned counting post-dedupe rows; note_scan never called; note_suppressed(dropped) removed; verdict upgraded on a clean scan; verdict not upgraded on a dropping scan; suppressed-ids config gate inverted; scan never serialized; accounting leaked into the envelope under a fresh key — killed solely by the byte-identity test, proving it load-bearing for I3; drop count zeroed at the call site; first-write-wins in note_scan; cell state never drained into the record; dropped ids replaced with zeros (killed by the exact-id-list assertions); plus the two per-chunk reset mutants from finding 3.

Gate

cargo fmt --all --check · cargo clippy --workspace --all-targets -- -D warnings · cargo test --workspace --all-targets --locked (342 passed, 0 failed) · cargo deny check (advisories/bans/licenses/sources ok) · typos — all green, re-run independently after the review fixes.

The quicksearch window scan counted the upstream rows it examined and
the rows it withheld by verdict, then threw both away after a debug log,
so a search that silently dropped many denied bugs was indistinguishable
in the audit stream from one that dropped none (issue #29).

quicksearch_window now returns a SearchWindow carrying the served bugs
plus the scan accounting, filter_bug_list reports the dropped ids rather
than a bare count, and bugs_quicksearch records the numbers as
guard.scan on the audit record — the dropped ids ride the existing
suppressed_ids machinery and its config switch, while the counts are
always recorded. A dropping scan upgrades the verdict to
served_filtered; the client response stays byte-identical (I3).

The schema-goldens header now states the additive-optional exception
this change practices (golden updated in place under v1, prior bytes
pinned as a PRE constant with an old-line deserialization test), a
multi-chunk fixture pins that the accounting accumulates across scan
chunks, and the DESIGN.md bullet states the exact presence rule (every
served call records scan, zero-limit as {0, 0}; a failed search records
none).
@plusky
plusky merged commit 0cf59fc into main Aug 2, 2026
10 checks passed
@plusky
plusky deleted the search-scan-accounting-29 branch August 2, 2026 18:42
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.

Audit records do not account for rows dropped by the search window scan

1 participant