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>
213 lines
7.5 KiB
TypeScript
213 lines
7.5 KiB
TypeScript
/**
|
||
* Deterministic weighted subscores for an (org, grant) pair — Stage 2 of
|
||
* the scoring engine (docs/plan.md). Pure functions over plain inputs;
|
||
* the match workflow supplies the embedding similarity, everything else
|
||
* derives from columns.
|
||
*
|
||
* `subscores` records each component so weights can be re-tuned from
|
||
* review/booking data without re-deriving inputs.
|
||
*
|
||
* mission fit 30 embedding cosine similarity, scaled
|
||
* funder precedent 25 historical giving into the org's state (990-PF)
|
||
* capacity fit 15 award ceiling vs org revenue (sweet spot 10–75%)
|
||
* competition 15 state/NH-restricted pools beat national ones
|
||
* effort 10 LOI/short-form beat full federal
|
||
* runway 5 3–10 weeks to deadline is ideal
|
||
*/
|
||
|
||
export interface MatchSubscores {
|
||
readonly missionFit: number;
|
||
readonly capacityFit: number;
|
||
readonly competition: number;
|
||
readonly effort: number;
|
||
readonly runway: number;
|
||
readonly funderPrecedent: number;
|
||
}
|
||
|
||
export interface ScoreMatchInput {
|
||
/** Cosine similarity in [-1, 1] between org mission and grant synopsis. */
|
||
readonly similarity: number;
|
||
readonly orgTotalRevenue: number | null;
|
||
readonly awardCeiling: number | null;
|
||
readonly geographicScope: string | null;
|
||
readonly applicationEffortEstimate:
|
||
| 'loi_only'
|
||
| 'short_form'
|
||
| 'full_federal'
|
||
| 'unknown';
|
||
readonly closeDate: Date | null;
|
||
readonly now: Date;
|
||
/**
|
||
* Historical grants this funder has paid to recipients in the org's
|
||
* state (990-PF index for foundations; USASpending PEER award count for
|
||
* federal programs — recipients name-matched to primary-ICP NH orgs).
|
||
* Null = no precedent data — scores 0, not neutral: the plan weights
|
||
* precedent as the strongest single predictor, and absence of evidence
|
||
* should rank below presence.
|
||
*/
|
||
readonly funderStateGrantCount: number | null;
|
||
/** Grant source (`grants.source`) — drives the source-aware fit floor. */
|
||
readonly grantSource: string;
|
||
}
|
||
|
||
export const ACHIEVABLE_MAX_SCORE = 100;
|
||
/**
|
||
* "Easy win" threshold. With the 990-PF precedent subscore live the scale
|
||
* is the plan's full 0–100; the plan's >=75 easy-win bar applies, plus its
|
||
* precedent floor (see scoreMatch). Thresholds re-tune on review and
|
||
* demo-booking data.
|
||
*/
|
||
export const EASY_WIN_THRESHOLD = 65;
|
||
export const EASY_WIN_MIN_PRECEDENT = 12;
|
||
|
||
/**
|
||
* Mission-fit floor (federal RFPs only): below 12/30 (≈ cosine 0.57) the
|
||
* match is stored but NOT review-viable — non-mission subscores sum to 50,
|
||
* so without a floor a boys' camp scores 60 on an NIH obesity-research
|
||
* center grant purely on precedent + capacity. Foundation-synthesized
|
||
* grants are exempt: their synopses are generic by construction ("grants
|
||
* for NH nonprofits"), so embedding fit carries no signal there and the
|
||
* precedent evidence IS the case for the match.
|
||
*/
|
||
export const MISSION_FIT_VIABLE_MIN = 12;
|
||
const FIT_FLOOR_SOURCES: ReadonlySet<string> = new Set(['grants_gov']);
|
||
|
||
/** Similarity below this scores 0 fit; above the ceiling scores full fit. */
|
||
const SIMILARITY_FLOOR = 0.45;
|
||
const SIMILARITY_CEILING = 0.75;
|
||
|
||
export function missionFitSubscore(similarity: number): number {
|
||
const clamped = Math.min(
|
||
Math.max(similarity, SIMILARITY_FLOOR),
|
||
SIMILARITY_CEILING,
|
||
);
|
||
return Math.round(
|
||
((clamped - SIMILARITY_FLOOR) / (SIMILARITY_CEILING - SIMILARITY_FLOOR)) * 30,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Sweet spot: award is 10–75% of annual revenue (docs/plan.md). A grant
|
||
* dwarfing the org's budget is a capacity red flag to federal funders; a
|
||
* tiny one isn't worth the email. Unknown revenue scores a neutral 7.
|
||
*/
|
||
export function capacityFitSubscore(
|
||
awardCeiling: number | null,
|
||
orgTotalRevenue: number | null,
|
||
): number {
|
||
if (awardCeiling == null || orgTotalRevenue == null || orgTotalRevenue <= 0) {
|
||
return awardCeiling == null ? 0 : 7;
|
||
}
|
||
const ratio = awardCeiling / orgTotalRevenue;
|
||
if (ratio >= 0.1 && ratio <= 0.75) return 15;
|
||
if (ratio >= 0.05 && ratio < 0.1) return 10;
|
||
if (ratio > 0.75 && ratio <= 1.5) return 8;
|
||
if (ratio < 0.05) return 4;
|
||
return 2; // > 150% of revenue: real capacity red flag.
|
||
}
|
||
|
||
const STATE_RESTRICTED_PATTERN =
|
||
/new hampshire|\bnh\b|state of|statewide|county|municipal/i;
|
||
const REGIONAL_PATTERN = /new england|northeast|regional/i;
|
||
|
||
/**
|
||
* Competition proxy until expected-applicant-pool modeling exists:
|
||
* geographically restricted pools are dramatically less competitive than
|
||
* national ones. Null scope (typical for federal) = national = low score.
|
||
*/
|
||
export function competitionSubscore(geographicScope: string | null): number {
|
||
if (geographicScope == null || geographicScope.trim() === '') return 3;
|
||
if (STATE_RESTRICTED_PATTERN.test(geographicScope)) return 15;
|
||
if (REGIONAL_PATTERN.test(geographicScope)) return 10;
|
||
return 3;
|
||
}
|
||
|
||
export function effortSubscore(
|
||
estimate: ScoreMatchInput['applicationEffortEstimate'],
|
||
): number {
|
||
switch (estimate) {
|
||
case 'loi_only':
|
||
return 10;
|
||
case 'short_form':
|
||
return 8;
|
||
case 'unknown':
|
||
return 4;
|
||
case 'full_federal':
|
||
return 2;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Funder precedent (25): "a foundation that gave to three NH orgs like
|
||
* this one is a near-certain match for a fourth" — the plan's strongest
|
||
* single predictor. v1 measures repeated giving into the org's state;
|
||
* NTEE-level matching arrives when recipient orgs get resolved to EINs.
|
||
*/
|
||
export function funderPrecedentSubscore(
|
||
funderStateGrantCount: number | null,
|
||
): number {
|
||
if (funderStateGrantCount == null || funderStateGrantCount <= 0) return 0;
|
||
if (funderStateGrantCount >= 10) return 25;
|
||
if (funderStateGrantCount >= 5) return 20;
|
||
if (funderStateGrantCount >= 3) return 15;
|
||
return 8;
|
||
}
|
||
|
||
const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000;
|
||
|
||
/** 3–10 weeks out is ideal: urgent enough to act on, long enough to apply. */
|
||
export function runwaySubscore(closeDate: Date | null, now: Date): number {
|
||
if (closeDate == null) return 2; // rolling/unknown deadline: usable, not urgent.
|
||
const weeks = (closeDate.getTime() - now.getTime()) / MS_PER_WEEK;
|
||
if (weeks < 3) return 0;
|
||
if (weeks <= 10) return 5;
|
||
if (weeks <= 20) return 3;
|
||
return 1;
|
||
}
|
||
|
||
export interface ScoredMatch {
|
||
readonly totalScore: number;
|
||
readonly subscores: MatchSubscores;
|
||
readonly easyWin: boolean;
|
||
/**
|
||
* False when mission fit is below the source-aware floor — the match is
|
||
* recorded (audit trail, re-scoring continuity) but hidden from the
|
||
* pending review queue. The LLM match judge may later override in
|
||
* either direction.
|
||
*/
|
||
readonly fitViable: boolean;
|
||
}
|
||
|
||
export function scoreMatch(input: ScoreMatchInput): ScoredMatch {
|
||
const subscores: MatchSubscores = {
|
||
missionFit: missionFitSubscore(input.similarity),
|
||
capacityFit: capacityFitSubscore(input.awardCeiling, input.orgTotalRevenue),
|
||
competition: competitionSubscore(input.geographicScope),
|
||
effort: effortSubscore(input.applicationEffortEstimate),
|
||
runway: runwaySubscore(input.closeDate, input.now),
|
||
funderPrecedent: funderPrecedentSubscore(input.funderStateGrantCount),
|
||
};
|
||
|
||
const totalScore =
|
||
subscores.missionFit +
|
||
subscores.capacityFit +
|
||
subscores.competition +
|
||
subscores.effort +
|
||
subscores.runway +
|
||
subscores.funderPrecedent;
|
||
|
||
const fitViable =
|
||
!FIT_FLOOR_SOURCES.has(input.grantSource) ||
|
||
subscores.missionFit >= MISSION_FIT_VIABLE_MIN;
|
||
|
||
return {
|
||
totalScore,
|
||
subscores,
|
||
easyWin:
|
||
fitViable &&
|
||
totalScore >= EASY_WIN_THRESHOLD &&
|
||
subscores.funderPrecedent >= EASY_WIN_MIN_PRECEDENT,
|
||
fitViable,
|
||
};
|
||
}
|