yau-plant-assistant/api/tests/test_classifier_rules.py
Claude 34d2ccc576 Scaffold the WRPS plant operations assistant repository
Build spec and host brief carried in from C:\Claude and WRPS/02-env; the
plant model (equipment, tags, alarm bitmask, enums, unit conversions) is
derived from WRPS/04-plc/register-map.csv, WRPS/05-scada/modbus/scada-points.csv
and WRPS-CTL-003.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 13:56:32 +10:00

64 lines
2.4 KiB
Python

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