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:
Croissant Le Doux
2026-07-16 16:55:10 -04:00
parent a532f0bebf
commit 0ee478ec3d
25 changed files with 2213 additions and 4 deletions

View File

@@ -36,6 +36,10 @@ import {
runIngestNhdojOrgsNow,
setIngestNhdojOrgsDeps,
} from './workflows/ingest-nhdoj-orgs.js';
import {
runMatchGrantsNow,
setMatchGrantsDeps,
} from './workflows/match-grants.js';
import {
runIngestPndRssNow,
setIngestPndRssDeps,
@@ -48,6 +52,7 @@ const RUNNERS: Record<string, () => Promise<void>> = {
enrichOrgs: runEnrichOrgsNow,
expireGrants: runExpireGrantsNow,
embedGrants: runEmbedGrantsNow,
matchGrants: runMatchGrantsNow,
};
const FIRST_RUN_ORDER = [
@@ -57,6 +62,7 @@ const FIRST_RUN_ORDER = [
'expireGrants',
'enrichOrgs',
'embedGrants',
'matchGrants',
];
if (process.env.DATABASE_URL == null) {
@@ -92,6 +98,7 @@ async function main() {
setIngestNhdojOrgsDeps({ db });
setEnrichOrgsDeps({ db });
setEmbedGrantsDeps({ db });
setMatchGrantsDeps({ db });
DBOS.setConfig({
name: 'helmdocs-outreach-worker',