[object Object]

← back to Rentv

PR intel: yoloforever cycle 2 — enrich lockfile + linkedin re-check

02cf415b6c723f7911504755a7232d8212d4f3b5 · 2026-08-05 15:21:56 -0700 · Steve

- linkedin-enrich.js: advisory lockfile (30-min stale) so the 6am launchd
  can't race a manual run and double-PUT contacts.
- linkedin-recheck.js: re-validate found_uncorroborated URLs against the
  tightened surname guard (cleared 14 namesakes) + demote junk-name rows to
  wrong_person (10). Reversible; $0 (no search calls).

Files touched

Diff

commit 02cf415b6c723f7911504755a7232d8212d4f3b5
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Aug 5 15:21:56 2026 -0700

    PR intel: yoloforever cycle 2 — enrich lockfile + linkedin re-check
    
    - linkedin-enrich.js: advisory lockfile (30-min stale) so the 6am launchd
      can't race a manual run and double-PUT contacts.
    - linkedin-recheck.js: re-validate found_uncorroborated URLs against the
      tightened surname guard (cleared 14 namesakes) + demote junk-name rows to
      wrong_person (10). Reversible; $0 (no search calls).
---
 src/pr/tools/linkedin-enrich.js  |  9 +++++
 src/pr/tools/linkedin-recheck.js | 74 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 83 insertions(+)

