Repository navigation
Defense-stance votes that fail no guard leave no defense_degraded record #178
Description
Summary
#177 made the defense wave account for its losses: a stance entry failing an integrity guard is dropped and recorded in red_team.defense_degraded instead of aborting the run. Two entry shapes still evaporate without a record, because they never reach an integrity guard. Both are observability-only — no verdict, metric, or rendered finding is wrong today — and both were adjudicated as accepted pre-existing behavior in #177. This issue exists so that adjudication and the constraints on fixing it survive the PR.
Line references are against team-review.workflow.mjs as merged by #177.
Case 1 — withdrawn entry naming an unknown finding_id
A defender's withdrawal vote is tallied into withdrawVotes without resolving the id, and the tally is consulted only against c.kept (the c.kept.filter fold, ~line 2089). A withdrawal naming an id that resolves to no record casts a vote nothing ever reads, and nothing records that the defender produced it.
Constraint on the fix: withdrawal votes are consulted against kept only, so the naive fix — record every withdrawal whose id is absent from kept — misrecords a legitimate withdrawal of a finding currently sitting in contested (moved there earlier in the same fold, or contested since consensus) as an integrity drop. Resolution has to check kept and contested (and arguably the red-team new_findings ids) before classifying the entry as malformed.
Case 2 — single-vote unknown id in adopted_new / re_adopted
Adoption and re-adoption votes are tallied by castVote (~line 2062) without resolution; ids are resolved only when a promotion reaches the two-vote floor, where promote drops and records an unresolvable id via dropDefense (~line 2120). A single defender citing an unknown id therefore parks a vote that never reaches promote, and no defense_degraded entry is written — the same malformed-payload class #177 started recording, absorbed silently one vote below the floor.
Constraint on the fix: validating at castVote time needs resolveOriginal's inputs — in particular the red-team newFindingsByPath map — earlier than they are currently in scope (~line 2111, after the per-defender loop). That is a small restructuring, not a one-liner; an adversary-introduced id is legitimate in adopted_new and must not be classified malformed just because it lives in neither kept nor contested.
Suggested direction
Route both cases into the existing channel: resolve each vote's id at ingestion against the full identity space (kept, contested, withdrawn originals, red-team new findings), and on failure drop the entry via dropDefense with a guard name distinguishing it from the #177 cases. No new result fields; defense_degraded's shape already fits. The report render (references/report-format.md CAUTION block) needs no change.
Severity
Low. Diagnostics only: today a defender hallucinating ids one vote below the promotion floor is indistinguishable from a healthy defense wave. Nothing downstream is corrupted.
Related: #173 (the crash that established the drop-and-record path), #177 (where both gaps are documented under Limits).
Activity