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
parent 7cefbbbfa1
commit cbc4512ffa
37 changed files with 4379 additions and 163 deletions

View File

@@ -11,6 +11,10 @@ export interface MatchDetail {
rationale: unknown;
easyWin: boolean;
isHero: boolean;
fitViable: boolean;
judgeVerdict: 'strong_fit' | 'plausible' | 'weak' | 'mismatch' | null;
judgeRationale: string | null;
judgedAt: Date | null;
reviewStatus: 'pending' | 'approved' | 'rejected' | 'edited';
};
org: {
@@ -56,6 +60,9 @@ export interface MatchDetail {
amount: number | null;
purpose: string | null;
taxYear: number;
/** Federal evidence rows only: recipient name-matched a primary-ICP
* NH nonprofit (the peer-precedent basis). Null for 990-PF rows. */
isPeer: boolean | null;
}>;
}
@@ -71,6 +78,10 @@ export async function serverGetMatchDetail(
rationale: schema.matches.rationale,
easyWin: schema.matches.easyWin,
isHero: schema.matches.isHero,
fitViable: schema.matches.fitViable,
judgeVerdict: schema.matches.judgeVerdict,
judgeRationale: schema.matches.judgeRationale,
judgedAt: schema.matches.judgedAt,
reviewStatus: schema.matches.reviewStatus,
orgId: schema.orgs.id,
orgName: schema.orgs.name,
@@ -123,19 +134,29 @@ export async function serverGetMatchDetail(
if (row.grantFunderEin == null && Array.isArray(row.grantProgramAwards)) {
// Federal grants: USASpending program awards fill the same evidence
// table (recipient/amount/year) the 990-PF history uses.
// Peers first (the actual precedent evidence), newest within each
// group; capped so a 500-award program doesn't flood the page.
funderGivingHistory = (
row.grantProgramAwards as Array<{
recipientName?: string;
amount?: number | null;
startDate?: string | null;
isPeer?: boolean;
}>
).map((a) => ({
recipientName: a.recipientName ?? 'Unknown recipient',
recipientCity: null,
amount: a.amount ?? null,
purpose: null,
taxYear: a.startDate != null ? Number(a.startDate.slice(0, 4)) : 0,
}));
)
.map((a) => ({
recipientName: a.recipientName ?? 'Unknown recipient',
recipientCity: null,
amount: a.amount ?? null,
purpose: null,
taxYear: a.startDate != null ? Number(a.startDate.slice(0, 4)) : 0,
isPeer: a.isPeer ?? null,
}))
.sort((a, b) => {
const peerRank = Number(b.isPeer === true) - Number(a.isPeer === true);
return peerRank !== 0 ? peerRank : b.taxYear - a.taxYear;
})
.slice(0, 40);
}
if (row.grantFunderEin != null) {
const funderRow = await db
@@ -170,7 +191,7 @@ export async function serverGetMatchDetail(
return aIn - bIn;
})
.slice(0, 40)
.map(({ recipientState: _s, ...rest }) => rest);
.map(({ recipientState: _s, ...rest }) => ({ ...rest, isPeer: null }));
}
return {
@@ -181,6 +202,10 @@ export async function serverGetMatchDetail(
rationale: row.rationale,
easyWin: row.easyWin,
isHero: row.isHero,
fitViable: row.fitViable,
judgeVerdict: row.judgeVerdict,
judgeRationale: row.judgeRationale,
judgedAt: row.judgedAt,
reviewStatus: row.reviewStatus,
},
org: {