feat(scoring): v4 — peer precedent, mission-fit floor, LLM match judge

Fixes the federal mismatch class (boys' camp × NIH research center):

- Peer precedent: federalPrecedent paginates USASpending (≤500 awards/
  program) and name-matches every recipient against primary-ICP NH
  registry orgs (shared normalizeOrgNameForMatching, also used by the
  self-match gate). The 25-pt precedent tiers now key off
  program_state_peer_award_count — Dartmouth renewals and SBIR LLCs no
  longer grant precedent to community nonprofits. Raw count + peer-
  annotated award list stay as review evidence (peer badges, peers-first).
- Mission-fit floor (12/30, grants_gov only): below it a match is stored
  with fit_viable=false and hidden from the pending queue, hero selection,
  and easy-win. Foundation-synthesized grants exempt (generic synopses).
- Mission-fit judge live (judgeMatches, 06:15, 200/night best-first):
  JUDGE_MODEL reads the synopsis against the org profile with an explicit
  ignore-eligibility-breadth instruction; graded verdict with required
  citations; deterministic verdict→points map (27/18/8/0) sets missionFit,
  total, easy-win, and viability. Verdicts survive nightly re-scores via
  an upsert splice and re-enter the judge queue when the org profile is
  re-researched (org_profiles.updated_at).

First sweep: 81/149 programs have NH history, only 6 have peer history;
queue-head judging zeroes the research-mechanism garbage (mismatch) while
surfacing genuine strong fits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Croissant Le Doux
2026-07-17 10:52:50 -04:00
co-authored by Claude Fable 5
parent 7cefbbbfa1
commit cbc4512ffa
37 changed files with 4379 additions and 163 deletions
@@ -50,6 +50,13 @@ const SUBSCORE_MAX: Record<string, number> = {
runway: 5,
};
const JUDGE_VERDICT_STYLE: Record<string, string> = {
strong_fit: 'bg-green-100 text-green-800',
plausible: 'bg-blue-100 text-blue-800',
weak: 'bg-amber-100 text-amber-800',
mismatch: 'bg-red-100 text-red-800',
};
export default function MatchDetail({ loaderData }: Route.ComponentProps) {
const { detail } = loaderData ?? {};
if (detail == null) return null;
@@ -95,6 +102,12 @@ export default function MatchDetail({ loaderData }: Route.ComponentProps) {
{match.easyWin ? ' · Easy win' : ''}
{match.isHero ? ' · Hero' : ''} · Status: {match.reviewStatus}
</p>
{!match.fitViable && (
<p className="mt-1 text-sm font-medium text-red-700">
Hidden from the pending queue — mission fit below floor or
judged non-viable.
</p>
)}
</div>
<div className="flex shrink-0 gap-2">
<Form method="post">
@@ -289,6 +302,26 @@ export default function MatchDetail({ loaderData }: Route.ComponentProps) {
)}
</section>
{match.judgeVerdict != null && (
<section className="rounded border p-4">
<h2 className="mb-2 text-lg font-semibold">
Mission-fit judge{' '}
<span
className={`ml-1 rounded px-2 py-0.5 text-sm font-medium ${JUDGE_VERDICT_STYLE[match.judgeVerdict] ?? ''}`}
>
{match.judgeVerdict.replace('_', ' ')}
</span>
</h2>
<p className="whitespace-pre-wrap text-sm">{match.judgeRationale}</p>
{match.judgedAt != null && (
<p className="mt-2 text-xs text-gray-500">
Judged {new Date(match.judgedAt).toLocaleDateString('en-US')} —
the verdict replaces the embedding mission-fit score above.
</p>
)}
</section>
)}
{applyInfo != null && (
<section className="rounded border p-4">
<h2 className="mb-2 text-lg font-semibold">
@@ -346,7 +379,7 @@ export default function MatchDetail({ loaderData }: Route.ComponentProps) {
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.'}
: 'who this federal program actually funded in NH recently, peers first. Only "peer" recipients (registered NH nonprofits our size) count toward the precedent score — universities, hospital systems, and companies do not.'}
</p>
<table className="w-full text-sm">
<thead>
@@ -361,7 +394,14 @@ export default function MatchDetail({ loaderData }: Route.ComponentProps) {
<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.recipientName}
{g.isPeer === true && (
<span className="ml-1 rounded bg-green-100 px-1 text-xs font-medium text-green-800">
peer
</span>
)}
</td>
<td className="py-1 pr-2">{g.recipientCity ?? '—'}</td>
<td className="py-1 pr-2 text-right tabular-nums">
{dollars(g.amount)}