Stage 2.6A-FIX2 — Fail-Closed Rule Engine
02 — Fail-Closed Rule Engine
Governed registry
qt001_tier_rule_registry_v2 (11 rows) — columns: rule_code (unique index), tier_code, rule_role in (GRANT,BLOCK), signal_key, predicate_type (CHECK to the 10 supported operators), predicate_params jsonb, expected_result in (true,false), rule_version, approval_status in (APPROVED,DRAFT,REVOKED), approved_by, authority_lock, provenance, rule_checksum (md5 of semantics), active, superseded. Only active + not-superseded + APPROVED rules participate.
Interpreter (fn_qt001_eval_predicate_v2)
Returns TRUE / FALSE / FAIL_MISSING_SIGNAL / FAIL_UNSUPPORTED_OP. Rules: a missing signal (no row) returns FAIL_MISSING_SIGNAL; a present-but-NULL typed value returns FAIL_MISSING_SIGNAL (this closes not_in_set on NULL, which previously returned TRUE); an unknown operator returns FAIL_UNSUPPORTED_OP. There is no path by which a missing signal yields a pass.
Rule core (fn_qt001_eval_rule_core, IMMUTABLE, testable)
NOT_PARTICIPATING if inactive/superseded/non-APPROVED. Else evaluates the predicate; FAIL_MISSING_SIGNAL/FAIL_UNSUPPORTED_OP propagate. Otherwise match := (predicate=TRUE) = (expected_result=true) — expected_result IS evaluated. GRANT -> PASS/FAIL_PREDICATE; BLOCK -> BLOCK_FIRED/BLOCK_CLEAR; unknown role -> FAIL_UNKNOWN_ROLE (fail-closed).
Machine tier (fn_qt001_machine_tier_v2; public fn_qt001_machine_tier delegates to it)
A tier is granted only when: it has at least one participating GRANT rule, ALL its participating GRANT rules PASS, and NO participating BLOCK rule is BLOCK_FIRED. Lowest tier_rank wins; otherwise TIER_BLOCKED (fail-closed). This is the only authoritative tier path and it reads registry_v2 (proven via pg_get_functiondef in the parity guard).
Negative tests (v_qt001_rule_engine_negative_tests / _fail_closed_guard)
11/11 pass: missing_signal_absent, not_in_set_null_present, bool_signal_null_present -> FAIL_MISSING_SIGNAL; unsupported_operator -> FAIL_UNSUPPORTED_OP; expected_result_mismatch -> FAIL_PREDICATE; superseded/inactive/draft -> NOT_PARTICIPATING; valid_grant_pass -> PASS; block_fires_on_divergence -> BLOCK_FIRED; block_clear_when_zero -> BLOCK_CLEAR. Guard pass=true.