feat(scoring): match-generation workflow — scoring engine v1 (Stage 2, step 2)
matchGrants (nightly 05:15): per candidate org (NH + good standing + primary ICP) ensure a v0 NTEE-derived profile embedding (RETRIEVAL_QUERY, confidence 0.2 stub the Stage 4 profiler upgrades in place), retrieve top-50 open grants by pgvector cosine with SQL-enforced cheap gates (deadline >=21d, ceiling >=10K), run entity/geography gates in TS, score deterministically, upsert pair-keyed matches, reassign hero. Scoring: pure scoreMatch (mission fit 30 / capacity 15 / competition 15 / effort 10 / runway 5; precedent's 25 reserved until the 990-PF index; easy win >= 50/75). Eligibility gate now pattern-matches Grants.gov applicantTypes prose, conservatively (ambiguous entries do not admit). application_form_supported ignored for pass/fail per the manual-first decision, still recorded in rationale. Schema: unique (org_id, grant_id) on matches; unique org_id on org_profiles (latest-profile semantics). Review queue query now ordered hero > easy-win > score and capped at 100. Live run: 64 orgs -> 3,200 matches in 28s, 0 easy wins / max 39 — the honest result of an NIH-heavy 200-grant corpus vs NH service nonprofits; engine mechanics verified, corpus breadth is the next lever. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { desc, eq } from 'drizzle-orm';
|
||||
|
||||
import type { NpOutreachDatabase, NpOutreachTransaction } from '#~/db/db.js';
|
||||
import { schema } from '#~/db/db.js';
|
||||
@@ -37,5 +37,14 @@ export async function serverListPendingReviewMatches(
|
||||
.from(schema.matches)
|
||||
.innerJoin(schema.orgs, eq(schema.matches.orgId, schema.orgs.id))
|
||||
.innerJoin(schema.grants, eq(schema.matches.grantId, schema.grants.id))
|
||||
.where(eq(schema.matches.reviewStatus, 'pending'));
|
||||
.where(eq(schema.matches.reviewStatus, 'pending'))
|
||||
// Reviewers see the best candidates first: heroes, then easy wins,
|
||||
// then raw score. Capped — nightly re-scoring generates thousands of
|
||||
// pending pairs and the queue is worked top-down, not exhaustively.
|
||||
.orderBy(
|
||||
desc(schema.matches.isHero),
|
||||
desc(schema.matches.easyWin),
|
||||
desc(schema.matches.totalScore),
|
||||
)
|
||||
.limit(100);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user