← back to Rentv
PR-intel: add found_role_corroborated status (DTD verdict C) — role-backed search hit distinct from URL-identity-confirmed; same-run promotion; score 60; UI badge + filter + migration 011
78a39e804419a71bcfc3046b23955f22b02dfa93 · 2026-08-06 12:09:12 -0700 · Steve
Files touched
M public/admin/pr-intelligence/people.htmlM public/admin/pr-intelligence/pr-shared.jsM src/pr/jobs/index.jsM src/pr/lib/scoring.jsA src/pr/migrations/011_found_role_corroborated.sql
Diff
commit 78a39e804419a71bcfc3046b23955f22b02dfa93
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Aug 6 12:09:12 2026 -0700
PR-intel: add found_role_corroborated status (DTD verdict C) — role-backed search hit distinct from URL-identity-confirmed; same-run promotion; score 60; UI badge + filter + migration 011
---
public/admin/pr-intelligence/people.html | 2 +-
public/admin/pr-intelligence/pr-shared.js | 2 +-
src/pr/jobs/index.js | 18 +++++++++++-------
src/pr/lib/scoring.js | 2 +-
src/pr/migrations/011_found_role_corroborated.sql | 12 ++++++++++++
5 files changed, 26 insertions(+), 10 deletions(-)
diff --git a/public/admin/pr-intelligence/people.html b/public/admin/pr-intelligence/people.html
index 72e5be00..8059fe03 100644
--- a/public/admin/pr-intelligence/people.html
+++ b/public/admin/pr-intelligence/people.html
@@ -20,7 +20,7 @@
{ key: 'metro', type: 'select', options: [['', 'Metro: all'], ...CA.metros.map((m) => [m.key, 'CA · ' + m.label]), ...AZ.metros.map((m) => [m.key, 'AZ · ' + m.label])] },
{ key: 'status', type: 'select', options: [['', 'Status: all']].concat('discovered,needs_verification,verified,stale,wrong_person,left_company,duplicate,suppressed,ready_for_outreach,contacted,replied'.split(',').map((s) => [s, s.replace(/_/g, ' ')])) },
{ key: 'email_status', type: 'select', options: [['', 'Email: all'], ['manually_verified', 'manually verified'], ['company_site_verified', 'site verified'], ['press_release_verified', 'press verified'], ['corroborated', 'corroborated'], ['inferred', '⚠ inferred'], ['unverified', 'unverified']] },
- { key: 'linkedin_status', type: 'select', options: [['', 'LinkedIn: all'], ['found_corroborated', 'found + corroborated'], ['found_uncorroborated', 'found, not corroborated'], ['manually_verified', 'manually verified'], ['none', 'none']] },
+ { key: 'linkedin_status', type: 'select', options: [['', 'LinkedIn: all'], ['found_corroborated', 'found + corroborated'], ['found_role_corroborated', 'found + role-corroborated'], ['found_uncorroborated', 'found, not corroborated'], ['manually_verified', 'manually verified'], ['none', 'none']] },
{ key: 'outreach_eligible', type: 'select', options: [['', 'Eligible: any'], ['1', 'Outreach-eligible'], ['0', 'Not eligible']] },
], () => { offset = 0; load(); });
c.appendChild(bar);
diff --git a/public/admin/pr-intelligence/pr-shared.js b/public/admin/pr-intelligence/pr-shared.js
index bc375db1..0589512d 100644
--- a/public/admin/pr-intelligence/pr-shared.js
+++ b/public/admin/pr-intelligence/pr-shared.js
@@ -115,7 +115,7 @@
verifBadge(s) {
const map = { manually_verified: 'green', company_site_verified: 'green', press_release_verified: 'green', registry_verified: 'green',
corroborated: 'blue', search_indexed: 'amber', inferred: 'red', unverified: 'gray', stale: 'amber', invalid: 'red',
- found_corroborated: 'blue', found_uncorroborated: 'amber', none: 'gray' };
+ found_corroborated: 'blue', found_role_corroborated: 'purple', found_uncorroborated: 'amber', none: 'gray' };
return PR.badge((s || 'unverified').replace(/_/g, ' '), map[s] || 'gray');
},
diff --git a/src/pr/jobs/index.js b/src/pr/jobs/index.js
index e1b5b5a7..09c3b5e0 100644
--- a/src/pr/jobs/index.js
+++ b/src/pr/jobs/index.js
@@ -293,11 +293,13 @@ const handlers = {
const hit = rep.items[0];
if (hit) {
await people.update(pid, { linkedin_url: hit.linkedin_url, linkedin_status: 'found_uncorroborated' }, 'system:verify-person');
- // NOTE (open design question — see pending-approval memo): the corroboration check below reads
- // a STALE in-memory `p` (loaded at the top), so a JUST-located URL is only promoted to
- // found_corroborated on a FUTURE re-verify, not same-run. Whether that promotion should happen
- // at all on org+title site evidence (which corroborates the ROLE, not the URL's identity) is a
- // trust-model decision for Steve — deliberately NOT changed here.
+ // Keep the in-memory `p` fresh so the corroboration check below sees the JUST-located URL and
+ // promotes same-run (was a stale-read that deferred promotion to a FUTURE re-verify). DTD verdict
+ // C (2026-08-06): org+title site evidence corroborates the ROLE, not the URL's identity, so the
+ // promotion target below is `found_role_corroborated` — NOT `found_corroborated` (which is reserved
+ // for URL-identity proof: direct profile fetch / company-page LinkedIn link / manual review).
+ p.linkedin_url = hit.linkedin_url;
+ p.linkedin_status = 'found_uncorroborated';
await db.query('UPDATE pr_people SET linkedin_indexed_title=$2, linkedin_indexed_snippet=$3 WHERE id=$1', [pid, hit.indexed_title, hit.indexed_snippet]);
await people.attachEvidence(pid, {
source: { source_type: 'search_api', source_name: 'LinkedIn locate (authorized search)', url: hit.source_url, short_excerpt: hit.indexed_snippet, is_primary_source: false, license_or_usage_note: li.sourceMeta().license_or_usage_note, adapter: 'linkedin' },
@@ -305,11 +307,13 @@ const handlers = {
}, 'system:verify-person');
}
}
- // Cross-check: company-site evidence for org+title already? then linkedin found → corroborated.
+ // Cross-check: org+title site evidence present? Then a located LinkedIn URL is ROLE-corroborated
+ // (the role is site-backed) — but NOT identity-confirmed for the URL itself. Promote to the
+ // intermediate found_role_corroborated; found_corroborated stays reserved for URL-identity proof.
const ev = await sources.evidenceFor('person', pid);
const siteBacked = ev.some((e) => ['org_website', 'press_release', 'gov_registry', 'manual'].includes(e.source_type) && ['exact_title', 'organization'].includes(e.field_name));
if (p.linkedin_url && siteBacked && p.linkedin_status === 'found_uncorroborated') {
- await people.update(pid, { linkedin_status: 'found_corroborated' }, 'system:verify-person');
+ await people.update(pid, { linkedin_status: 'found_role_corroborated' }, 'system:verify-person');
}
if (siteBacked && p.lifecycle_status === 'discovered') {
await people.update(pid, { lifecycle_status: 'needs_verification' }, 'system:verify-person');
diff --git a/src/pr/lib/scoring.js b/src/pr/lib/scoring.js
index a06ae51d..65e091c0 100644
--- a/src/pr/lib/scoring.js
+++ b/src/pr/lib/scoring.js
@@ -60,7 +60,7 @@ function personConfidence(person, evidenceRows) {
else if (person.public_work_email && ev === 'inferred') contact = 20; // stored, flagged, never sendable
else if (person.public_business_phone) contact = 50;
const li = String(person.linkedin_status || 'none');
- const liScore = li === 'manually_verified' ? 100 : li === 'found_corroborated' ? 80 : li === 'found_uncorroborated' ? 35 : 0;
+ const liScore = li === 'manually_verified' ? 100 : li === 'found_corroborated' ? 80 : li === 'found_role_corroborated' ? 60 : li === 'found_uncorroborated' ? 35 : 0;
const score = Math.round(src * 0.45 + contact * 0.25 + liScore * 0.1 + fresh * 0.2);
return { score: Math.min(100, score), components: { source_confidence: src, contact_method: contact, linkedin: liScore, freshness: fresh } };
}
diff --git a/src/pr/migrations/011_found_role_corroborated.sql b/src/pr/migrations/011_found_role_corroborated.sql
new file mode 100644
index 00000000..49c08c1c
--- /dev/null
+++ b/src/pr/migrations/011_found_role_corroborated.sql
@@ -0,0 +1,12 @@
+-- 011_found_role_corroborated.sql
+-- Add an intermediate LinkedIn corroboration state (TK-10297 follow-up; DTD verdict C, 2026-08-06, 4/5).
+--
+-- verify-person locates a person's LinkedIn URL via authorized surname-slug search, then promotes it
+-- on org+title SITE evidence. That evidence corroborates the person's ROLE — NOT that the search-located
+-- URL's IDENTITY is actually that human. Collapsing both meanings into `found_corroborated` violates the
+-- system's evidence-honesty ethos (it already refuses to count inferred/hand-typed contacts). So:
+-- found_role_corroborated = "role confirmed (org+title site-backed), URL identity NOT yet confirmed"
+-- found_corroborated = reserved for URL-identity proof (direct profile fetch / company-page
+-- LinkedIn link / manual review)
+-- Idempotent (IF NOT EXISTS). Scoring is JS-side (see src/pr/lib/scoring.js), so enum ordering is irrelevant.
+ALTER TYPE pr_linkedin_status ADD VALUE IF NOT EXISTS 'found_role_corroborated';
← 8d4a638e feat(services): guarantee >=1 vendor per category — curated
·
back to Rentv
·
fix(deploy-frontpage): zsh array for ssh opts (plain string 0121172a →