[object Object]

← back to Rentv 2026

pr-intelligence: alias-qualify all whole-row reads (fleet pre-deploy lint: no bare SELECT *)

1bcdcd8d92bc176cbe169cb66554654b9dd84321 · 2026-07-30 15:16:02 -0700 · Steve Abrams

Files touched

Diff

commit 1bcdcd8d92bc176cbe169cb66554654b9dd84321
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 30 15:16:02 2026 -0700

    pr-intelligence: alias-qualify all whole-row reads (fleet pre-deploy lint: no bare SELECT *)
---
 public/backend.html              |   1 +
 public/summit-leads.html         | 152 +++++++++++++++++++++++++++++++++++++++
 server.js                        |   6 +-
 src/pr/index.js                  |  12 ++--
 src/pr/jobs/index.js             |  14 ++--
 src/pr/services/audit.js         |   8 +--
 src/pr/services/campaigns.js     |   4 +-
 src/pr/services/importexport.js  |   4 +-
 src/pr/services/letters.js       |  12 ++--
 src/pr/services/organizations.js |  12 ++--
 src/pr/services/outreach.js      |  12 ++--
 src/pr/services/people.js        |  12 ++--
 src/pr/services/relationships.js |   2 +-
 src/pr/services/runs.js          |   4 +-
 src/pr/services/sources.js       |   2 +-
 src/pr/services/suppression.js   |   2 +-
 test/pr/integration.test.js      |   4 +-
 17 files changed, 209 insertions(+), 54 deletions(-)

diff --git a/public/backend.html b/public/backend.html
index 4ed0abad..1095e6e8 100644
--- a/public/backend.html
+++ b/public/backend.html
@@ -35,6 +35,7 @@
   <div class="sect">Intelligence &amp; CRM</div>
   <div class="grid">
     <a class="card" href="/desk"><div class="ico">🗂️</div><b>Broker &amp; Owner Desk</b><span>382k brokers · 34k firms · sublease registry</span></a>
+    <a class="card" href="/summit-leads"><div class="ico">🎟️</div><b>Summit Leads</b><span>Invite + sponsor-target lists from the registry</span></a>
     <a class="card" href="/audience"><div class="ico">🎯</div><b>Audience &amp; CRM</b><span>Subscribers + sublease brokers, auto-segmented</span></a>
     <a class="card" href="/desk-admin"><div class="ico">✍️</div><b>Desk Admin</b><span>Add / withdraw sublease inventory</span></a>
     <a class="card" href="/admin/pr-intelligence"><div class="ico">📡</div><b>CRE PR Intelligence</b><span>CA/AZ press-contact research · letters · replies</span></a>
