feat(attacks): surface + de-prioritize un-clusterable contract_anomaly rows - #78
Merged
Merged
Conversation
…y rows Consumes the sidecar's unclusterable_fit marker (migration 012, PRs #75/#76) on the host attacks feed. An "anomaly" verdict from a structurally un-clusterable fit is DBSCAN-noise from a model that could not cluster its own data, not a distinguishing signal, so the feed should visibly discount it without silencing anything. Backend: - clustering_queries: add unclusterable_fit to the tx_contract_anomaly projection (_FIELDS); it flows through _row_to_dict/_group to every reader. - contract_anomaly.resolve: carry the flag forward, coerced to bool. - ClassScoreResult.contract_anomaly_unclusterable: new field, parallel to contract_anomaly_corroborates; set by _merge_contract_anomaly. - _sort_results: append a final-tie-break de-prioritization so an un-clusterable contract_anomaly row loses only to an equally-scored / equally-dated peer, never on score or recency. Frontend: - Thread the flag through ApiAnalysisResult -> toRiskAlert -> RiskAlert. - AttacksPage: an "unclusterable model" badge on those rows. Recall-safe by construction: score, risk_band, and alert routing are UNCHANGED. A human-labeled `malicious` verdict still floors Critical and wins max_class even when its fit is flagged; the marker is evidence only. Nothing is filtered, hidden, or re-banded. Mirrors the Phase-2 decision to add an evidence marker rather than withdraw the DBSCAN-noise vote. Tests: backend merge/recall-guard/sort tie-break cases (incl. test_malicious_still_fires_when_unclusterable and test_flag_never_beats_a_higher_score); frontend mapping pass-through/default. Note: group-by-contract collapse is deferred to a follow-on (net-new list UI; the row type carries no contract id and the list has no grouping infra yet). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Completes the un-clusterable-contract work (#75 / #76 / #77). Those made the sidecar stamp
unclusterable_fitontx_contract_anomalyrows; until now the host never read it. This is the consumer.An
anomalyverdict from a structurally un-clusterable fit (Strike, Djed@N=2000) is DBSCAN-noise from a model that couldn't cluster its own data, so it carries no distinguishing signal. #72 already caps every bareanomalyat Moderate (no paging); this additionally lets the feed tell structural noise apart from a genuine outlier within the Moderate tier and visibly discount it.Change
Backend
clustering_queries:unclusterable_fitadded to thetx_contract_anomalyprojection; flows through to every reader.contract_anomaly.resolve: carries the flag forward (coerced to bool).ClassScoreResult.contract_anomaly_unclusterable: new field (parallel tocontract_anomaly_corroborates), set in_merge_contract_anomaly._sort_results: a final tie-break demotion, so an un-clusterable contract_anomaly row loses only to an equally-scored / equally-dated peer.Frontend
ApiAnalysisResult → toRiskAlert → RiskAlert.AttacksPage: an "unclusterable model" badge on those rows.Recall safety
Score,
risk_band, and alert routing are unchanged. A human-labeledmaliciousverdict still floors Critical and winsmax_classeven when its fit is flagged: the marker is evidence only. Nothing is filtered, hidden, or re-banded. The sort change is a final tie-break, so it never reorders rows that differ on score or recency. Mirrors the #76 decision to add an evidence marker rather than withdraw the DBSCAN-noise vote.Tests
test_unclusterable_flag_surfaced_when_marked,test_unclusterable_defaults_false_when_absent,test_malicious_still_fires_when_unclusterable(recall guard),test_unclusterable_loses_tie_to_clusterable_peer,test_flag_never_beats_a_higher_score. Existingproject_scoreband tests unchanged and green (568 in the analysis/api/notifications suites).Deferred
Group-by-contract collapse in the attacks list is left as a follow-on: it's net-new list UI (the row type carries no contract id and the flat table has no grouping infra). The badge + backend de-prioritization deliver the de-clutter without it.
🤖 Generated with Claude Code