Candidate orgs now also exclude any org whose EIN appears in the funders
table — an org that files a 990-PF IS a private foundation regardless of
its NTEE code (Bean/McIninch-style grantmakers carry E/S codes or none).
1,692 stale pending matches for PF-filer orgs cleaned. Candidate limit
raised to 2,000 after the enrichment drain grew the pool 56 -> 887.
Full-pool run: 856 candidates, 42,800 matches, 850 easy wins across 373
orgs, top heroes led by community-precedent pairs (Seacoast Pathways ->
Foundation for Seacoast Health; Granite Backcountry Alliance ->
Gibson-Woodbury). Known residual: hospital-legacy 'health foundation'
grantmakers file 990 (not 990-PF), so the PF roster can't catch them —
they reach the queue and rely on the human reject gate; profiler-based
classification is the durable fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New funders/funder_grants schema + ingest990pf monthly workflow: IRS BMF
state file discovers NH private foundations (747), e-file index CSVs
select their latest 990-PF filings, batch ZIPs stream through fflate
(4/run cap, most-hits-first, deferred logged), grants-paid rows land in
funder_grants, and funders with >=2 NH grants synthesize rolling grant
rows (source irs_990pf, funder_ein linked) that flow through the
existing embed+match pipeline.
Scoring v2: funderPrecedentSubscore tiers repeated in-state giving
(1/3/5/10 -> 8/15/20/25); easy win = >=65 total AND >=12 precedent
(plan's precedent floor); scale is the full 0-100. Rolling deadlines
pass the runway gate. Retrieval computes per-funder in-state counts and
exposes funder_ein.
Lead-quality gates from the first precedent run's failures: candidate
orgs exclude NTEE T* grantmakers; self-matches gated by EIN + normalized
name (NHDOJ registers foundations as charities, several without resolved
EINs — the first run's top 'leads' were foundations matched to
themselves).
Live: ~6.5GB of IRS batches processed, 2,766 grants-paid rows, 123
synthesized foundation grants, 89 easy wins across 27 orgs, credible
top-10 (AIDS Response-Seacoast -> Foundation for Seacoast Health, 25/25
precedent). 153 tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
embedGrants (cron 04:15, after the ingest crons): open grants with a
synopsis and no vector → buildGrantEmbeddingText (pure, tested:
title+funder+program areas+synopsis, 8K cap) → gemini-embedding-001 @
1536 dims RETRIEVAL_DOCUMENT (org profiles will embed as
RETRIEVAL_QUERY on the other side) → grants.synopsis_embedding.
Chunked embed→store (100/chunk) so failures resume from the last
stored chunk; 500/run spend cap.
Core: serverListGrantsNeedingEmbedding (open+unembedded, closest
deadline first), serverSetGrantEmbeddings. Worker gains
@novelpad/outreach-ai dep; wired into main.ts and run-once (incl. the
missed run-once deps injection).
Live-verified: 199/199 open grants embedded in 16s; semantic probe
('after-school STEM education for youth') ranks NCI Youth Enjoy
Science R25 first at 0.639 cosine via the hnsw index.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NHDOJ: parser rebuilt for the real 8-column registry layout (Reg. No. |
Charity Name | Address | City | State | Zip | Status | Report Due) with
single-letter G/X/S statuses; Reg. No. is the stable upsert key (new
orgs.registration_number column + partial unique index, enum gains
'suspended' via idempotent ADD VALUE); out-of-state registrants keep
their real state. Akamai-safe fetch headers + NHDOJ_REGISTRY_PDF_PATH
local-file override.
ProPublica: zero-hit state-scoped searches return 404, not an empty
list — map to no-candidates instead of failure (tripped the systemic-
failure breaker at 60/200 on first contact). Enrichment queue now
prioritizes NH good-standing orgs over the out-of-state tail.
PND: feed retired upstream (HTML shell on every historical path) —
documented as rework candidate, low priority.
run-once.ts: supervised one-off runner through the durable DBOS
handles (workflow modules now export run*Now accessors); drop the
double pool.end() after DBOS.shutdown().
First supervised run: 200 Grants.gov opportunities (1 auto-expired),
13,632 orgs from the 427-page registry, enrichment at failed=0 with
121/200 EIN resolution in the NH-priority batch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>