[object Object]

← back to Rentv

PR-intel CA gate: count a verified general_press_email as a contact

466646373e06530936013c52bedced159929b484 · 2026-08-06 10:47:12 -0700 · Steve Abrams

The contact_coverage gate only counted an org as "covered" if it had a
pr_people row in a comms/leadership role, so orgs reachable via a verified
org-level press channel (e.g. Lockton's media@lockton.com) scored as
"no PR contact" and held the CA gate under 70%.

Extend the with_contact FILTER in settings.stateGate with an OR arm that
also counts an org whose general_press_email is present AND backed by a
pr_field_evidence row for field_name='general_press_email' — so only
verified, evidence-backed press channels count, never an inferred or
hand-typed address (mirrors the org_completeness has_evidence gate and the
no_mislabeled_inferred rule). The stricter comms_only sub-metric is left
untouched and still reported.

Mirror the same definition in organizations.list's no_pr_contact filter and
the dashboard's orgs_without_pr_contact count so all three agree.

TK-10297. Verified against throwaway rentv_pr_test: an evidence-backed
press-email org counts; an inferred one and a bare org do not. Live deploy
Steve-gated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 466646373e06530936013c52bedced159929b484
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 6 10:47:12 2026 -0700

    PR-intel CA gate: count a verified general_press_email as a contact
    
    The contact_coverage gate only counted an org as "covered" if it had a
    pr_people row in a comms/leadership role, so orgs reachable via a verified
    org-level press channel (e.g. Lockton's media@lockton.com) scored as
    "no PR contact" and held the CA gate under 70%.
    
    Extend the with_contact FILTER in settings.stateGate with an OR arm that
    also counts an org whose general_press_email is present AND backed by a
    pr_field_evidence row for field_name='general_press_email' — so only
    verified, evidence-backed press channels count, never an inferred or
    hand-typed address (mirrors the org_completeness has_evidence gate and the
    no_mislabeled_inferred rule). The stricter comms_only sub-metric is left
    untouched and still reported.
    
    Mirror the same definition in organizations.list's no_pr_contact filter and
    the dashboard's orgs_without_pr_contact count so all three agree.
    
    TK-10297. Verified against throwaway rentv_pr_test: an evidence-backed
    press-email org counts; an inferred one and a bare org do not. Live deploy
    Steve-gated.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 src/pr/index.js                  |  8 +++++++-
 src/pr/services/organizations.js |  9 ++++++++-
 src/pr/services/settings.js      | 20 +++++++++++++++-----
 3 files changed, 30 insertions(+), 7 deletions(-)

diff --git a/src/pr/index.js b/src/pr/index.js
index 319510aa..d157d27d 100644
--- a/src/pr/index.js
+++ b/src/pr/index.js
@@ -262,11 +262,17 @@ module.exports = function mountPR(app, { adminOnly, sendPage }) {
       `SELECT m AS metro, count(*)::int AS n FROM pr_organizations, unnest(metros) m
         WHERE lifecycle_status NOT IN ('duplicate','archived') GROUP BY 1 ORDER BY n DESC`, []);
     const excludeTypes = await settings.get('coverage_exclude_types', ['bank', 'credit_union']);
+    // Coherent with the CA gate's contact_coverage + the organizations.list no_pr_contact filter:
+    // an org counts as "no PR contact" only if it has NO named comms/agency person AND NO
+    // verified (evidence-backed) general_press_email.
     const noPr = await db.one(
       `SELECT count(*)::int AS n FROM pr_organizations o
         WHERE lifecycle_status NOT IN ('duplicate','archived') AND organization_type <> ALL($1::text[])
           AND NOT EXISTS (SELECT 1 FROM pr_people p WHERE p.organization_id=o.id
-             AND p.department IN ('communications','agency') AND p.lifecycle_status NOT IN ('duplicate','suppressed'))`, [excludeTypes]);
+             AND p.department IN ('communications','agency') AND p.lifecycle_status NOT IN ('duplicate','suppressed'))
+          AND NOT (o.general_press_email IS NOT NULL AND EXISTS (
+             SELECT 1 FROM pr_field_evidence e WHERE e.entity_type='organization'
+               AND e.entity_id = o.id AND e.field_name = 'general_press_email'))`, [excludeTypes]);
     const agencies = await db.rows(
       `SELECT o.id, o.display_name,
               (SELECT count(*) FROM pr_org_relationships r WHERE r.source_organization_id=o.id AND r.relationship_type='agency_client')::int AS clients
diff --git a/src/pr/services/organizations.js b/src/pr/services/organizations.js
index cdd673c6..615df5e8 100644
--- a/src/pr/services/organizations.js
+++ b/src/pr/services/organizations.js
@@ -162,8 +162,15 @@ async function list(f = {}) {
   if (f.min_priority) add(`priority_score >= ?`, Number(f.min_priority));
   if (f.verified_before) add(`(last_verified_at IS NULL OR last_verified_at < ?)`, f.verified_before);
   if (f.no_pr_contact === '1' || f.no_pr_contact === true) {
+    // Mirror the CA gate's contact_coverage definition (settings.stateGate): an org is "covered"
+    // — i.e. NOT a no-PR-contact org — when it has a named comms/agency person OR a VERIFIED,
+    // evidence-backed general_press_email. The press-email arm is gated on pr_field_evidence so a
+    // guessed/inferred address never counts as reachable.
     where.push(`NOT EXISTS (SELECT 1 FROM pr_people p WHERE p.organization_id = pr_organizations.id
-                 AND p.department IN ('communications','agency') AND p.lifecycle_status NOT IN ('duplicate','suppressed'))`);
+                 AND p.department IN ('communications','agency') AND p.lifecycle_status NOT IN ('duplicate','suppressed'))
+                AND NOT (pr_organizations.general_press_email IS NOT NULL AND EXISTS (
+                  SELECT 1 FROM pr_field_evidence e WHERE e.entity_type='organization'
+                    AND e.entity_id = pr_organizations.id AND e.field_name = 'general_press_email'))`);
   }
   if (!f.include_duplicates) where.push(`lifecycle_status <> 'duplicate'`);
   const sortable = { name: 'display_name', type: 'organization_type', status: 'lifecycle_status', confidence: 'confidence_score', priority: 'priority_score', verified: 'last_verified_at', created: 'created_at', updated: 'updated_at', people: 'people_count', evidence: 'evidence_count', pr: 'internal_pr_status' };
diff --git a/src/pr/services/settings.js b/src/pr/services/settings.js
index a4c7a2d6..a6cf5e2a 100644
--- a/src/pr/services/settings.js
+++ b/src/pr/services/settings.js
@@ -70,14 +70,24 @@ async function stateGate(state = 'CA') {
   // comms staff (banks/credit unions) are out of the coverage DENOMINATOR — they stay
   // in the catalog, just not counted as gate-relevant outreach targets.
   const exclude = await get('coverage_exclude_types', ['bank', 'credit_union']);
+  // An org is "covered" if it has a named comms/leadership/etc. person OR a VERIFIED,
+  // evidence-backed org-level press channel (general_press_email). The press-email arm is
+  // gated on the presence of pr_field_evidence for `general_press_email` — an inferred or
+  // hand-typed address with no source does NOT count (mirrors the org_completeness has_evidence
+  // gate + the no_mislabeled_inferred rule: reachability must be evidence-backed, never guessed).
   const hi = await db.one(
-    `WITH hi AS (SELECT id FROM pr_organizations WHERE $1=ANY(state_presence) AND priority_score >= 60
+    `WITH hi AS (SELECT id, general_press_email FROM pr_organizations WHERE $1=ANY(state_presence) AND priority_score >= 60
                  AND lifecycle_status NOT IN ('duplicate','archived') AND organization_type <> ALL($2::text[]))
      SELECT count(*) AS n,
-            count(*) FILTER (WHERE EXISTS (
-              SELECT 1 FROM pr_people p WHERE p.organization_id = hi.id
-                AND p.lifecycle_status NOT IN ('duplicate','suppressed','wrong_person','left_company')
-                AND p.department IN ('communications','marketing','leadership','bd','agency'))) AS with_contact
+            count(*) FILTER (WHERE
+              (hi.general_press_email IS NOT NULL AND EXISTS (
+                 SELECT 1 FROM pr_field_evidence e
+                  WHERE e.entity_type='organization' AND e.entity_id = hi.id
+                    AND e.field_name = 'general_press_email'))
+              OR EXISTS (
+                SELECT 1 FROM pr_people p WHERE p.organization_id = hi.id
+                  AND p.lifecycle_status NOT IN ('duplicate','suppressed','wrong_person','left_company')
+                  AND p.department IN ('communications','marketing','leadership','bd','agency'))) AS with_contact
        FROM hi`, [ST, exclude]);
   const nHi = Number(hi.n), nWith = Number(hi.with_contact);
   // Strict lens (informational, Cody cycle 2): comms/marketing/agency/bd ONLY — the

← 41fe6207 rentv front page: feature Boomer's CRE Talk video (vimeo 117  ·  back to Rentv  ·  PR-intel: soft-delete (Discard) for outreach drafts a8efed7c →