Originally created by: fu351
Closes a fail-closed defect: the mandated lethal-trifecta floor could be masked in strict/paranoid.
decide() runs the objective guardrail first and short-circuits on any non-PASS. The lethal-trifecta floor lived only in the subjective layer (subjective.py::_score_result), so when an objective rule (e.g. ExternalDestinationRule) returned AUTH, the subjective guardrail — and its floor — never ran, and the mandated hard BLOCK was downgraded to AUTH. The floor is supposed to be absolute ("blocked regardless of score"), so this was a real gap.
Fix (minimal, raise-only, defense-in-depth):
src/doberman/engine/trifecta.py — a models-only leaf module holding trifecta_fires + the TRIFECTA_* frozensets (moved verbatim from subjective.py). This is the key design point: the objective-rules package is loaded by the host hooks on every tool call, so it must not pull subjective.py's ML import chain (numpy/scipy/river). subjective.py re-exports the names so existing from doberman.engine.subjective import trifecta_fires callers keep working; _score_result's floor is untouched.src/doberman/engine/rules/trifecta_floor.py::TrifectaFloorRule — emits BLOCK only when thresholds_for(ctx.mode).trifecta_hard_block and trifecta_fires(action.algebra) (strict/paranoid); abstains (PASS) in soft modes, so Light/Balanced stay byte-identical. Registered in BUILTIN_RULE_TYPES.Reviewed by an independent Opus reviewer (adversarial, seven vectors) directed by the session model; verdict APPROVE-WITH-NITS, no blocker/major. Confirmed: no mode+algebra+rule-set lets a full lethal trifecta in strict/paranoid escape the hard BLOCK (combine max-verdict turns AUTH+BLOCK → BLOCK); benign/single-leg/default-unknown algebra never trips; soft modes unchanged; perf invariant preserved; redaction clean.
tests/unit/test_rule_trifecta_floor.py (37 cases): strict/paranoid → BLOCK+lethal_trifecta; light/balanced → PASS (subjective still AUTHs); benign/single-leg/all-unknown → PASS everywhere; end-to-end red→green — objective-AUTH scenario yields AUTH without the rule, BLOCK with it; a registration/mutation guard fails if the rule is dropped from BUILTIN_RULE_TYPES; back-compat of the subjective re-export.
lethal_trifecta) + human explanationThe "objective-rules import stays ML-free" property is a convention, not a CI-enforced contract — filing a separate issue to add an import-linter guard.
Ticket changed by: fu351