Skip to content

feat(attacks): surface + de-prioritize un-clusterable contract_anomaly rows - #78

Merged
elluff merged 1 commit into
mainfrom
feat/attacks-feed-unclusterable-deprioritize
Jul 23, 2026
Merged

feat(attacks): surface + de-prioritize un-clusterable contract_anomaly rows#78
elluff merged 1 commit into
mainfrom
feat/attacks-feed-unclusterable-deprioritize

Conversation

@elluff

@elluff elluff commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Why

Completes the un-clusterable-contract work (#75 / #76 / #77). Those made the sidecar stamp unclusterable_fit on tx_contract_anomaly rows; until now the host never read it. This is the consumer.

An anomaly verdict 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 bare anomaly at 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_fit added to the tx_contract_anomaly projection; flows through to every reader.
  • contract_anomaly.resolve: carries the flag forward (coerced to bool).
  • ClassScoreResult.contract_anomaly_unclusterable: new field (parallel to contract_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

  • Threaded through ApiAnalysisResult → toRiskAlert → RiskAlert.
  • AttacksPage: an "unclusterable model" badge on those rows.

Recall safety

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. 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

  • Backend: 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. Existing project_score band tests unchanged and green (568 in the analysis/api/notifications suites).
  • Frontend: mapping pass-through + default-false (vitest 49 passing; build + lint clean).

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

…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>
@elluff
elluff merged commit 1c64ed7 into main Jul 23, 2026
9 checks passed
@elluff
elluff deleted the feat/attacks-feed-unclusterable-deprioritize branch July 23, 2026 12:47
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