CRE P4 Rails Audit — every rail/deny rule vs its failing-path test

Date: 2026-08-17 Lane: cre-p4-rails (session that built P1, 0a6d36de) Scope: cells 38 + 39 compliance rails and deny rules. Additive test files only — no rail code changed, no other lane's files edited. Gate satisfied: plan-of-record P4 gate — "every deny-list rule has a test that fails when the rule is removed."

Method

Grep-driven matrix: every failure reason emitted by src/cells/cell39/outreach_cell/passport.py, every term in outreach_cell/voice.py, every key in the Cell 38 deny/PII frozensets (cre_mappers.DENY_LISTED_FIELDS, RAW_PERSON_FIELDS, prospecting_cell/profiles.BLOCKED_TARGETING_FEATURES), mapped against tests/cre_outreach/** and src/cells/cell38/tests/**. Commands and counts are reproducible from the tree at this commit's parent.

Findings (pre-fix, measured)

Rail surface Rules Seeded failing-path tests Verdict
Passport check 1 (quality) reasons 8 8 covered
Passport check 2 (compliance) reasons 9 5 4 branches untested: tcpa_consent_basis_missing, license_disclosure_unconfigured, sender_postal_address_unconfigured (+recipient_bounce_suppressed in check 5)
Passport check 3 (market freshness) 1 1 covered
Passport check 4 (claims / O2 "80%") 2 2 covered
Passport check 5 (opt-out) reasons 2 1 recipient_bounce_suppressed untested
FORBIDDEN_TERMS_BASE 13 2 11 terms unseeded
FORBIDDEN_TERMS_RESIDENTIAL (Fair-Housing steering) 12 2 10 terms unseeded
Bounce classification (classify_bounce) hard/soft/none/conservative 0 direct untested
Cell 38 DENY_LISTED_FIELDS 31 keys 7 24 keys unseeded
Cell 38 BLOCKED_TARGETING_FEATURES 32 keys 12 20 keys unseeded
Cell 38 RAW_PERSON_FIELDS 15 keys 5 10 keys unseeded
Freshness gate (cell 38 submarket) 4 failing paths 4 covered (P1)
Consent gates (parse fail-closed; person-resolution) 2 2 covered
AUTO_SEND / holdout preconditions 2 2 covered (P3, AST-asserted)

The exposure class: a rule silently REMOVED from a list (one line of diff) would not have turned any test red for the uncovered entries — precisely the failure mode rule 01 names ("a gate that does not cover a surface cannot defend it").

Fix (this landing)

Two additive batteries, each using two mechanisms per rail:

  1. Literal ratchet lists asserted ⊆ the shipped set — removing a shipped rule turns red; adding one never requires editing the test (floor, not equality — the TRUTH 5.1.1 trap avoided).
  2. Seeded behaviour — every entry driven through the real refusal path (full passport evaluation / mapper / feature gate) with the specific check and reason asserted.
File Tests Covers
tests/cre_outreach/test_p4_rails_coverage.py 42 all 13+12 forbidden terms through the passport (both profiles), steering-term residential scoping pinned in both directions, the 4 untested passport branches, bounce threshold both directions (1 bounce ≠ suppressed; 2 = suppressed), classify_bounce 6 cases incl. the conservative default, erasure-keeps-suppression, residential control draft passes (non-vacuity)
src/cells/cell38/tests/test_p4_deny_coverage.py 161 all 31 deny keys × both roster mappers, all 32 blocked features (alone + hidden among legal features), all 15 raw-person fields × both mappers, case-insensitivity, intake↔scoring rail-divergence guard

The TCPA passport branch is reached by dataclasses.replace on a parsed match — parse_prospect_match itself refuses an empty basis (fail-closed, already tested), so the passport re-check is exercised as designed: a basis stripped AFTER intake.

Measured results (fresh run, this tree)

Honest limits

← All docsView source on GitHub →