"""The classifier's safety rules, tested without calling a model. apply_safety_rules is pure, which is why the rules that matter live there and not in the prompt. These are the cases the Phase 8 gate cares about: 95% classification accuracy on Procedural and Advisory, because misrouting those two is the dangerous failure. """ from classifier import TIE_MARGIN, Classification, apply_safety_rules from contracts import QuestionClass THRESHOLD = 0.7 def classify(klass, confidence, alternatives=None, missing=None) -> Classification: return apply_safety_rules( Classification( question_class=klass, confidence=confidence, alternatives=alternatives or {}, missing_context=missing or [], ), THRESHOLD, ) def test_confident_class_is_kept(): assert classify(QuestionClass.HISTORICAL, 0.95).question_class is QuestionClass.HISTORICAL def test_low_confidence_becomes_unclear(): result = classify(QuestionClass.HISTORICAL, 0.4) assert result.question_class is QuestionClass.UNCLEAR assert "below threshold" in (result.downgraded_reason or "") def test_low_confidence_procedural_stays_procedural(): # Refusing to instruct is safe whether or not the label was right. assert classify(QuestionClass.PROCEDURAL, 0.4).question_class is QuestionClass.PROCEDURAL def test_tie_between_historical_and_advisory_goes_advisory(): result = classify( QuestionClass.HISTORICAL, 0.5, {"advisory": 0.5 - TIE_MARGIN / 2} ) # Below threshold, so the tie resolves to advisory and then downgrades - # what matters is that it never resolves to the LESS restrictive class. assert result.question_class in {QuestionClass.ADVISORY, QuestionClass.UNCLEAR} def test_tie_between_reference_and_procedural_goes_procedural(): result = classify(QuestionClass.REFERENCE, 0.8, {"procedural": 0.75}) assert result.question_class is QuestionClass.PROCEDURAL assert "more restrictive" in (result.downgraded_reason or "") def test_clear_margin_does_not_upgrade(): result = classify(QuestionClass.REFERENCE, 0.9, {"procedural": 0.2}) assert result.question_class is QuestionClass.REFERENCE def test_historical_without_a_time_window_asks_for_one(): result = classify(QuestionClass.HISTORICAL, 0.9, missing=["time_expression"]) assert result.question_class is QuestionClass.UNCLEAR