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>
64 lines
2.4 KiB
Python
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
|