diff --git a/prod/clean_arch/violet/test_violet_v4_readiness.py b/prod/clean_arch/violet/test_violet_v4_readiness.py new file mode 100644 index 00000000..8d253afb --- /dev/null +++ b/prod/clean_arch/violet/test_violet_v4_readiness.py @@ -0,0 +1,140 @@ +"""V4 arm-readiness reporter — self-test to death. + +Spec: prod/docs/VIOLET_PASS_MM1_V4_READINESS_REPORTER.md +Style: follows test_violet_domain.py patterns. +""" + +from __future__ import annotations + +import sys +sys.path.insert(0, "/mnt/dolphinng5_predict") + +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +from prod.clean_arch.violet.v4_readiness import ( + V4ReadinessReport, + assess_v4_readiness, +) + +ALL_FIELDS = [ + "creds_ok", "canary_passed", "arming_gate_present", + "conviction_bit_identical", "selection_parity_ok", "feed_live", +] + + +def _all_true(**overrides: bool) -> dict: + return {f: overrides.get(f, True) for f in ALL_FIELDS} + + +def _single_false(field: str) -> dict: + return _all_true(**{field: False}) + + +# ── happy path ──────────────────────────────────────────────────────────────── + +def test_all_true_ready_to_arm(): + r = assess_v4_readiness(**_all_true()) + assert r.ready_to_arm is True + assert r.reasons == [] + + +# ── each single False yields ready_to_arm=False + correct reason ───────────── + +@pytest.mark.parametrize("field", ALL_FIELDS) +def test_single_false_not_ready(field: str): + r = assess_v4_readiness(**_single_false(field)) + assert r.ready_to_arm is False + assert len(r.reasons) == 1 + assert field in r.reasons[0] + + +# ── hypothesis: invariant holds across all 2^6 combos ──────────────────────── + +@given( + creds_ok=st.booleans(), + canary_passed=st.booleans(), + arming_gate_present=st.booleans(), + conviction_bit_identical=st.booleans(), + selection_parity_ok=st.booleans(), + feed_live=st.booleans(), +) +@settings(max_examples=100) +def test_ready_to_arm_equals_all_inputs( + creds_ok: bool, + canary_passed: bool, + arming_gate_present: bool, + conviction_bit_identical: bool, + selection_parity_ok: bool, + feed_live: bool, +): + r = assess_v4_readiness( + creds_ok=creds_ok, + canary_passed=canary_passed, + arming_gate_present=arming_gate_present, + conviction_bit_identical=conviction_bit_identical, + selection_parity_ok=selection_parity_ok, + feed_live=feed_live, + ) + expected = all(( + creds_ok, canary_passed, arming_gate_present, + conviction_bit_identical, selection_parity_ok, feed_live, + )) + assert r.ready_to_arm is expected + + +# ── mutation litmus: flipping all() to any() MUST break this test ──────────── + +def test_mutation_litmus_all_must_be_true(): + """If someone changes all() to any(), at least one all-False should become + ready_to_arm=True — this test catches it.""" + r = assess_v4_readiness(**{f: False for f in ALL_FIELDS}) + assert r.ready_to_arm is False + assert len(r.reasons) == 6 + + +# ── poison: StrictModel rejects non-bool / extra fields ───────────────────── + +def test_rejects_non_bool(): + with pytest.raises(Exception): + V4ReadinessReport( + creds_ok={"ok": True}, # dict is not coercible to bool + canary_passed=True, + arming_gate_present=True, + conviction_bit_identical=True, + selection_parity_ok=True, + feed_live=True, + reasons=[], + ) + + +def test_rejects_extra_fields(): + with pytest.raises(Exception): + V4ReadinessReport( + creds_ok=True, + canary_passed=True, + arming_gate_present=True, + conviction_bit_identical=True, + selection_parity_ok=True, + feed_live=True, + reasons=[], + unknown_field="oops", + ) + + +def test_rejects_frozen_mutation(): + r = assess_v4_readiness(**_all_true()) + with pytest.raises(Exception): + r.creds_ok = False # type: ignore[misc] + + +# ── reasons content check: False fields produce descriptive strings ────────── + +def test_reasons_mention_gate_name(): + r = assess_v4_readiness(**_single_false("feed_live")) + assert any("PASS2.6" in reason for reason in r.reasons) + + +if __name__ == "__main__": + raise SystemExit(pytest.main([__file__, "-v"])) diff --git a/prod/clean_arch/violet/v4_readiness.py b/prod/clean_arch/violet/v4_readiness.py new file mode 100644 index 00000000..4915f876 --- /dev/null +++ b/prod/clean_arch/violet/v4_readiness.py @@ -0,0 +1,75 @@ +"""V4 arm-readiness reporter — DARK, read-only, non-colliding. + +Aggregates V4 gate outcomes into a typed report answering: "is V4 ready +to ARM the live canary?" Does NOT run gates — CONSUMES their results +(passed in) and applies fail-closed readiness logic. + +Spec: prod/docs/VIOLET_PASS_MM1_V4_READINESS_REPORTER.md +""" + +from __future__ import annotations + +from .domain import StrictModel, typed + +__all__ = ["V4ReadinessReport", "assess_v4_readiness"] + + +class V4ReadinessReport(StrictModel): + """Typed V4 readiness report — all six gates must pass for ready_to_arm.""" + + creds_ok: bool + canary_passed: bool + arming_gate_present: bool + conviction_bit_identical: bool + selection_parity_ok: bool + feed_live: bool + reasons: list[str] + + @property + def ready_to_arm(self) -> bool: + return all(( + self.creds_ok, + self.canary_passed, + self.arming_gate_present, + self.conviction_bit_identical, + self.selection_parity_ok, + self.feed_live, + )) + + +@typed +def assess_v4_readiness( + *, + creds_ok: bool, + canary_passed: bool, + arming_gate_present: bool, + conviction_bit_identical: bool, + selection_parity_ok: bool, + feed_live: bool, +) -> V4ReadinessReport: + """Build a V4ReadinessReport from gate outcomes. Fail-closed: any False + means ready_to_arm is False. No partial-arm.""" + + reasons: list[str] = [] + if not creds_ok: + reasons.append("creds_ok: PASS2.1 not satisfied") + if not canary_passed: + reasons.append("canary_passed: PASS2.3 canary not round-tripped flat") + if not arming_gate_present: + reasons.append("arming_gate_present: PASS2.2 not satisfied") + if not conviction_bit_identical: + reasons.append("conviction_bit_identical: PASS2.5 fidelity mismatch") + if not selection_parity_ok: + reasons.append("selection_parity_ok: PASS2.7/3.1 parity mismatch") + if not feed_live: + reasons.append("feed_live: PASS2.6 HZ feed stale or deaf") + + return V4ReadinessReport( + creds_ok=creds_ok, + canary_passed=canary_passed, + arming_gate_present=arming_gate_present, + conviction_bit_identical=conviction_bit_identical, + selection_parity_ok=selection_parity_ok, + feed_live=feed_live, + reasons=reasons, + )