diff --git a/src/pr/tools/linkedin-enrich.js b/src/pr/tools/linkedin-enrich.js
index 8dbe50b9..32238a50 100644
--- a/src/pr/tools/linkedin-enrich.js
+++ b/src/pr/tools/linkedin-enrich.js
@@ -43,6 +43,15 @@ async function api(p, opts = {}) {
 }
 
 async function main() {
+  // Advisory lockfile (Cody cycle 1): prevent the 6am launchd from racing a manual run
+  // and double-PUTting the same contacts. Stale after 30 min.
+  const LOCK = path.join(require('os').tmpdir(), 'rentv-linkedin-enrich.lock');
+  try {
+    const prev = fs.existsSync(LOCK) ? Number(fs.readFileSync(LOCK, 'utf8')) : 0;
+    if (prev && (Date.now() - prev) < 30 * 60 * 1000) { console.error('another linkedin-enrich run holds the lock — exiting'); process.exit(0); }
+    fs.writeFileSync(LOCK, String(Date.now()));
+    process.on('exit', () => { try { fs.unlinkSync(LOCK); } catch { /* ignore */ } });
+  } catch { /* lock is best-effort */ }
   if (!search.configured()) { console.error('no search key (BRAVE_SEARCH_API_KEY) in rentv/.env — run: node ~/Projects/secrets-manager/cli.js sync'); process.exit(1); }
   // Media/PR contacts are the outreach targets — enrich them first. The /people API
   // has no organization_type filter, so build the media org-id set and sort by it.
diff --git a/src/pr/tools/linkedin-recheck.js b/src/pr/tools/linkedin-recheck.js
new file mode 100644
index 00000000..3da503c0
--- /dev/null
+++ b/src/pr/tools/linkedin-recheck.js
@@ -0,0 +1,74 @@
+'use strict';
+// Re-validate every found_uncorroborated LinkedIn URL against the tightened surname
+// guard (Cody cycle 1). Matches stored before the guard tightened may be namesakes;
+// clear any whose /in/ slug fails the surname test (set linkedin_url=null,
+// linkedin_status='none' so the daily job can re-locate correctly). Also demotes
+// obvious junk-name rows on media orgs to 'wrong_person' (reversible). Dry-run by
+// default; --apply writes. $0 (no search calls — pure re-validation of stored data).
+const API = process.env.PR_API || 'https://rentv.agentabrams.com';
+const AUTH = 'Basic ' + Buffer.from(process.env.PR_AUTH || 'admin:DW2024!').toString('base64');
+const APPLY = process.argv.includes('--apply');
+
+async function api(p, opts = {}) {
+  const r = await fetch(API + '/api/pr' + p, { ...opts, headers: { Authorization: AUTH, 'Content-Type': 'application/json', ...(opts.headers || {}) }, body: opts.body ? JSON.stringify(opts.body) : undefined });
+  if (!r.ok) throw new Error(p + ' → ' + r.status + ': ' + (await r.text()).slice(0, 120));
+  return r.json();
+}
+
+// same guard as linkedin-enrich.js (surname required + first-name/initial signal)
+function slugOk(full_name, url) {
+  const slug = String(url || '').match(/\/in\/([^/?#]+)/);
+  if (!slug) return false;
+  const toks = String(full_name).toLowerCase().split(/\s+/).map((t) => t.replace(/[^a-z]/g, '')).filter((t) => t.length > 2);
+  const lastTok = toks[toks.length - 1], firstTok = toks[0], s = slug[1].toLowerCase();
+  return !!lastTok && s.includes(lastTok) && (!firstTok || firstTok === lastTok || s.includes(firstTok) || s.includes(firstTok[0]));
+}
+
+const JUNK = /\b(award|awards|issue|issues|content|studio|systems|center|centre|insights|markets|forum|council|links|magazine|edition|subscribe|newsletter|sponsored|current|archive|archives)\b/i;
+
+async function main() {
+  // (b) re-check found_uncorroborated
+  let offset = 0, checked = 0, cleared = 0;
+  const clearedSamples = [];
+  for (;;) {
+    const page = await api(`/people?linkedin_status=found_uncorroborated&limit=500&offset=${offset}`);
+    const rows = page.rows || page.people || [];
+    if (!rows.length) break;
+    for (const p of rows) {
+      checked++;
+      if (!slugOk(p.full_name, p.linkedin_url)) {
+        if (clearedSamples.length < 10) clearedSamples.push(`${p.full_name} ⇏ ${p.linkedin_url}`);
+        cleared++;
+        if (APPLY) { try { await api('/people/' + p.id, { method: 'PUT', body: { linkedin_url: null, linkedin_status: 'none' } }); } catch { /* skip */ } }
+      }
+    }
+    offset += rows.length;
+    if (rows.length < 500) break;
+  }
+  // (c) demote junk-name rows (media orgs) — scan all people, JUNK regex on full_name
+  let demoted = 0; const demotedSamples = [];
+  offset = 0;
+  for (;;) {
+    const page = await api(`/people?limit=500&offset=${offset}`);
+    const rows = page.rows || page.people || [];
+    if (!rows.length) break;
+    for (const p of rows) {
+      if (p.lifecycle_status === 'wrong_person' || p.lifecycle_status === 'duplicate') continue;
+      if (JUNK.test(p.full_name || '') || !/\s/.test(String(p.full_name || '').trim())) {
+        // require it to look non-personish: junk word OR single-token name
+        if (JUNK.test(p.full_name || '')) {
+          if (demotedSamples.length < 10) demotedSamples.push(p.full_name);
+          demoted++;
+          if (APPLY) { try { await api('/people/' + p.id, { method: 'PUT', body: { lifecycle_status: 'wrong_person' } }); } catch { /* skip */ } }
+        }
+      }
+    }
+    offset += rows.length;
+    if (rows.length < 500) break;
+  }
+  console.log(`[linkedin-recheck] found_uncorroborated checked ${checked} · would-clear ${cleared} · junk would-demote ${demoted} · ${APPLY ? 'APPLIED' : 'DRY-RUN (--apply to write)'}`);
+  console.log('cleared samples:\n  ' + clearedSamples.join('\n  '));
+  console.log('demote samples: ' + demotedSamples.join(' | '));
+  process.exit(0);
+}
+main().catch((e) => { console.error('fatal:', e.message); process.exit(1); });

← 7701c773 auto-save: 2026-08-05T15:13:13 (8 files) — data/closings-ove  ·  back to Rentv  ·  PR intel: safe org dedup (page-title captures → canonical br ea4a72c8 →