← 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
M src/pr/tools/linkedin-enrich.jsA src/pr/tools/linkedin-recheck.js
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 →