← back to Rentv 2026
docs/pr-intelligence-gate-and-discard-brief.md
89 lines
# PR-Intelligence build brief — CA gate + outreach discard
**Ticket:** TK-10297 · **Owner:** RENTV build (vp-research-content / re-props) · **Status:** proposed (code + Kamatera deploy = Steve-gated)
Origin: 2026-08-06 session on `rentv.agentabrams.com/admin/pr-intelligence`. Two build gaps
surfaced that together (a) let the California quality gate actually pass and (b) clean up
un-removable outreach drafts. Neither is fixable from data/ops — both need code.
---
## Change 1 — count a verified `general_press_email` as a contact (unblocks CA gate)
**Problem.** The CA `contact_coverage` gate sits at 64% (147/228) vs the ≥70% threshold and
**cannot be closed by discovery** — a full re-run (convergence reset, 2026-08-06) found 0 new
contacts and spent $0, because automated people-discovery is exhausted for these orgs. The real
cause is definitional: an org is only "covered" if it has a `pr_people` row in a comms role.
Orgs like **Lockton** (`media@lockton.com`, `people_count=3`) are fully reachable for a
media-list-add campaign yet score as "no PR contact" because no *person* wears a comms role.
**Fix.** In `src/pr/services/settings.js` (the `contact_coverage` computation, ~lines 77–80),
extend the `with_contact` FILTER so an org **also** counts as covered when it has a verified
`general_press_email` (and, symmetrically, in the `no_pr_contact` filter in
`src/pr/services/organizations.js:164`). Current logic:
```sql
count(*) FILTER (WHERE EXISTS (
SELECT 1 FROM pr_people p WHERE p.organization_id = hi.id
AND p.public_work_email IS NOT NULL
AND p.department IN ('communications','marketing','leadership','bd','agency'))) AS with_contact
```
Add an OR arm:
```sql
count(*) FILTER (WHERE
hi.general_press_email IS NOT NULL -- verified org-level press channel counts
OR EXISTS ( ... existing pr_people subquery ... )) AS with_contact
```
**Guardrails.**
- Only count a press email that is **verified** (not an inferred/guessed address) — mirror the
`no_mislabeled_inferred` gate. If verification status isn't tracked at org level, gate on the
presence of field-evidence for `general_press_email`.
- Keep `comms_only` as a separate, stricter sub-metric so the "named human" coverage stays visible.
- Re-run the gate check after deploy; expect CA `contact_coverage` to clear 70% (a large share of
the 234 channel-having CA orgs expose a press email).
**Acceptance:** CA gate `contact_coverage.pass = true` and `passed = true` without any fabricated
contacts; `comms_only` still reported; AZ gate unaffected.
---
## Change 2 — add `DELETE /api/pr/outreach/:id` + a Discard button (cleans draft clutter)
**Problem.** The outreach layer is append-only: `POST /api/pr/outreach/draft` always **inserts a
new row** (no upsert), and there is **no delete/void** route. Result: duplicate drafts accumulate
with no way to remove them — e.g. Tim Jemal / NAIOP SoCal has 2 identical drafts (outreach #3, #6),
and a settings-verification regen on 2026-08-06 created draft #7 (a dup of Jennifer Vasquez #1) that
can't be removed. `drafts_awaiting_review` is inflated by these ghosts.
**Fix.** In `src/pr/index.js` (outreach routes ~lines 527–544, next to the existing
`POST /:id/status`), add:
```js
app.delete('/api/pr/outreach/:id', adminOnly, h(async (req, res) => { /* soft-delete or hard-delete a draft */ }));
```
- Only allow delete for non-sent drafts (`status IN ('draft_generated','provider_draft_created','approved')`);
never delete a row with `sent_at IS NOT NULL` (audit trail).
- Prefer a **soft delete** (`status='discarded'` + `discarded_at`) so nothing sent is ever lost, and
exclude `discarded` from the review list + the `drafts_awaiting_review` count.
- Add a **Discard** button to the review UI (`public/admin/pr-intelligence/` review page) wired to
the new route via `PR.api('/outreach/'+id, {method:'DELETE'})` (the csrf.js shim already attaches
`X-CSRF-Token`).
**Bonus (optional):** make `POST /outreach/draft` upsert-per-person (one live draft per person_id)
so re-generation refreshes instead of duplicating — this prevents the dup class at the source.
**Acceptance:** a draft can be discarded from the review UI; discarded drafts drop out of
`drafts_awaiting_review`; sent messages can never be deleted; Tim Jemal collapses to one live draft.
---
## Deploy
RENTV app is Kamatera-hosted (pm2 `rentv`, served at rentv.agentabrams.com). Standard flow:
migration (if a `discarded_at` column is added) → `/deploy` → gate re-check. **Live deploy is
Steve-gated.**