Files
grant-outreach-engine/apps/outreach-review/app/routes/matches.$matchId.tsx
Croissant Le Doux 4f35dcdb8d feat(profiler): Stage 4 grounded research profiler — cited profiles replace NTEE stubs
profileOrgs (nightly 05:45, 25/run, easy-win orgs best-first per the
plan's coarse-match-gates-profiling rule): two-pass Gemini flash —
Google Search-grounded research (groundingMetadata = citation universe)
then JSON-schema extraction citing only from it. Overwrites the stub
profile (mission/programs/geography/funders/staff/news/sources/
confidence); re-embeds mission fit from researched text unless identity
unconfirmed or confidence <0.5 (then the NTEE embedding stays —
unverified research must not steer scoring). Detail page renders
researched programs w/ source links, funders, staff.

Fixes: org_profiles.confidence is float4 — 0.2 stores as 0.20000000298
so 'confidence <= 0.2' excluded every stub (epsilon comparison); RR7
loader serialization turns unknown into never (hoisted casts).

Live: 55 orgs profiled (41 research-grade, 2 low-confidence, 0 failed).
Scoring effect verified both directions: Annie's Angels x NIH mammalian
models fit 19->12; Seacoast Pathways x Foundation for Seacoast Health
fit 25/30 — and the researched knownFunders independently names that
funder. 161 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 00:08:12 -04:00

380 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { nteeDescription } from '@novelpad/outreach-core';
import {
serverGetMatchDetail,
serverSetMatchReview,
} from '@novelpad/outreach-core/server';
import { Form, Link, redirect } from 'react-router';
import { db } from '#~/db.server.js';
import type { Route } from './+types/matches.$matchId.js';
export async function loader({ request, params }: Route.LoaderArgs) {
if (request.method === 'HEAD') return;
const detail = await serverGetMatchDetail(db, params.matchId);
if (detail == null) {
throw new Response('Match not found', { status: 404 });
}
return { detail };
}
export async function action({ request, params }: Route.ActionArgs) {
if (request.method === 'HEAD') return;
const formData = await request.formData();
const decision = formData.get('decision');
if (decision !== 'approved' && decision !== 'rejected') {
throw new Response('Invalid review submission', { status: 400 });
}
await serverSetMatchReview(db, {
matchId: params.matchId,
reviewStatus: decision,
rejectReason: decision === 'rejected' ? 'other' : undefined,
});
return redirect('/');
}
function dollars(n: number | null): string {
if (n == null) return '—';
return `$${n.toLocaleString('en-US')}`;
}
const SUBSCORE_MAX: Record<string, number> = {
missionFit: 30,
funderPrecedent: 25,
capacityFit: 15,
competition: 15,
effort: 10,
runway: 5,
};
export default function MatchDetail({ loaderData }: Route.ComponentProps) {
const { detail } = loaderData ?? {};
if (detail == null) return null;
const { match, org, grant, funderGivingHistory, funderApplicationInfo } =
detail;
const applyInfo = (funderApplicationInfo ?? null) as {
preselectedOnly?: boolean;
recipientName?: string;
formAndInfoAndMaterials?: string;
submissionDeadlines?: string;
restrictionsOnAwards?: string;
} | null;
const subscores = (match.subscores ?? {}) as Record<string, number>;
const orgPrograms = (org.programs ?? null) as Array<{
name: string;
description?: string | null;
populationServed?: string | null;
sourceUrl?: string | null;
}> | null;
const orgFunders = (org.knownFunders ?? null) as Array<{ name: string }> | null;
const orgStaff = (org.staff ?? null) as Array<{
name: string;
role?: string | null;
}> | null;
const rationale = (match.rationale ?? {}) as Record<string, unknown>;
const nhHistory = funderGivingHistory;
return (
<main className="container mx-auto max-w-4xl space-y-6 p-6">
<nav>
<Link to="/" className="text-blue-700 underline">
Back to queue
</Link>
</nav>
<header className="flex items-start justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold">
{org.name} {grant.funder}
</h1>
<p className="text-gray-600">
Score {match.totalScore}/100
{match.easyWin ? ' · Easy win' : ''}
{match.isHero ? ' · Hero' : ''} · Status: {match.reviewStatus}
</p>
</div>
<div className="flex shrink-0 gap-2">
<Form method="post">
<input type="hidden" name="decision" value="approved" />
<button
type="submit"
className="rounded bg-green-600 px-4 py-2 text-white hover:bg-green-700"
>
Approve
</button>
</Form>
<Form method="post">
<input type="hidden" name="decision" value="rejected" />
<button
type="submit"
className="rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700"
>
Reject
</button>
</Form>
</div>
</header>
<section className="grid gap-6 md:grid-cols-2">
<div className="rounded border p-4">
<h2 className="mb-2 text-lg font-semibold">Organization</h2>
<dl className="space-y-1 text-sm">
<div>
<dt className="inline font-medium">Location: </dt>
<dd className="inline">
{[org.city, org.state].filter(Boolean).join(', ')}
</dd>
</div>
<div>
<dt className="inline font-medium">Focus (NTEE): </dt>
<dd className="inline">
{org.nteeCode ?? '—'}
{nteeDescription(org.nteeCode) != null &&
`${nteeDescription(org.nteeCode)}`}
</dd>
</div>
<div>
<dt className="inline font-medium">Annual revenue: </dt>
<dd className="inline">{dollars(org.totalRevenue)}</dd>
</div>
<div>
<dt className="inline font-medium">Mission (profile): </dt>
<dd className="inline">
{org.missionStatement ?? '—'}
{org.profileConfidence != null &&
org.profileConfidence <= 0.2 && (
<span className="text-amber-700">
{' '}
(auto-generated stub verify on their website)
</span>
)}
</dd>
</div>
</dl>
{orgPrograms != null && orgPrograms.length > 0 && (
<div className="mt-3 text-sm">
<h3 className="font-medium">Programs (researched)</h3>
<ul className="ml-4 list-disc">
{orgPrograms.map(
(p, i) => (
<li key={i}>
{p.name}
{p.populationServed != null && ` — serving ${p.populationServed}`}
{p.sourceUrl != null && (
<>
{' '}
<a className="text-blue-700 underline" href={p.sourceUrl} target="_blank" rel="noreferrer">
src
</a>
</>
)}
</li>
),
)}
</ul>
</div>
)}
{orgFunders != null && orgFunders.length > 0 && (
<p className="mt-2 text-sm">
<span className="font-medium">Known funders: </span>
{orgFunders.map((f) => f.name).join(', ')}
</p>
)}
{orgStaff != null && orgStaff.length > 0 && (
<p className="mt-2 text-sm">
<span className="font-medium">Staff: </span>
{orgStaff
.map((s) => (s.role != null ? `${s.name} (${s.role})` : s.name))
.join(', ')}
</p>
)}
<div className="mt-3 space-x-3 text-sm">
{org.ein != null && (
<a
className="text-blue-700 underline"
href={`https://projects.propublica.org/nonprofits/organizations/${org.ein.replace(/^0+/, '')}`}
target="_blank"
rel="noreferrer"
>
ProPublica filings
</a>
)}
<a
className="text-blue-700 underline"
href={`https://www.google.com/search?q=${encodeURIComponent(`"${org.name}" ${org.city ?? ''} NH nonprofit`)}`}
target="_blank"
rel="noreferrer"
>
Search their site
</a>
</div>
</div>
<div className="rounded border p-4">
<h2 className="mb-2 text-lg font-semibold">Grant</h2>
<dl className="space-y-1 text-sm">
<div>
<dt className="inline font-medium">Title: </dt>
<dd className="inline">{grant.title}</dd>
</div>
<div>
<dt className="inline font-medium">Award: </dt>
<dd className="inline">
{dollars(grant.awardFloor)} {dollars(grant.awardCeiling)}
</dd>
</div>
<div>
<dt className="inline font-medium">Closes: </dt>
<dd className="inline">
{grant.closeDate == null
? 'Rolling / no stated deadline'
: new Date(grant.closeDate).toLocaleDateString('en-US')}
</dd>
</div>
<div>
<dt className="inline font-medium">Source: </dt>
<dd className="inline">{grant.source}</dd>
</div>
{grant.eligibilityEntityTypes != null &&
grant.eligibilityEntityTypes.length > 0 && (
<div>
<dt className="inline font-medium">Eligibility: </dt>
<dd className="inline">
{grant.eligibilityEntityTypes.join('; ')}
</dd>
</div>
)}
</dl>
<div className="mt-3 text-sm">
<a
className="text-blue-700 underline"
href={grant.sourceUrl}
target="_blank"
rel="noreferrer"
>
{grant.source === 'irs_990pf'
? 'Funder on ProPublica ↗'
: 'Grant listing ↗'}
</a>
</div>
</div>
</section>
<section className="rounded border p-4">
<h2 className="mb-2 text-lg font-semibold">Score breakdown</h2>
<table className="w-full max-w-md text-sm">
<tbody>
{Object.entries(SUBSCORE_MAX).map(([key, max]) => (
<tr key={key} className="border-b">
<td className="py-1 capitalize">
{key.replace(/([A-Z])/g, ' $1').toLowerCase()}
</td>
<td className="py-1 text-right tabular-nums">
{subscores[key] ?? 0} / {max}
</td>
</tr>
))}
</tbody>
</table>
{typeof rationale.similarity === 'number' && (
<p className="mt-2 text-sm text-gray-600">
Embedding similarity: {(rationale.similarity as number).toFixed(3)}
{Array.isArray(rationale.gateFailures) &&
(rationale.gateFailures as string[]).length > 0 &&
` · Ignored gate flags: ${(rationale.gateFailures as string[]).join(', ')}`}
</p>
)}
</section>
{applyInfo != null && (
<section className="rounded border p-4">
<h2 className="mb-2 text-lg font-semibold">
How to apply (funder's own 990-PF, Part XV)
</h2>
{applyInfo.preselectedOnly === true && (
<p className="mb-2 rounded bg-red-100 p-2 text-sm font-medium text-red-800">
This funder states it only contributes to preselected
organizations and does not accept unsolicited requests — do
not pitch.
</p>
)}
<dl className="space-y-1 text-sm">
{applyInfo.formAndInfoAndMaterials != null && (
<div>
<dt className="inline font-medium">Application form: </dt>
<dd className="inline">{applyInfo.formAndInfoAndMaterials}</dd>
</div>
)}
{applyInfo.submissionDeadlines != null && (
<div>
<dt className="inline font-medium">Deadlines: </dt>
<dd className="inline">{applyInfo.submissionDeadlines}</dd>
</div>
)}
{applyInfo.restrictionsOnAwards != null && (
<div>
<dt className="inline font-medium">Restrictions: </dt>
<dd className="inline">{applyInfo.restrictionsOnAwards}</dd>
</div>
)}
{applyInfo.recipientName != null && (
<div>
<dt className="inline font-medium">Applications to: </dt>
<dd className="inline">{applyInfo.recipientName}</dd>
</div>
)}
</dl>
</section>
)}
<section className="rounded border p-4">
<h2 className="mb-2 text-lg font-semibold">Grant synopsis</h2>
<p className="whitespace-pre-wrap text-sm">{grant.synopsis ?? ''}</p>
</section>
{nhHistory.length > 0 && (
<section className="rounded border p-4">
<h2 className="mb-1 text-lg font-semibold">
{grant.source === 'irs_990pf'
? 'Funder giving history (from 990-PF filings)'
: 'Recent program awards to NH recipients (USASpending)'}
</h2>
<p className="mb-3 text-sm text-gray-600">
The concrete evidence behind the precedent score —{' '}
{grant.source === 'irs_990pf'
? 'who this funder actually paid, sorted in-state first.'
: 'who this federal program actually funded in NH recently. Watch for one incumbent recapturing renewals vs. genuine spread across orgs.'}
</p>
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left">
<th className="py-1 pr-2">Recipient</th>
<th className="py-1 pr-2">City</th>
<th className="py-1 pr-2 text-right">Amount</th>
<th className="py-1 pr-2">Year</th>
<th className="py-1">Purpose</th>
</tr>
</thead>
<tbody>
{nhHistory.map((g, i) => (
<tr key={i} className="border-b align-top">
<td className="py-1 pr-2">{g.recipientName}</td>
<td className="py-1 pr-2">{g.recipientCity ?? ''}</td>
<td className="py-1 pr-2 text-right tabular-nums">
{dollars(g.amount)}
</td>
<td className="py-1 pr-2">{g.taxYear}</td>
<td className="py-1">{g.purpose ?? ''}</td>
</tr>
))}
</tbody>
</table>
</section>
)}
</main>
);
}