From 86e56915695b9facfbe83d265fdf404de8aab2db Mon Sep 17 00:00:00 2001 From: ayushi2577 Date: Mon, 25 May 2026 13:26:04 +0530 Subject: [PATCH 1/8] fix: normalize default weights to sum to 1.0 instead of 1.15 --- adiuvare/core/scorer.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/adiuvare/core/scorer.py b/adiuvare/core/scorer.py index 70304c5..cca287c 100644 --- a/adiuvare/core/scorer.py +++ b/adiuvare/core/scorer.py @@ -3,9 +3,9 @@ _weights = { "payload": 0.40, "behavior": 0.35, - "identity": 0.25, - "context": 0.10, - "ip_rep": 0.05, + "identity": 0.15, + "context": 0.06, + "ip_rep": 0.04, } @@ -19,6 +19,9 @@ def compute_score(sig_res: dict[str, SignalResult], snap=None) -> tuple[float, d weights["payload"] = snap.payload_weight weights["behavior"] = snap.behavior_weight weights["identity"] = snap.identity_weight + total_w = sum(weights.values()) + if total_w > 0: + weights = {k: v / total_w for k, v in weights.items()} for name, res in sig_res.items(): weight = weights.get(name, 0.0) From ad3ea2be63ed6e6688ac913a35998c6bc306fded Mon Sep 17 00:00:00 2001 From: ayushi2577 Date: Mon, 25 May 2026 13:42:15 +0530 Subject: [PATCH 2/8] fix: update test assertions to match normalized weights --- tests/test_scorer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_scorer.py b/tests/test_scorer.py index fe634c6..3170940 100644 --- a/tests/test_scorer.py +++ b/tests/test_scorer.py @@ -39,7 +39,7 @@ def test_score_can_use_snapshot_weights(): snap, ) - assert round(score, 3) == 0.45 + assert round(score, 3) == 0.41 assert round(breakdown["payload"], 3) == 0.35 From acc1e3677d48504edae15534e2393723548a2bc0 Mon Sep 17 00:00:00 2001 From: ayushi2577 Date: Mon, 25 May 2026 13:45:12 +0530 Subject: [PATCH 3/8] fix: update remaining test assertions for normalized weights --- tests/test_command_probe.py | 4 ++-- tests/test_scorer.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_command_probe.py b/tests/test_command_probe.py index 43f0ce4..697c66a 100644 --- a/tests/test_command_probe.py +++ b/tests/test_command_probe.py @@ -11,7 +11,7 @@ def test_command_probe_semicolon_detected(): assert gate.passed is True assert event is not None - assert event.score > 0.35 + assert event.score > 0.33 assert "payload" in event.breakdown assert event.breakdown["payload"] > 0.0 @@ -26,7 +26,7 @@ def test_command_probe_dollar_detected(): assert gate.passed is True assert event is not None - assert event.score > 0.35 + assert event.score > 0.33 assert "payload" in event.breakdown assert event.breakdown["payload"] > 0.0 diff --git a/tests/test_scorer.py b/tests/test_scorer.py index 3170940..4d07f80 100644 --- a/tests/test_scorer.py +++ b/tests/test_scorer.py @@ -40,7 +40,7 @@ def test_score_can_use_snapshot_weights(): ) assert round(score, 3) == 0.41 - assert round(breakdown["payload"], 3) == 0.35 + assert round(breakdown["payload"], 3) == 0.318 def test_verdict_gets_identity_nudge_inline(): From b530180a17fe98ff46d04c326983f03b3905a64f Mon Sep 17 00:00:00 2001 From: ayushi2577 Date: Tue, 26 May 2026 20:10:57 +0530 Subject: [PATCH 4/8] fix: add negative/zero weight validation and pytest.approx assertions --- adiuvare/core/scorer.py | 32 +++++++++++++++++++++++--------- tests/test_scorer.py | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/adiuvare/core/scorer.py b/adiuvare/core/scorer.py index cca287c..87f42a3 100644 --- a/adiuvare/core/scorer.py +++ b/adiuvare/core/scorer.py @@ -1,13 +1,16 @@ -from ..core.models import SignalResult +from ..core.models import SignalResult _weights = { "payload": 0.40, - "behavior": 0.35, + "behavior": 0.30, "identity": 0.15, - "context": 0.06, - "ip_rep": 0.04, + "context": 0.10, + "ip_rep": 0.05, } +_total_w = sum(_weights.values()) +_weights = {k: v / _total_w for k, v in _weights.items()} + def compute_score(sig_res: dict[str, SignalResult], snap=None) -> tuple[float, dict[str, float]]: breakdown: dict[str, float] = {} @@ -16,12 +19,23 @@ def compute_score(sig_res: dict[str, SignalResult], snap=None) -> tuple[float, d weights = dict(_weights) if snap: - weights["payload"] = snap.payload_weight - weights["behavior"] = snap.behavior_weight - weights["identity"] = snap.identity_weight + snap_weights = { + "payload": snap.payload_weight, + "behavior": snap.behavior_weight, + "identity": snap.identity_weight, + } + + for k, v in snap_weights.items(): + if v < 0: + raise ValueError(f"Weight for '{k}' must be non-negative, got {v}") + + total_snap = sum(snap_weights.values()) + if total_snap == 0: + raise ValueError("All weights sum to zero - cannot normalize.") + + weights.update(snap_weights) total_w = sum(weights.values()) - if total_w > 0: - weights = {k: v / total_w for k, v in weights.items()} + weights = {k: v / total_w for k, v in weights.items()} for name, res in sig_res.items(): weight = weights.get(name, 0.0) diff --git a/tests/test_scorer.py b/tests/test_scorer.py index 4d07f80..96e1792 100644 --- a/tests/test_scorer.py +++ b/tests/test_scorer.py @@ -1,3 +1,4 @@ +import pytest from adiuvare.core.models import ConfigSnapshot, SignalResult from adiuvare.core.scorer import compute_score from adiuvare.core.verdict import compute_verdict @@ -11,8 +12,8 @@ def test_score_uses_hardcoded_weights(): } ) - assert round(score, 3) == 0.395 - assert round(breakdown["payload"], 3) == 0.28 + assert score == pytest.approx(0.38, rel=1e-3) + assert breakdown["payload"] == pytest.approx(0.28, rel=1e-3) def test_verdict_maps_score_ranges(): @@ -39,8 +40,7 @@ def test_score_can_use_snapshot_weights(): snap, ) - assert round(score, 3) == 0.41 - assert round(breakdown["payload"], 3) == 0.318 + assert score == pytest.approx(0.393, rel=1e-3) def test_verdict_gets_identity_nudge_inline(): @@ -53,3 +53,29 @@ def test_verdict_gets_identity_nudge_inline(): block_threshold=0.80, ) assert compute_verdict(0.50, snap, identity_risk=0.70) == "throttle" + +def test_identity_heavy_detection(): + score, breakdown = compute_score( + { + "identity": SignalResult(score=1.0, reason="known_bad_actor"), + "ip_rep": SignalResult(score=0.8, reason="blacklisted_ip"), + } + ) + assert score > 0.15 + assert breakdown["identity"] > 0.10 + +def test_negative_weight_raises(): + snap = ConfigSnapshot( + payload_weight=-0.5, behavior_weight=0.5, identity_weight=0.0, + flag_threshold=0.25, throttle_threshold=0.55, block_threshold=0.80, + ) + with pytest.raises(ValueError, match="non-negative"): + compute_score({"payload": SignalResult(score=0.7, reason="sql_hit")}, snap) + +def test_all_zero_weights_raises(): + snap = ConfigSnapshot( + payload_weight=0.0, behavior_weight=0.0, identity_weight=0.0, + flag_threshold=0.25, throttle_threshold=0.55, block_threshold=0.80, + ) + with pytest.raises(ValueError, match="sum to zero"): + compute_score({"payload": SignalResult(score=0.7, reason="sql_hit")}, snap) \ No newline at end of file From f546a69b8220534789608925d5caac48604b5575 Mon Sep 17 00:00:00 2001 From: ayushi2577 Date: Tue, 26 May 2026 21:09:18 +0530 Subject: [PATCH 5/8] fix: update command probe score threshold after weight normalization --- tests/test_command_probe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_command_probe.py b/tests/test_command_probe.py index 697c66a..eeb4c10 100644 --- a/tests/test_command_probe.py +++ b/tests/test_command_probe.py @@ -11,7 +11,7 @@ def test_command_probe_semicolon_detected(): assert gate.passed is True assert event is not None - assert event.score > 0.33 + assert event.score > 0.30 assert "payload" in event.breakdown assert event.breakdown["payload"] > 0.0 @@ -26,7 +26,7 @@ def test_command_probe_dollar_detected(): assert gate.passed is True assert event is not None - assert event.score > 0.33 + assert event.score > 0.30 assert "payload" in event.breakdown assert event.breakdown["payload"] > 0.0 From 756aa2d2aa077bff2ae2d6818c7d0d6b2551207a Mon Sep 17 00:00:00 2001 From: ayushi2577 Date: Fri, 29 May 2026 14:48:21 +0530 Subject: [PATCH 6/8] fix: use <= 0 for zero-sum check and tighten identity test assertion --- adiuvare/core/scorer.py | 2 +- tests/test_scorer.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/adiuvare/core/scorer.py b/adiuvare/core/scorer.py index 87f42a3..b9ef2da 100644 --- a/adiuvare/core/scorer.py +++ b/adiuvare/core/scorer.py @@ -30,7 +30,7 @@ def compute_score(sig_res: dict[str, SignalResult], snap=None) -> tuple[float, d raise ValueError(f"Weight for '{k}' must be non-negative, got {v}") total_snap = sum(snap_weights.values()) - if total_snap == 0: + if total_snap <= 0: raise ValueError("All weights sum to zero - cannot normalize.") weights.update(snap_weights) diff --git a/tests/test_scorer.py b/tests/test_scorer.py index 96e1792..7b13f57 100644 --- a/tests/test_scorer.py +++ b/tests/test_scorer.py @@ -61,8 +61,8 @@ def test_identity_heavy_detection(): "ip_rep": SignalResult(score=0.8, reason="blacklisted_ip"), } ) - assert score > 0.15 - assert breakdown["identity"] > 0.10 + assert score == pytest.approx(0.20, rel=1e-3) + assert breakdown["identity"] == pytest.approx(0.15, rel=1e-3) def test_negative_weight_raises(): snap = ConfigSnapshot( From e1aade64f80f525c189b0fc373475e0b1307da40 Mon Sep 17 00:00:00 2001 From: ayushi2577 Date: Sat, 30 May 2026 11:30:25 +0530 Subject: [PATCH 7/8] fix: resave scorer.py as plain UTF-8 without BOM --- adiuvare/core/scorer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adiuvare/core/scorer.py b/adiuvare/core/scorer.py index b9ef2da..4250f50 100644 --- a/adiuvare/core/scorer.py +++ b/adiuvare/core/scorer.py @@ -1,4 +1,4 @@ -from ..core.models import SignalResult +from ..core.models import SignalResult _weights = { "payload": 0.40, From 5a997881b9c950649ca4dbc998334a987ffb0098 Mon Sep 17 00:00:00 2001 From: ayushi2577 Date: Sat, 30 May 2026 16:09:37 +0530 Subject: [PATCH 8/8] fix: improve error message, add assert for default weights, add clarifying comment --- adiuvare/core/scorer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/adiuvare/core/scorer.py b/adiuvare/core/scorer.py index 4250f50..3b80040 100644 --- a/adiuvare/core/scorer.py +++ b/adiuvare/core/scorer.py @@ -9,6 +9,7 @@ } _total_w = sum(_weights.values()) +assert _total_w > 0, "default _weights must sum to a positive value" _weights = {k: v / _total_w for k, v in _weights.items()} @@ -25,13 +26,15 @@ def compute_score(sig_res: dict[str, SignalResult], snap=None) -> tuple[float, d "identity": snap.identity_weight, } + # only snap-overridden keys are validated; context and ip_rep carry + # over from _weights which are already normalized and guaranteed positive for k, v in snap_weights.items(): if v < 0: raise ValueError(f"Weight for '{k}' must be non-negative, got {v}") total_snap = sum(snap_weights.values()) if total_snap <= 0: - raise ValueError("All weights sum to zero - cannot normalize.") + raise ValueError("Snap weights sum to zero or below - cannot normalize.") weights.update(snap_weights) total_w = sum(weights.values())