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:
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user