add rule generalization, sized LLM defaults, ops docs, and UI polish
Feedback rules can now be suggested for merging when a human has already reviewed the same pattern multiple times (never guesses about new values). LLM defaults sized for constrained VRAM instead of llama3.3/llama3.1. Adds systemd units, a state backup script, and reverse-proxy auth guidance for production deployment. Findings view is collapsible and sorts suggested-review rows to the top for a quicker overview.
This commit is contained in:
@@ -82,6 +82,16 @@ class ConfigTests(unittest.TestCase):
|
||||
public = store.update({"field_confidence_threshold": "not-a-number"})
|
||||
self.assertEqual(public["field_confidence_threshold"], 70)
|
||||
|
||||
def test_feedback_generalization_min_values_is_numeric_and_clamped(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
store = ConfigStore(str(Path(directory) / "config.json"))
|
||||
public = store.update({"feedback_generalization_min_values": "1"})
|
||||
self.assertEqual(public["feedback_generalization_min_values"], 2)
|
||||
public = store.update({"feedback_generalization_min_values": "500"})
|
||||
self.assertEqual(public["feedback_generalization_min_values"], 20)
|
||||
public = store.update({"feedback_generalization_min_values": "5"})
|
||||
self.assertEqual(public["feedback_generalization_min_values"], 5)
|
||||
|
||||
def test_empty_stream_update_does_not_clear_existing_streams(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
store = ConfigStore(str(Path(directory) / "config.json"))
|
||||
|
||||
@@ -94,6 +94,35 @@ class DashboardTests(unittest.TestCase):
|
||||
self.assertIn("openDetailIds", HTML)
|
||||
self.assertIn("addEventListener('toggle'", HTML)
|
||||
|
||||
def test_dashboard_field_deviations_are_collapsible_and_open_by_default(self):
|
||||
start = HTML.index("Field Baseline Deviations")
|
||||
preceding = HTML[max(0, start - 80):start]
|
||||
self.assertIn('<details class="collapsible-section" open>', preceding)
|
||||
self.assertIn('id="findingSummaryBadge"', HTML)
|
||||
|
||||
def test_dashboard_sorts_likely_worth_confirming_rows_first(self):
|
||||
self.assertIn('const suggestPriority', HTML)
|
||||
self.assertIn('suggestPriority(left) - suggestPriority(right)', HTML)
|
||||
|
||||
def test_dashboard_has_generalization_suggestion_panel(self):
|
||||
self.assertIn('id="feedbackGeneralizationSuggestions"', HTML)
|
||||
|
||||
def test_dashboard_generalization_merge_button_confirms_before_widening_scope(self):
|
||||
self.assertIn('merge-feedback-generalization', HTML)
|
||||
self.assertIn('Merge into one field-wide rule', HTML)
|
||||
self.assertIn('will be revoked', HTML)
|
||||
|
||||
def test_dashboard_generalization_merge_only_ever_posts_field_scope(self):
|
||||
start = HTML.index("querySelectorAll('.merge-feedback-generalization')")
|
||||
end = HTML.index('}));', start)
|
||||
merge_handler = HTML[start:end]
|
||||
self.assertIn("scope: 'field'", merge_handler)
|
||||
self.assertNotIn("scope: 'entity'", merge_handler)
|
||||
self.assertNotIn("scope: 'stream'", merge_handler)
|
||||
|
||||
def test_dashboard_exposes_feedback_generalization_setting(self):
|
||||
self.assertIn('name="feedback_generalization_min_values"', HTML)
|
||||
|
||||
def test_dashboard_suggests_review_from_detector_history(self):
|
||||
self.assertIn("label:'Suggested', render:suggestReview", HTML)
|
||||
self.assertIn("likely false positive", HTML)
|
||||
|
||||
@@ -3,7 +3,7 @@ import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from fgai.feedback import FeedbackStore, feedback_matches, select_match
|
||||
from fgai.feedback import FeedbackStore, feedback_matches, select_match, suggest_generalizations
|
||||
|
||||
|
||||
class FeedbackTests(unittest.TestCase):
|
||||
@@ -120,6 +120,43 @@ class FeedbackTests(unittest.TestCase):
|
||||
self.assertEqual(select_match(candidates), value_rule)
|
||||
self.assertEqual(select_match([stream_rule, entity_rule]), entity_rule)
|
||||
|
||||
def test_suggest_generalizations_groups_narrow_value_rules_by_stream_entity_field_status(self):
|
||||
entries = [
|
||||
{"scope": "value", "status": "false_positive", "entity": "alice", "stream_id": "windows", "field": "username", "value": "bob", "id": "1"},
|
||||
{"scope": "value", "status": "false_positive", "entity": "alice", "stream_id": "windows", "field": "username", "value": "carol", "id": "2"},
|
||||
{"scope": "value", "status": "false_positive", "entity": "alice", "stream_id": "windows", "field": "username", "value": "dave", "id": "3"},
|
||||
{"scope": "value", "status": "confirmed", "entity": "alice", "stream_id": "windows", "field": "username", "value": "eve", "id": "4"},
|
||||
]
|
||||
suggestions = suggest_generalizations(entries, min_values=3)
|
||||
self.assertEqual(len(suggestions), 1)
|
||||
self.assertEqual(suggestions[0]["value_count"], 3)
|
||||
self.assertEqual(suggestions[0]["status"], "false_positive")
|
||||
self.assertEqual(set(suggestions[0]["entry_ids"]), {"1", "2", "3"})
|
||||
|
||||
def test_suggest_generalizations_respects_min_values_threshold(self):
|
||||
entries = [
|
||||
{"scope": "value", "status": "expected", "entity": "a", "stream_id": "s", "field": "f", "value": "x", "id": "1"},
|
||||
{"scope": "value", "status": "expected", "entity": "a", "stream_id": "s", "field": "f", "value": "y", "id": "2"},
|
||||
]
|
||||
self.assertEqual(suggest_generalizations(entries, min_values=3), [])
|
||||
|
||||
def test_suggest_generalizations_ignores_broader_scope_entries(self):
|
||||
entries = [
|
||||
{"scope": "field", "status": "false_positive", "entity": "a", "stream_id": "s", "field": "f", "id": "1"},
|
||||
{"scope": "entity", "status": "expected", "entity": "a", "id": "2"},
|
||||
]
|
||||
self.assertEqual(suggest_generalizations(entries, min_values=2), [])
|
||||
|
||||
def test_suggest_generalizations_keeps_different_entities_separate(self):
|
||||
entries = [
|
||||
{"scope": "value", "status": "false_positive", "entity": "alice", "stream_id": "s", "field": "f", "value": "1", "id": "1"},
|
||||
{"scope": "value", "status": "false_positive", "entity": "alice", "stream_id": "s", "field": "f", "value": "2", "id": "2"},
|
||||
{"scope": "value", "status": "false_positive", "entity": "bob", "stream_id": "s", "field": "f", "value": "3", "id": "3"},
|
||||
]
|
||||
self.assertEqual(suggest_generalizations(entries, min_values=2), [
|
||||
{"stream_id": "s", "entity": "alice", "field": "f", "status": "false_positive", "value_count": 2, "sample_values": ["1", "2"], "entry_ids": ["1", "2"]}
|
||||
])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user