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>
This commit is contained in:
Croissant Le Doux
2026-07-17 00:08:12 -04:00
parent 88affbeb4f
commit 4f35dcdb8d
14 changed files with 765 additions and 0 deletions

View File

@@ -63,6 +63,17 @@ export default function MatchDetail({ loaderData }: Route.ComponentProps) {
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;
@@ -143,6 +154,43 @@ export default function MatchDetail({ loaderData }: Route.ComponentProps) {
</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