← 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
M src/pr/index.jsM src/pr/services/organizations.jsM src/pr/services/settings.js
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 →