diff --git a/public/summit-leads.html b/public/summit-leads.html
new file mode 100644
index 00000000..a625e0f0
--- /dev/null
+++ b/public/summit-leads.html
@@ -0,0 +1,152 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Summit Leads — RENTV Conferences</title>
+<style>
+  :root{--navy:#0d1b2a;--ink:#12263a;--line:#e2e8f0;--red:#c0392b;--muted:#5b6b7b;--blue:#0a6cff;}
+  *{box-sizing:border-box}
+  body{margin:0;font:15px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:var(--ink);background:#f6f8fb}
+  header{background:var(--navy);color:#fff;padding:20px 28px;display:flex;align-items:baseline;gap:14px;flex-wrap:wrap}
+  header h1{margin:0;font-size:20px;font-weight:800;letter-spacing:.01em}
+  header h1 span{color:var(--red)}
+  header .sub{color:#8ea3b8;font-size:13px}
+  header a{margin-left:auto;color:#c8d3df;text-decoration:none;font-size:13px;border:1px solid #2a3f56;padding:6px 12px;border-radius:8px}
+  header a:hover{color:#fff;border-color:#3d5568}
+  .wrap{max-width:1200px;margin:0 auto;padding:22px 28px 60px}
+  .note{background:#fff7ed;border:1px solid #fed7aa;color:#9a3412;border-radius:10px;padding:12px 16px;font-size:13.5px;margin:0 0 20px}
+  .note b{color:#7c2d12}
+  .controls{display:flex;gap:14px;flex-wrap:wrap;align-items:flex-end;background:#fff;border:1px solid var(--line);border-radius:12px;padding:16px 18px;margin-bottom:18px}
+  .fld{display:flex;flex-direction:column;gap:5px}
+  .fld label{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:700}
+  select,input[type=text],input[type=number]{border:1px solid var(--line);border-radius:8px;padding:8px 10px;font:inherit;background:#fff;min-width:130px}
+  .seg{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden}
+  .seg button{border:0;background:#fff;padding:8px 14px;font:inherit;font-weight:600;color:var(--muted);cursor:pointer}
+  .seg button.on{background:var(--navy);color:#fff}
+  .chk{display:flex;align-items:center;gap:7px;font-size:13.5px;padding-bottom:8px}
+  .go{background:var(--blue);color:#fff;border:0;border-radius:8px;padding:9px 18px;font:inherit;font-weight:700;cursor:pointer}
+  .go:hover{filter:brightness(1.06)}
+  .exp{background:#fff;border:1px solid var(--line);border-radius:8px;padding:9px 14px;font:inherit;font-weight:600;cursor:pointer;color:var(--ink)}
+  .exp:disabled{opacity:.45;cursor:default}
+  .meta{display:flex;justify-content:space-between;align-items:center;margin:0 2px 10px;color:var(--muted);font-size:13px;gap:10px;flex-wrap:wrap}
+  table{width:100%;border-collapse:collapse;background:#fff;border:1px solid var(--line);border-radius:12px;overflow:hidden}
+  th,td{text-align:left;padding:10px 14px;border-bottom:1px solid #eef2f7;font-size:14px}
+  th{background:#f1f5f9;font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);position:sticky;top:0}
+  tr:last-child td{border-bottom:0}
+  td.num{text-align:right;font-variant-numeric:tabular-nums}
+  .tier{display:inline-block;font-size:11px;font-weight:700;padding:2px 8px;border-radius:999px}
+  .t-a{background:#dcfce7;color:#166534}.t-b{background:#dbeafe;color:#1e40af}.t-c{background:#f1f5f9;color:#475569}
+  .rank{color:var(--muted);font-variant-numeric:tabular-nums}
+  .empty{padding:40px;text-align:center;color:var(--muted)}
+  .spin{display:inline-block;width:14px;height:14px;border:2px solid #cbd5e1;border-top-color:var(--blue);border-radius:50%;animation:sp .7s linear infinite;vertical-align:-2px}
+  @keyframes sp{to{transform:rotate(360deg)}}
+</style>
+</head>
+<body>
+<header>
+  <h1>REN<span>TV</span> · Summit Leads</h1>
+  <span class="sub">Invite &amp; sponsor-target lists from the live CA registry — 383k brokers · 34.8k firms</span>
+  <a href="/backend">← Backend</a>
+</header>
+<div class="wrap">
+  <p class="note"><b>Targeting tool, not a send list.</b> The registry is the CA DRE license roster — names, firm, city, license &amp; size only. It has <b>no email, phone, or website</b>, and no residential/commercial flag. "Commercial only" searches the firm/broker name for <i>commercial</i> as a CRE proxy; firm size (<i>agents</i>) is the sponsor-budget proxy. Export a shortlist → work it via LinkedIn, mail-match, or the newsletter/CRM. A true ad-signal sponsor scan still needs each firm's domain resolved (offer to run that on an exported shortlist).</p>
+
+  <div class="controls">
+    <div class="fld"><label>List</label>
+      <div class="seg" id="typeSeg">
+        <button data-type="firms" class="on">Firms (sponsors)</button>
+        <button data-type="brokers">Brokers (delegates)</button>
+      </div>
+    </div>
+    <div class="fld"><label>Metro</label>
+      <select id="metro"><option value="">All California</option></select>
+    </div>
+    <div class="fld" id="minWrap"><label>Min agents</label>
+      <input type="number" id="minAgents" value="25" min="0" step="5" style="width:110px">
+    </div>
+    <div class="fld"><label>Search name</label>
+      <input type="text" id="q" placeholder="e.g. NAI, CBRE, capital" style="width:180px">
+    </div>
+    <label class="chk"><input type="checkbox" id="commercial" checked> Commercial only</label>
+    <button class="go" id="run">Build list</button>
+    <button class="exp" id="csv" disabled>Export CSV</button>
+  </div>
+
+  <div class="meta"><span id="status">Set filters and build a list.</span><span id="hint"></span></div>
+  <table id="tbl"><thead></thead><tbody><tr><td class="empty">No list yet.</td></tr></tbody></table>
+</div>
+
+<script>
+let CUR = { type:'firms', rows:[], metro:'' };
+const $ = s => document.querySelector(s);
+const esc = s => String(s==null?'':s).replace(/[<>&"]/g,c=>({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]));
+const cap = s => String(s||'').toLowerCase().replace(/\b\w/g,c=>c.toUpperCase());
+
+// type toggle
+$('#typeSeg').addEventListener('click', e => {
+  const b = e.target.closest('button'); if(!b) return;
+  [...$('#typeSeg').children].forEach(x=>x.classList.toggle('on', x===b));
+  CUR.type = b.dataset.type;
+  $('#minWrap').style.display = CUR.type==='firms' ? '' : 'none';
+});
+
+async function loadMetros(){
+  try{ const r = await fetch('/api/summit/metros',{credentials:'same-origin'}); const j = await r.json();
+    (j.metros||[]).forEach(m=>{ const o=document.createElement('option'); o.value=m; o.textContent=m; $('#metro').appendChild(o); });
+  }catch(e){}
+}
+function tier(n){ n=n||0; return n>=250?['t-a','Major']:n>=50?['t-b','Mid']:['t-c','Boutique']; }
+
+async function run(){
+  const type = CUR.type;
+  const p = new URLSearchParams({ type, metro: $('#metro').value, commercial: $('#commercial').checked?'1':'0', q: $('#q').value.trim(), limit:'300' });
+  if(type==='firms') p.set('minAgents', $('#minAgents').value||'0');
+  $('#status').innerHTML = '<span class="spin"></span> Querying the registry…';
+  $('#csv').disabled = true;
+  try{
+    const r = await fetch('/api/summit/leads?'+p, {credentials:'same-origin'});
+    const j = await r.json();
+    if(j.error){ $('#status').textContent = 'Registry unavailable ('+j.error+').'; return; }
+    CUR.rows = j.rows||[]; CUR.metro = j.metro;
+    render(j);
+    $('#csv').disabled = CUR.rows.length===0;
+  }catch(e){ $('#status').textContent = 'Request failed.'; }
+}
+
+function render(j){
+  const type = j.type, rows = j.rows||[];
+  $('#status').textContent = rows.length + (rows.length>=300?'+ ':' ') + (type==='firms'?'firms':'brokers') + ' · ' + j.metro + (j.commercial?' · commercial':'');
+  $('#hint').textContent = type==='firms' ? 'Ranked by size (sponsor-budget proxy)' : '';
+  const thead = $('#tbl thead'), tb = $('#tbl tbody');
+  if(!rows.length){ thead.innerHTML=''; tb.innerHTML='<tr><td class="empty">No matches — widen the metro, drop "commercial only", or lower min agents.</td></tr>'; return; }
+  if(type==='firms'){
+    thead.innerHTML = '<tr><th>#</th><th>Firm</th><th>HQ city</th><th>Agents</th><th>Tier</th><th>License</th></tr>';
+    tb.innerHTML = rows.map((r,i)=>{ const [c,l]=tier(r.agent_count); return `<tr><td class="rank">${i+1}</td><td>${esc(cap(r.name))}</td><td>${esc(cap(r.hq_city))}</td><td class="num">${(r.agent_count||0).toLocaleString()}</td><td><span class="tier ${c}">${l}</span></td><td>${esc(r.license_no||'')}</td></tr>`; }).join('');
+  } else {
+    thead.innerHTML = '<tr><th>#</th><th>Broker</th><th>Firm</th><th>City</th><th>Type</th><th>License</th></tr>';
+    tb.innerHTML = rows.map((r,i)=>`<tr><td class="rank">${i+1}</td><td>${esc(cap(r.name))}</td><td>${esc(cap(r.firm_name||''))}</td><td>${esc(cap(r.city))}</td><td>${esc(r.license_type||'')}</td><td>${esc(r.license_no||'')}</td></tr>`).join('');
+  }
+}
+
+function csv(){
+  const rows = CUR.rows; if(!rows.length) return;
+  const cols = CUR.type==='firms'
+    ? [['name','Firm'],['hq_city','HQ City'],['agent_count','Agents'],['license_no','License']]
+    : [['name','Broker'],['firm_name','Firm'],['city','City'],['license_type','Type'],['license_no','License']];
+  const q = s => '"'+String(s==null?'':s).replace(/"/g,'""')+'"';
+  const lines = [cols.map(c=>c[1]).join(',')].concat(rows.map(r=>cols.map(c=>q(r[c[0]])).join(',')));
+  const blob = new Blob([lines.join('\r\n')], {type:'text/csv'});
+  const a = document.createElement('a');
+  a.href = URL.createObjectURL(blob);
+  a.download = 'summit-'+CUR.type+'-'+(CUR.metro||'CA').toLowerCase().replace(/[^a-z]+/g,'-')+'.csv';
+  a.click(); URL.revokeObjectURL(a.href);
+}
+
+$('#run').addEventListener('click', run);
+$('#csv').addEventListener('click', csv);
+$('#q').addEventListener('keydown', e=>{ if(e.key==='Enter') run(); });
+loadMetros();
+</script>
+</body>
+</html>
diff --git a/server.js b/server.js
index a67a7e42..5df594bd 100644
--- a/server.js
+++ b/server.js
@@ -78,7 +78,7 @@ footer:not(#rentv-footer){display:none!important}
   <div class="rf-bar"><span>© 2026 RENTV.com — Commercial Real Estate News across the Western U.S.</span><span>All rights reserved.</span></div>
 </footer>`;
 // Internal/admin shells never get the customer footer.
-const INTERNAL_PAGE = /(^|\/)(admin|versions|consulting|press|desk|desk-admin|audience|social|backend)(\.html)?$|\/(admin|versions|consulting|press)\//i;
+const INTERNAL_PAGE = /(^|\/)(admin|versions|consulting|press|desk|desk-admin|audience|social|backend|summit-leads)(\.html)?$|\/(admin|versions|consulting|press)\//i;
 function sendPage(res, absFile) {
   let html;
   try { html = fs.readFileSync(absFile, 'utf8'); }
@@ -322,6 +322,8 @@ app.get('/versions', adminOnly, (_q, r) => sendPage(r, path.join(PUB, 'versions'
 app.get('/admin', adminOnly, (_q, r) => sendPage(r, path.join(PUB, 'admin', 'index.html')));
 // Broker & Owner Intelligence Desk shell → admin-only (its data APIs are gated below too)
 app.get('/desk', adminOnly, (_q, r) => sendPage(r, path.join(PUB, 'desk.html')));
+// Summit Leads — build invite + sponsor-target lists off the registry (admin-only)
+app.get('/summit-leads', adminOnly, (_q, r) => sendPage(r, path.join(PUB, 'summit-leads.html')));
 // Blog — public index of original RENTV articles + the single-post reader
 app.get('/blog', (_q, r) => sendPage(r, path.join(PUB, 'blog.html')));
 app.get('/post/:id', (_q, r) => sendPage(r, path.join(PUB, 'post.html')));
@@ -833,7 +835,7 @@ require('./src/pr')(app, { adminOnly, sendPage, PUB });
 // The data APIs above are already admin-only; this additionally blocks user-tier
 // logins from loading any INTERNAL static shell/asset directly (desk, audience,
 // admin, versions, consulting, press). Front-page assets pass straight through.
-const INTERNAL_STATIC = /^\/(desk(\.html)?$|desk-assets\/|desk-admin|audience\.html|admin(\/|$|\.html)|versions(\/|$)|consulting(\/|$)|press(\/|$)|social(\.html)?$|la-commercial(\.html)?$|pulse(\.html)?$|sector(\.html)?$|hub(\.html)?$|brief(\.html)?$)/i;
+const INTERNAL_STATIC = /^\/(desk(\.html)?$|desk-assets\/|desk-admin|audience\.html|admin(\/|$|\.html)|versions(\/|$)|consulting(\/|$)|press(\/|$)|social(\.html)?$|summit-leads(\.html)?$|la-commercial(\.html)?$|pulse(\.html)?$|sector(\.html)?$|hub(\.html)?$|brief(\.html)?$)/i;
 app.use((req, res, next) => {
   if (req.role === 'admin') return next();
   if (INTERNAL_STATIC.test(req.path)) return res.status(403).send('Admin only — this area requires an admin login.');
diff --git a/src/pr/index.js b/src/pr/index.js
index 4752e677..6bbff53f 100644
--- a/src/pr/index.js
+++ b/src/pr/index.js
@@ -194,7 +194,7 @@ module.exports = function mountPR(app, { adminOnly, sendPage }) {
   app.post('/api/pr/organizations/:id/evidence', adminOnly, h(async (req, res) =>
     res.json(await organizations.attachEvidence(Number(req.params.id), req.body || {}, actorOf(req)))));
   app.get('/api/pr/organizations/:id/matches', adminOnly, h(async (req, res) => {
-    const org = await db.one('SELECT * FROM pr_organizations WHERE id=$1', [Number(req.params.id)]);
+    const org = await db.one('SELECT t.* FROM pr_organizations t WHERE id=$1', [Number(req.params.id)]);
     if (!org) return res.status(404).json({ error: 'not found' });
     const m = (await organizations.findMatches(org)).filter((x) => x.org.id !== org.id);
     res.json({ matches: m });
@@ -283,7 +283,7 @@ module.exports = function mountPR(app, { adminOnly, sendPage }) {
 
   // ── Query matrix ───────────────────────────────────────────────────────────
   app.get('/api/pr/query-templates', adminOnly, h(async (_q, res) =>
-    res.json({ rows: await db.rows('SELECT * FROM pr_query_templates ORDER BY id', []) })));
+    res.json({ rows: await db.rows('SELECT t.* FROM pr_query_templates t ORDER BY id', []) })));
   app.post('/api/pr/query-templates', adminOnly, h(async (req, res) => {
     const b = req.body || {};
     if (b.id) {
@@ -333,14 +333,14 @@ module.exports = function mountPR(app, { adminOnly, sendPage }) {
   app.post('/api/pr/letters/blocks', adminOnly, h(async (req, res) => res.json(await letters.saveBlock(req.body || {}, actorOf(req)))));
   app.post('/api/pr/letters/preview', adminOnly, h(async (req, res) => {
     const { person_id, organization_id, blocks, subject_template, extra_block_ids } = req.body || {};
-    const person = person_id ? await db.one('SELECT * FROM pr_people WHERE id=$1', [person_id]) : null;
-    const org = organization_id ? await db.one('SELECT * FROM pr_organizations WHERE id=$1', [organization_id])
-      : person && person.organization_id ? await db.one('SELECT * FROM pr_organizations WHERE id=$1', [person.organization_id]) : null;
+    const person = person_id ? await db.one('SELECT t.* FROM pr_people t WHERE id=$1', [person_id]) : null;
+    const org = organization_id ? await db.one('SELECT t.* FROM pr_organizations t WHERE id=$1', [organization_id])
+      : person && person.organization_id ? await db.one('SELECT t.* FROM pr_organizations t WHERE id=$1', [person.organization_id]) : null;
     const tpl = await letters.baseTemplate();
     const useBlocks = blocks || (tpl && tpl.current && tpl.current.blocks) || [];
     const subj = subject_template || (tpl && tpl.current && tpl.current.subject_template) || '';
     const context = await letters.mergeContext({ person, org });
-    const extra = (extra_block_ids || []).length ? await db.rows('SELECT * FROM pr_letter_blocks WHERE id = ANY($1::bigint[])', [extra_block_ids]) : [];
+    const extra = (extra_block_ids || []).length ? await db.rows('SELECT t.* FROM pr_letter_blocks t WHERE id = ANY($1::bigint[])', [extra_block_ids]) : [];
     const vertical = org ? await letters.listBlocks({ scope: 'vertical', org_type: org.organization_type }) : [];
     const orgSpecific = org ? await letters.listBlocks({ scope: 'organization', organization_id: org.id }) : [];
     const orgBlock = [...orgSpecific.filter((b) => b.organization_id === (org && org.id)), ...vertical.filter((b) => b.org_type === (org && org.organization_type)), ...extra].map((b) => b.content).join('\n\n');
diff --git a/src/pr/jobs/index.js b/src/pr/jobs/index.js
index da17598d..6eb7b937 100644
--- a/src/pr/jobs/index.js
+++ b/src/pr/jobs/index.js
@@ -48,7 +48,7 @@ async function enqueue(job_type, payload = {}, { priority = 5, run_id = null, de
 async function claim() {
   return db.tx(async (client) => {
     const r = await client.query(
-      `SELECT * FROM pr_jobs WHERE status='queued' AND run_after <= now()
+      `SELECT t.* FROM pr_jobs t WHERE status='queued' AND run_after <= now()
         ORDER BY priority, id LIMIT 1 FOR UPDATE SKIP LOCKED`);
     if (!r.rows.length) return null;
     const job = r.rows[0];
@@ -91,7 +91,7 @@ async function listJobs({ status, limit = 100 } = {}) {
   const where = []; const params = [];
   if (status) { params.push(status); where.push(`status=$${params.length}::pr_job_status`); }
   params.push(limit);
-  return db.rows(`SELECT * FROM pr_jobs ${where.length ? 'WHERE ' + where.join(' AND ') : ''} ORDER BY id DESC LIMIT $${params.length}`, params);
+  return db.rows(`SELECT t.* FROM pr_jobs t ${where.length ? 'WHERE ' + where.join(' AND ') : ''} ORDER BY id DESC LIMIT $${params.length}`, params);
 }
 
 // ── Shared helpers for handlers ──────────────────────────────────────────────
@@ -157,7 +157,7 @@ const handlers = {
       // 2) Query-matrix pass through the authorized search API.
       const search = adapters.getAdapter('search');
       const tpls = await db.rows(
-        `SELECT * FROM pr_query_templates WHERE enabled=true AND target IN ('organization','press_contact')
+        `SELECT t.* FROM pr_query_templates t WHERE enabled=true AND target IN ('organization','press_contact')
           AND (cardinality(states)=0 OR $1 = ANY(states))
           AND (cardinality(org_types)=0 OR $2::text IS NULL OR $2 = ANY(org_types))
           ORDER BY id`, [state, p.category || null]);
@@ -207,7 +207,7 @@ const handlers = {
   /** verify-organization: website profiling → press pages, press email, internal-PR flag, team pages. */
   async 'verify-organization'(job) {
     const orgId = job.payload.organization_id;
-    const org = await db.one('SELECT * FROM pr_organizations WHERE id=$1', [orgId]);
+    const org = await db.one('SELECT t.* FROM pr_organizations t WHERE id=$1', [orgId]);
     if (!org) return {};
     if (!org.website_url) {
       await organizations.update(orgId, { lifecycle_status: org.lifecycle_status === 'discovered' ? 'needs_review' : org.lifecycle_status }, 'system:verify');
@@ -245,7 +245,7 @@ const handlers = {
   /** discover-people: persist team-page extracted names+titles with per-field evidence. */
   async 'discover-people'(job) {
     const { organization_id, people: found = [], team_pages = [] } = job.payload;
-    const org = await db.one('SELECT * FROM pr_organizations WHERE id=$1', [organization_id]);
+    const org = await db.one('SELECT t.* FROM pr_organizations t WHERE id=$1', [organization_id]);
     if (!org) return {};
     const web = adapters.getAdapter('website');
     let list = found;
@@ -307,7 +307,7 @@ const handlers = {
   async 'discover-pr-agency-relationships'(job) {
     const state = job.payload.state || 'CA';
     const agencies = await db.rows(
-      `SELECT * FROM pr_organizations WHERE organization_type IN ('pr_agency','marketing_agency','public_affairs')
+      `SELECT t.* FROM pr_organizations t WHERE organization_type IN ('pr_agency','marketing_agency','public_affairs')
         AND $1 = ANY(state_presence) AND website_url IS NOT NULL AND lifecycle_status NOT IN ('duplicate','archived')
         ORDER BY id LIMIT 25 OFFSET $2`, [state, (job.checkpoint || {}).offset || 0]);
     const web = adapters.getAdapter('website');
@@ -430,7 +430,7 @@ const handlers = {
   /** detect-duplicates: sweep for uncertain matches and surface them (never auto-merge). */
   async 'detect-duplicates'(job) {
     const offset = (job.checkpoint || {}).offset || 0;
-    const orgs = await db.rows(`SELECT * FROM pr_organizations WHERE lifecycle_status NOT IN ('duplicate','archived') ORDER BY id LIMIT 50 OFFSET $1`, [offset]);
+    const orgs = await db.rows(`SELECT t.* FROM pr_organizations t WHERE lifecycle_status NOT IN ('duplicate','archived') ORDER BY id LIMIT 50 OFFSET $1`, [offset]);
     let flagged = 0;
     for (const o of orgs) {
       const matches = (await organizations.findMatches(o)).filter((m) => m.org.id !== o.id);
diff --git a/src/pr/services/audit.js b/src/pr/services/audit.js
index 973fd16f..347b86ab 100644
--- a/src/pr/services/audit.js
+++ b/src/pr/services/audit.js
@@ -24,18 +24,18 @@ async function activity({ entity_type, entity_id, activity: act, detail, actor }
 
 async function recent({ entity_type, entity_id, limit = 100 }) {
   if (entity_type && entity_id) {
-    return db.rows(`SELECT * FROM pr_activities WHERE entity_type=$1 AND entity_id=$2 ORDER BY created_at DESC LIMIT $3`,
+    return db.rows(`SELECT t.* FROM pr_activities t WHERE entity_type=$1 AND entity_id=$2 ORDER BY created_at DESC LIMIT $3`,
       [entity_type, entity_id, limit]);
   }
-  return db.rows(`SELECT * FROM pr_activities ORDER BY created_at DESC LIMIT $1`, [limit]);
+  return db.rows(`SELECT t.* FROM pr_activities t ORDER BY created_at DESC LIMIT $1`, [limit]);
 }
 
 async function auditTrail({ entity_type, entity_id, limit = 200 }) {
   if (entity_type && entity_id) {
-    return db.rows(`SELECT * FROM pr_audit_log WHERE entity_type=$1 AND entity_id=$2 ORDER BY at DESC LIMIT $3`,
+    return db.rows(`SELECT t.* FROM pr_audit_log t WHERE entity_type=$1 AND entity_id=$2 ORDER BY at DESC LIMIT $3`,
       [entity_type, entity_id, limit]);
   }
-  return db.rows(`SELECT * FROM pr_audit_log ORDER BY at DESC LIMIT $1`, [limit]);
+  return db.rows(`SELECT t.* FROM pr_audit_log t ORDER BY at DESC LIMIT $1`, [limit]);
 }
 
 module.exports = { log, activity, recent, auditTrail };
diff --git a/src/pr/services/campaigns.js b/src/pr/services/campaigns.js
index b54cd48d..f8fccc0b 100644
--- a/src/pr/services/campaigns.js
+++ b/src/pr/services/campaigns.js
@@ -18,7 +18,7 @@ async function update(id, patch, actor) {
   const allowed = ['name', 'state', 'metros', 'organization_types', 'asset_classes', 'status', 'base_template_id'];
   const sets = []; const params = [];
   for (const k of allowed) { if (patch[k] !== undefined) { params.push(patch[k]); sets.push(`${k}=$${params.length}`); } }
-  if (!sets.length) return db.one('SELECT * FROM pr_campaigns WHERE id=$1', [id]);
+  if (!sets.length) return db.one('SELECT t.* FROM pr_campaigns t WHERE id=$1', [id]);
   params.push(id);
   const row = (await db.query(`UPDATE pr_campaigns SET ${sets.join(', ')} WHERE id=$${params.length} RETURNING *`, params)).rows[0];
   await audit.log({ actor, action: 'campaign.update', entity_type: 'campaign', entity_id: id, after: patch });
@@ -35,7 +35,7 @@ async function list() {
 }
 
 async function get(id) {
-  const c = await db.one('SELECT * FROM pr_campaigns WHERE id=$1', [id]);
+  const c = await db.one('SELECT t.* FROM pr_campaigns t WHERE id=$1', [id]);
   if (!c) return null;
   c.members = await db.rows(
     `SELECT m.person_id, m.added_at, p.full_name, p.exact_title, p.email_verification_status,
diff --git a/src/pr/services/importexport.js b/src/pr/services/importexport.js
index ef38c48e..14e39066 100644
--- a/src/pr/services/importexport.js
+++ b/src/pr/services/importexport.js
@@ -157,7 +157,7 @@ async function runImport({ kind, csv, json, columnMap, dry_run = false, filename
 /** Reverse a batch: archive (not delete) everything that batch created. */
 async function reverse(batchId, actor) {
   return db.tx(async (client) => {
-    const b = (await client.query('SELECT * FROM pr_import_batches WHERE id=$1 FOR UPDATE', [batchId])).rows[0];
+    const b = (await client.query('SELECT t.* FROM pr_import_batches t WHERE id=$1 FOR UPDATE', [batchId])).rows[0];
     if (!b) throw new Error('batch not found');
     if (b.reversed_at) throw new Error('batch already reversed');
     const orgs = await client.query(`UPDATE pr_organizations SET lifecycle_status='archived' WHERE import_batch_id=$1 AND lifecycle_status NOT IN ('contacted','active_relationship') RETURNING id`, [batchId]);
@@ -169,7 +169,7 @@ async function reverse(batchId, actor) {
 }
 
 async function listBatches() {
-  return db.rows('SELECT * FROM pr_import_batches ORDER BY created_at DESC LIMIT 100', []);
+  return db.rows('SELECT t.* FROM pr_import_batches t ORDER BY created_at DESC LIMIT 100', []);
 }
 
 // ── Exports ──────────────────────────────────────────────────────────────────
diff --git a/src/pr/services/letters.js b/src/pr/services/letters.js
index 2d6abb5f..dfda681e 100644
--- a/src/pr/services/letters.js
+++ b/src/pr/services/letters.js
@@ -6,14 +6,14 @@ const audit = require('./audit');
 const settings = require('./settings');
 
 async function baseTemplate() {
-  const t = await db.one('SELECT * FROM pr_letter_templates WHERE is_base=true ORDER BY id LIMIT 1', []);
+  const t = await db.one('SELECT t.* FROM pr_letter_templates t WHERE is_base=true ORDER BY id LIMIT 1', []);
   if (!t) return null;
-  t.current = await db.one('SELECT * FROM pr_letter_versions WHERE template_id=$1 ORDER BY version DESC LIMIT 1', [t.id]);
+  t.current = await db.one('SELECT t.* FROM pr_letter_versions t WHERE template_id=$1 ORDER BY version DESC LIMIT 1', [t.id]);
   return t;
 }
 
 async function listTemplates() {
-  const ts = await db.rows('SELECT * FROM pr_letter_templates ORDER BY is_base DESC, id', []);
+  const ts = await db.rows('SELECT t.* FROM pr_letter_templates t ORDER BY is_base DESC, id', []);
   for (const t of ts) {
     t.current = await db.one('SELECT version, created_at, change_note FROM pr_letter_versions WHERE template_id=$1 ORDER BY version DESC LIMIT 1', [t.id]);
     t.version_count = Number((await db.one('SELECT count(*)::int AS n FROM pr_letter_versions WHERE template_id=$1', [t.id])).n);
@@ -25,7 +25,7 @@ async function versions(templateId) {
   return db.rows('SELECT id, version, subject_template, created_by, change_note, created_at FROM pr_letter_versions WHERE template_id=$1 ORDER BY version DESC', [templateId]);
 }
 async function getVersion(versionId) {
-  return db.one('SELECT * FROM pr_letter_versions WHERE id=$1', [versionId]);
+  return db.one('SELECT t.* FROM pr_letter_versions t WHERE id=$1', [versionId]);
 }
 
 /**
@@ -33,7 +33,7 @@ async function getVersion(versionId) {
  * removed unless `unlock` explicitly lists their keys (an audited human action).
  */
 async function saveVersion(templateId, { subject_template, blocks, change_note, unlock = [], actor }) {
-  const prev = await db.one('SELECT * FROM pr_letter_versions WHERE template_id=$1 ORDER BY version DESC LIMIT 1', [templateId]);
+  const prev = await db.one('SELECT t.* FROM pr_letter_versions t WHERE template_id=$1 ORDER BY version DESC LIMIT 1', [templateId]);
   const newBlocks = Array.isArray(blocks) ? blocks : [];
   if (prev) {
     const prevBlocks = Array.isArray(prev.blocks) ? prev.blocks : [];
@@ -78,7 +78,7 @@ async function listBlocks({ scope, org_type, organization_id } = {}) {
   if (scope) { params.push(scope); where.push(`scope=$${params.length}`); }
   if (org_type) { params.push(org_type); where.push(`(org_type=$${params.length} OR org_type IS NULL)`); }
   if (organization_id) { params.push(organization_id); where.push(`(organization_id=$${params.length} OR organization_id IS NULL)`); }
-  return db.rows(`SELECT * FROM pr_letter_blocks WHERE ${where.join(' AND ')} ORDER BY scope, org_type NULLS LAST, id`, params);
+  return db.rows(`SELECT t.* FROM pr_letter_blocks t WHERE ${where.join(' AND ')} ORDER BY scope, org_type NULLS LAST, id`, params);
 }
 async function saveBlock(b, actor) {
   if (b.id) {
diff --git a/src/pr/services/organizations.js b/src/pr/services/organizations.js
index 6a99a7fa..6b50f405 100644
--- a/src/pr/services/organizations.js
+++ b/src/pr/services/organizations.js
@@ -23,7 +23,7 @@ async function findMatches(candidate) {
   }
   if (!ors.length) return [];
   const rows = await db.rows(
-    `SELECT * FROM pr_organizations WHERE (${ors.join(' OR ')}) AND lifecycle_status <> 'duplicate' LIMIT 25`, params);
+    `SELECT t.* FROM pr_organizations t WHERE (${ors.join(' OR ')}) AND lifecycle_status <> 'duplicate' LIMIT 25`, params);
   return rows
     .map((org) => ({ org, ...classifyOrgMatch({ ...candidate, normalized_name: norm, domain }, org) }))
     .filter((m) => m.verdict !== 'distinct')
@@ -103,7 +103,7 @@ async function attachEvidence(orgId, evidence, actor) {
 }
 
 async function get(id) {
-  const org = await db.one('SELECT * FROM pr_organizations WHERE id=$1', [id]);
+  const org = await db.one('SELECT t.* FROM pr_organizations t WHERE id=$1', [id]);
   if (!org) return null;
   org.evidence = await sources.evidenceFor('organization', id);
   org.people = await db.rows(
@@ -166,7 +166,7 @@ async function list(f = {}) {
 }
 
 async function update(id, patch, actor) {
-  const before = await db.one('SELECT * FROM pr_organizations WHERE id=$1', [id]);
+  const before = await db.one('SELECT t.* FROM pr_organizations t WHERE id=$1', [id]);
   if (!before) throw new Error('not found');
   if (patch.lifecycle_status && !canOrgTransition(before.lifecycle_status, patch.lifecycle_status)) {
     throw new Error(`invalid status transition ${before.lifecycle_status} → ${patch.lifecycle_status}`);
@@ -196,8 +196,8 @@ async function update(id, patch, actor) {
 async function merge(winnerId, loserId, actor) {
   if (String(winnerId) === String(loserId)) throw new Error('cannot merge an organization into itself');
   return db.tx(async (client) => {
-    const w = (await client.query('SELECT * FROM pr_organizations WHERE id=$1 FOR UPDATE', [winnerId])).rows[0];
-    const l = (await client.query('SELECT * FROM pr_organizations WHERE id=$1 FOR UPDATE', [loserId])).rows[0];
+    const w = (await client.query('SELECT t.* FROM pr_organizations t WHERE id=$1 FOR UPDATE', [winnerId])).rows[0];
+    const l = (await client.query('SELECT t.* FROM pr_organizations t WHERE id=$1 FOR UPDATE', [loserId])).rows[0];
     if (!w || !l) throw new Error('organization not found');
     await client.query('UPDATE pr_people SET organization_id=$1 WHERE organization_id=$2', [winnerId, loserId]);
     await client.query(`UPDATE pr_field_evidence SET entity_id=$1 WHERE entity_type='organization' AND entity_id=$2`, [winnerId, loserId]);
@@ -226,7 +226,7 @@ async function merge(winnerId, loserId, actor) {
 
 /** Recompute + persist confidence/priority with components. */
 async function refreshScores(id) {
-  const org = await db.one('SELECT * FROM pr_organizations WHERE id=$1', [id]);
+  const org = await db.one('SELECT t.* FROM pr_organizations t WHERE id=$1', [id]);
   if (!org) return null;
   const ev = await sources.evidenceFor('organization', id);
   const conf = orgConfidence(org, ev.map((e) => ({ source_type: e.source_type, is_primary_source: e.is_primary_source, source_id: e.source_id })));
diff --git a/src/pr/services/outreach.js b/src/pr/services/outreach.js
index 086a5134..c67f4f7c 100644
--- a/src/pr/services/outreach.js
+++ b/src/pr/services/outreach.js
@@ -16,10 +16,10 @@ const { SENDABLE_EMAIL_STATUSES } = require('./people');
 
 /** Outreach-readiness check (Stage 7 gates). Returns { ready, reasons[] }. */
 async function readiness(personId) {
-  const p = await db.one('SELECT * FROM pr_people WHERE id=$1', [personId]);
+  const p = await db.one('SELECT t.* FROM pr_people t WHERE id=$1', [personId]);
   if (!p) return { ready: false, reasons: ['person not found'] };
   const reasons = [];
-  const org = p.organization_id ? await db.one('SELECT * FROM pr_organizations WHERE id=$1', [p.organization_id]) : null;
+  const org = p.organization_id ? await db.one('SELECT t.* FROM pr_organizations t WHERE id=$1', [p.organization_id]) : null;
   if (!org) reasons.push('person has no organization');
   if (org) {
     const orgEv = await db.one(`SELECT count(*)::int AS n FROM pr_field_evidence WHERE entity_type='organization' AND entity_id=$1`, [org.id]);
@@ -57,7 +57,7 @@ async function generateDraft({ person_id, campaign_id, extra_block_ids = [], act
   const orgBlocks = org ? await letters.listBlocks({ scope: 'organization', organization_id: org.id }) : [];
   const chosen = [...orgBlocks, ...vertBlocks].filter((b) => org && (b.organization_id === org.id || b.org_type === org.organization_type));
   const extra = extra_block_ids.length
-    ? await db.rows(`SELECT * FROM pr_letter_blocks WHERE id = ANY($1::bigint[])`, [extra_block_ids]) : [];
+    ? await db.rows(`SELECT t.* FROM pr_letter_blocks t WHERE id = ANY($1::bigint[])`, [extra_block_ids]) : [];
   const orgBlockText = [...chosen, ...extra].map((b) => b.content).join('\n\n');
   const context = await letters.mergeContext({ person: check.person, org });
   const rendered = letters.render({
@@ -123,7 +123,7 @@ async function list(f = {}) {
 
 /** Edit a draft's rendered content (before approval). */
 async function updateDraft(id, { subject, rendered_text, rendered_html, follow_up_at, actor }) {
-  const m = await db.one('SELECT * FROM pr_outreach_messages WHERE id=$1', [id]);
+  const m = await db.one('SELECT t.* FROM pr_outreach_messages t WHERE id=$1', [id]);
   if (!m) throw new Error('not found');
   if (!['draft_generated', 'needs_review'].includes(m.status)) throw new Error('only unapproved drafts can be edited');
   const row = (await db.query(
@@ -136,7 +136,7 @@ async function updateDraft(id, { subject, rendered_text, rendered_html, follow_u
 
 async function transition(id, to, actor, detail) {
   return db.tx(async (client) => {
-    const m = (await client.query('SELECT * FROM pr_outreach_messages WHERE id=$1 FOR UPDATE', [id])).rows[0];
+    const m = (await client.query('SELECT t.* FROM pr_outreach_messages t WHERE id=$1 FOR UPDATE', [id])).rows[0];
     if (!m) throw new Error('not found');
     if (!canOutreachTransition(m.status, to)) throw new Error(`invalid outreach transition ${m.status} → ${to}`);
     const stamps = {
@@ -153,7 +153,7 @@ async function transition(id, to, actor, detail) {
 }
 
 async function approve(id, actor) {
-  const m = await db.one('SELECT * FROM pr_outreach_messages WHERE id=$1', [id]);
+  const m = await db.one('SELECT t.* FROM pr_outreach_messages t WHERE id=$1', [id]);
   if (!m) throw new Error('not found');
   const check = await readiness(m.person_id);
   if (!check.ready) throw new Error('not outreach-ready: ' + check.reasons.join('; '));
diff --git a/src/pr/services/people.js b/src/pr/services/people.js
index 2a6b006c..a221ea3a 100644
--- a/src/pr/services/people.js
+++ b/src/pr/services/people.js
@@ -24,7 +24,7 @@ async function findMatches(candidate) {
   if (li) { params.push(li); ors.push(`linkedin_url = $${params.length}`); }
   if (norm) { params.push(norm); ors.push(`normalized_name = $${params.length}`); }
   if (!ors.length) return [];
-  const rows = await db.rows(`SELECT * FROM pr_people WHERE (${ors.join(' OR ')}) AND lifecycle_status <> 'duplicate' LIMIT 25`, params);
+  const rows = await db.rows(`SELECT t.* FROM pr_people t WHERE (${ors.join(' OR ')}) AND lifecycle_status <> 'duplicate' LIMIT 25`, params);
   return rows
     .map((p) => ({ person: p, ...classifyPersonMatch({ ...candidate, normalized_name: norm, public_work_email: email, linkedin_url: li }, p) }))
     .filter((m) => m.verdict !== 'distinct')
@@ -155,7 +155,7 @@ async function list(f = {}) {
  * and title changes append the prior role to role_history.
  */
 async function update(id, patch, actor) {
-  const before = await db.one('SELECT * FROM pr_people WHERE id=$1', [id]);
+  const before = await db.one('SELECT t.* FROM pr_people t WHERE id=$1', [id]);
   if (!before) throw new Error('not found');
   if (patch.lifecycle_status && !canPersonTransition(before.lifecycle_status, patch.lifecycle_status)) {
     throw new Error(`invalid status transition ${before.lifecycle_status} → ${patch.lifecycle_status}`);
@@ -224,8 +224,8 @@ async function update(id, patch, actor) {
 async function merge(winnerId, loserId, actor) {
   if (String(winnerId) === String(loserId)) throw new Error('cannot merge a person into themselves');
   return db.tx(async (client) => {
-    const w = (await client.query('SELECT * FROM pr_people WHERE id=$1 FOR UPDATE', [winnerId])).rows[0];
-    const l = (await client.query('SELECT * FROM pr_people WHERE id=$1 FOR UPDATE', [loserId])).rows[0];
+    const w = (await client.query('SELECT t.* FROM pr_people t WHERE id=$1 FOR UPDATE', [winnerId])).rows[0];
+    const l = (await client.query('SELECT t.* FROM pr_people t WHERE id=$1 FOR UPDATE', [loserId])).rows[0];
     if (!w || !l) throw new Error('person not found');
     await client.query(`UPDATE pr_field_evidence SET entity_id=$1 WHERE entity_type='person' AND entity_id=$2`, [winnerId, loserId]);
     await client.query('UPDATE pr_outreach_messages SET person_id=$1 WHERE person_id=$2', [winnerId, loserId]);
@@ -242,9 +242,9 @@ async function merge(winnerId, loserId, actor) {
 }
 
 async function refreshScores(id) {
-  const p = await db.one('SELECT * FROM pr_people WHERE id=$1', [id]);
+  const p = await db.one('SELECT t.* FROM pr_people t WHERE id=$1', [id]);
   if (!p) return null;
-  const org = p.organization_id ? await db.one('SELECT * FROM pr_organizations WHERE id=$1', [p.organization_id]) : null;
+  const org = p.organization_id ? await db.one('SELECT t.* FROM pr_organizations t WHERE id=$1', [p.organization_id]) : null;
   const ev = await sources.evidenceFor('person', id);
   const evRows = ev.map((e) => ({ source_type: e.source_type, is_primary_source: e.is_primary_source, source_id: e.source_id }));
   const conf = personConfidence(p, evRows);
diff --git a/src/pr/services/relationships.js b/src/pr/services/relationships.js
index 79b4e204..7a79a5b2 100644
--- a/src/pr/services/relationships.js
+++ b/src/pr/services/relationships.js
@@ -53,7 +53,7 @@ async function list({ organization_id, type, limit = 200 }) {
 }
 
 async function remove(id, actor) {
-  const before = await db.one('SELECT * FROM pr_org_relationships WHERE id=$1', [id]);
+  const before = await db.one('SELECT t.* FROM pr_org_relationships t WHERE id=$1', [id]);
   if (!before) return { ok: false, error: 'not found' };
   await db.query('DELETE FROM pr_org_relationships WHERE id=$1', [id]);
   await audit.log({ actor, action: 'relationship.delete', entity_type: 'relationship', entity_id: id, before });
diff --git a/src/pr/services/runs.js b/src/pr/services/runs.js
index 3bacdcd0..b3e2a375 100644
--- a/src/pr/services/runs.js
+++ b/src/pr/services/runs.js
@@ -14,7 +14,7 @@ async function create({ state, metro, category, query, adapter, actor }) {
   return row;
 }
 
-async function get(id) { return db.one('SELECT * FROM pr_research_runs WHERE id=$1', [id]); }
+async function get(id) { return db.one('SELECT t.* FROM pr_research_runs t WHERE id=$1', [id]); }
 
 async function list({ state, status, limit = 100 } = {}) {
   const where = []; const params = [];
@@ -22,7 +22,7 @@ async function list({ state, status, limit = 100 } = {}) {
   if (status) { params.push(status); where.push(`status=$${params.length}::pr_run_status`); }
   params.push(limit);
   return db.rows(
-    `SELECT * FROM pr_research_runs ${where.length ? 'WHERE ' + where.join(' AND ') : ''}
+    `SELECT t.* FROM pr_research_runs t ${where.length ? 'WHERE ' + where.join(' AND ') : ''}
      ORDER BY created_at DESC LIMIT $${params.length}`, params);
 }
 
diff --git a/src/pr/services/sources.js b/src/pr/services/sources.js
index 58c5628a..195c8326 100644
--- a/src/pr/services/sources.js
+++ b/src/pr/services/sources.js
@@ -26,7 +26,7 @@ async function createSource(s, client) {
 /** Reuse an existing source row for the same URL retrieved recently, else create. */
 async function findOrCreateSource(s, client) {
   if (s.url) {
-    const q = `SELECT * FROM pr_sources WHERE url=$1 AND source_type=$2
+    const q = `SELECT t.* FROM pr_sources t WHERE url=$1 AND source_type=$2
                AND retrieved_at > now() - interval '7 days' ORDER BY retrieved_at DESC LIMIT 1`;
     const r = client ? await client.query(q, [s.url, s.source_type]) : await db.query(q, [s.url, s.source_type]);
     if (r.rows[0]) return r.rows[0];
diff --git a/src/pr/services/suppression.js b/src/pr/services/suppression.js
index 042b02fa..d2f64f4a 100644
--- a/src/pr/services/suppression.js
+++ b/src/pr/services/suppression.js
@@ -48,7 +48,7 @@ async function list({ limit = 200, offset = 0 } = {}) {
 }
 
 async function remove(id, actor) {
-  const before = await db.one('SELECT * FROM pr_suppression WHERE id=$1', [id]);
+  const before = await db.one('SELECT t.* FROM pr_suppression t WHERE id=$1', [id]);
   if (!before) return { ok: false, error: 'not found' };
   if (before.permanent && before.reason === 'opt_out') {
     return { ok: false, error: 'permanent opt-outs cannot be removed (CAN-SPAM); add a note instead' };
diff --git a/test/pr/integration.test.js b/test/pr/integration.test.js
index fb3b0718..b4bf54c2 100644
--- a/test/pr/integration.test.js
+++ b/test/pr/integration.test.js
@@ -132,7 +132,7 @@ test('personalized draft: grounded blocks, provider draft WITHOUT sending, expli
 });
 
 test('incoming reply associates by thread; referral creates person + task; opt-out suppresses immediately', async () => {
-  const sentMsg = await db.one(`SELECT * FROM pr_outreach_messages WHERE status='sent' ORDER BY id DESC LIMIT 1`, []);
+  const sentMsg = await db.one(`SELECT t.* FROM pr_outreach_messages t WHERE status='sent' ORDER BY id DESC LIMIT 1`, []);
   assert.ok(sentMsg, 'previous test produced a sent message');
   const inbound = await replies.associateInbound({
     provider: 'mock', provider_message_id: 'in-1', provider_thread_id: sentMsg.provider_thread_id,
@@ -145,7 +145,7 @@ test('incoming reply associates by thread; referral creates person + task; opt-o
   assert.equal(outRow.status, 'replied', 'outbound flipped to replied by thread association');
   const ref = await replies.processReferral({ message_id: sentMsg.id, referred_name: 'Riley Referral', referred_title: 'PR Lead', referred_email: 'riley@draft-org-test.com', actor: 'test' });
   assert.ok(ref.referred_person.id);
-  const task = await db.one(`SELECT * FROM pr_tasks WHERE entity_type='person' AND entity_id=$1`, [ref.referred_person.id]);
+  const task = await db.one(`SELECT t.* FROM pr_tasks t WHERE entity_type='person' AND entity_id=$1`, [ref.referred_person.id]);
   assert.ok(task, 'follow-up task created for the referral');
   // opt-out reply
   const inbound2 = await replies.associateInbound({

← fbe5b696 pr-intelligence: discover-people-linkedin job + adapter (per  ·  back to Rentv 2026  ·  pr-intelligence: full HTML-entity decode in extraction paths 8ed15735 →