← back to Rentv 2026
pr-intelligence: Cody c3 — two-tier name predicate (HARD title/product words reject anywhere; SOFT filler needs all-tokens), 16/16 known junk rejected, 10/10 real names pass
b3dfe478cbf1159f06c120302e2d34bc3c043f16 · 2026-07-30 20:04:04 -0700 · Steve Abrams
Files touched
M src/pr/lib/normalize.jsM test/pr/integration.test.js
Diff
commit b3dfe478cbf1159f06c120302e2d34bc3c043f16
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 30 20:04:04 2026 -0700
pr-intelligence: Cody c3 — two-tier name predicate (HARD title/product words reject anywhere; SOFT filler needs all-tokens), 16/16 known junk rejected, 10/10 real names pass
---
src/pr/lib/normalize.js | 51 ++++++++++++++++++++++++++++++---------------
test/pr/integration.test.js | 4 ++--
2 files changed, 36 insertions(+), 19 deletions(-)
diff --git a/src/pr/lib/normalize.js b/src/pr/lib/normalize.js
index 0811478c..a959490c 100644
--- a/src/pr/lib/normalize.js
+++ b/src/pr/lib/normalize.js
@@ -71,34 +71,51 @@ function normalizeLinkedInUrl(url) {
return `https://www.linkedin.com/${m[1].toLowerCase()}/${m[2].replace(/\/+$/, '')}`;
}
-// Nav-chrome / section-header words that masquerade as Name-Case people on team pages
-// ("Who We Are", "Personal Login", "Leaving Exchange Bank" — all confirmed in prod).
-const COMMON_NONNAME_WORDS = new Set([
+// Two-tier non-name vocabulary (Cody gates, cycles 2+3).
+// HARD: title / product / section words that are NEVER part of a wanted human name —
+// ANY occurrence rejects ("Chief People Officer", "First Industrial Realty",
+// "Statement Savings Account" all die here even when other tokens look name-ish).
+// SOFT: filler that only rejects when the WHOLE string is filler ("Who We Are") —
+// so "Larry Page"-class real names with one ambiguous token survive.
+const HARD_NONNAME_WORDS = new Set([
+ 'chief', 'officer', 'officers', 'president', 'vice', 'deputy', 'manager', 'director',
+ 'directors', 'executive', 'senior', 'junior', 'associate', 'assistant', 'coordinator',
+ 'specialist', 'analyst', 'administrator', 'admin', 'oversight', 'leadership',
+ 'management', 'market', 'markets', 'money', 'savings', 'checking', 'account',
+ 'accounts', 'loan', 'loans', 'mortgage', 'deposit', 'deposits', 'realty', 'properties',
+ 'commercial', 'residential', 'industrial', 'intelligence', 'spotlight', 'industry',
+ 'success', 'stories', 'story', 'choose', 'features', 'feature', 'improvement',
+ 'performance', 'venues', 'venue', 'center', 'centers', 'centre', 'data', 'solutions',
+ 'strategies', 'insights', 'overview', 'resources', 'foundation', 'login', 'banking',
+ 'welcome', 'careers', 'privacy', 'policy', 'profile', 'profiles', 'employee',
+ 'employees', 'portal', 'menu', 'website', 'contact', 'services', 'staff', 'why',
+ 'how', 'what', 'statement', 'client', 'clients', 'testimonials', 'awards',
+]);
+const SOFT_NONNAME_WORDS = new Set([
'who', 'we', 'are', 'our', 'your', 'the', 'and', 'about', 'us', 'meet', 'team',
- 'board', 'directors', 'director', 'management', 'leadership', 'staff', 'people',
- 'login', 'log', 'sign', 'online', 'personal', 'business', 'banking', 'bank',
- 'welcome', 'home', 'contact', 'leaving', 'exchange', 'visit', 'partner', 'partners',
- 'website', 'privacy', 'policy', 'careers', 'news', 'events', 'services', 'search',
- 'menu', 'group', 'company', 'officers', 'executive', 'senior', 'more', 'view', 'all',
- 'of', 'for', 'with', 'from', 'to', 'in', 'at', 'on', 'read', 'learn', 'get', 'our',
- 'profile', 'profiles', 'employee', 'employees', 'account', 'portal', 'apply', 'join',
- 'see', 'click', 'bio', 'bios', 'info', 'details', 'page', 'load', 'show', 'full',
+ 'board', 'people', 'home', 'news', 'events', 'search', 'group', 'company', 'more',
+ 'view', 'all', 'of', 'for', 'with', 'from', 'to', 'in', 'at', 'on', 'read', 'learn',
+ 'get', 'see', 'click', 'bio', 'bios', 'info', 'details', 'page', 'load', 'show',
+ 'full', 'apply', 'join', 'visit', 'partner', 'partners', 'business', 'bank', 'banks',
+ 'community', 'online', 'personal', 'leaving', 'exchange', 'sign', 'log', 'head',
+ 'lead', 'agent', 'live', 'first', 'trust', 'united', 'american', 'pacific',
]);
/**
- * Is this string plausibly a HUMAN name? (Cody gate, cycle 2: nav chrome like
- * "Who We Are" classified as leadership and polluted the people table.)
- * Requires 2–4 tokens, each ≥2 chars, no digits/HTML entities, ≤40 chars, and at
- * least one token that is NOT a common non-name word.
+ * Is this string plausibly a HUMAN name? Shape: 2–4 alpha tokens, ≤40 chars, no
+ * digits/entities; NO hard non-name token anywhere; at least one token that isn't
+ * soft filler. Callers can override for legit unusual names (audited).
*/
function isLikelyPersonName(name) {
const s = String(name || '').trim();
if (s.length < 4 || s.length > 40) return false;
- if (/[&<>@\d;#]/.test(s)) return false; // entities, emails, digits
+ if (/[&<>@\d;#|]/.test(s)) return false; // entities, emails, digits, pipes
const tokens = s.split(/\s+/);
if (tokens.length < 2 || tokens.length > 4) return false;
if (!tokens.every((t) => /^[A-Za-z'’.-]{2,}$/.test(t) || /^[A-Z]\.?$/.test(t))) return false;
- const hasProper = tokens.some((t) => !COMMON_NONNAME_WORDS.has(t.toLowerCase().replace(/[^a-z]/g, '')));
+ const low = tokens.map((t) => t.toLowerCase().replace(/[^a-z]/g, ''));
+ if (low.some((t) => HARD_NONNAME_WORDS.has(t))) return false;
+ const hasProper = low.some((t) => !SOFT_NONNAME_WORDS.has(t));
return hasProper;
}
diff --git a/test/pr/integration.test.js b/test/pr/integration.test.js
index b4bf54c2..2dd03cd8 100644
--- a/test/pr/integration.test.js
+++ b/test/pr/integration.test.js
@@ -97,8 +97,8 @@ test('inferred email can be stored but NEVER becomes outreach-eligible', async (
test('campaign enrollment is transactional and skips suppressed contacts', async () => {
const org = (await organizations.create({ display_name: 'Campaign Org', website_url: 'https://campaign-org-test.com', organization_type: 'pr_agency', state_presence: ['CA'], metros: ['la'] }, { evidence: SRC('campaign org'), actor: 'test' })).org;
- const good = (await people.create({ full_name: 'Good Contact', exact_title: 'Account Director', organization_id: org.id, public_work_email: 'good@campaign-org-test.com', email_verification_status: 'company_site_verified' }, { evidence: SRC('good'), actor: 'test' })).person;
- const bad = (await people.create({ full_name: 'Suppressed Contact', exact_title: 'Media Relations Director', organization_id: org.id, public_work_email: 'stop@campaign-org-test.com', email_verification_status: 'company_site_verified' }, { evidence: SRC('bad'), actor: 'test' })).person;
+ const good = (await people.create({ full_name: 'Gwen Goodman', exact_title: 'Account Director', organization_id: org.id, public_work_email: 'good@campaign-org-test.com', email_verification_status: 'company_site_verified' }, { evidence: SRC('good'), actor: 'test' })).person;
+ const bad = (await people.create({ full_name: 'Sam Suppressa', exact_title: 'Media Relations Director', organization_id: org.id, public_work_email: 'stop@campaign-org-test.com', email_verification_status: 'company_site_verified' }, { evidence: SRC('bad'), actor: 'test' })).person;
await suppression.add({ email: 'stop@campaign-org-test.com', person_id: bad.id, reason: 'opt_out', actor: 'test' });
const camp = await campaigns.create({ name: 'Test Campaign', state: 'CA' }, 'test');
const res = await campaigns.enroll(camp.id, [good.id, bad.id], 'test');
← d37f9138 pr-intelligence: headless team-page extractor (local Playwri
·
back to Rentv 2026
·
yoloforever: cycle 3 ledger (SHIP 3/3 local) 6ffb2d7c →