Commit Graph

7 Commits

Author SHA1 Message Date
Croissant Le Doux
8f9c19a32c fix(scoring): definitive grantmaker exclusion + full-pool matching
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>
2026-07-16 21:16:22 -04:00
Croissant Le Doux
63b58e514d feat(scoring): 990-PF funder-precedent index — the 25-point subscore goes live
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>
2026-07-16 18:17:28 -04:00
Croissant Le Doux
0ee478ec3d 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>
2026-07-16 16:55:10 -04:00
Croissant Le Doux
a532f0bebf feat(scoring): nightly grant-embedding workflow (Stage 2, step 1)
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>
2026-07-16 16:26:41 -04:00
Croissant Le Doux
8d57d57557 fix(ingestion): survive first contact with real data sources
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>
2026-07-16 16:20:47 -04:00
Croissant Le Doux
1735ff6754 feat(ingestion): Phase 1 data spine — Grants.gov, PND RSS, NHDOJ registry, ProPublica enrichment
Core: icpBandForRevenue (100K-5M primary), serverUpsertOrgFromRegistry
(case-insensitive name/city/state key, new-registrant signal),
serverEnrichOrg (IRS fields + re-band, all-null marks attempted),
serverListOrgsNeedingEnrichment.

Worker: four source verticals, each a thin fail-loud client + pure
tested normalize layer + DBOS scheduled workflow:
- ingestGrants (nightly): Search2 paginated (cap 1000) -> fetchOpportunity
  details (cap 200, logged drops, 250ms politeness) -> batched upsert
- ingestPndRss (nightly): RSS via fast-xml-parser, heuristic funder/
  deadline extraction, link-keyed upsert
- ingestNhdojOrgs (monthly): pdfjs-dist positioned-text extraction, pure
  row reconstruction (multi-line names, inferred columns, fail-loud on
  layout change), registry upsert; no-op warn when PDF URL unset
- enrichOrgs (daily): ProPublica search -> conservative name/city match
  (null beats guess) -> latest-filing revenue/NTEE/FYE, per-org steps for
  checkpointed resume, >20% batch failure rethrows

94 worker + 16 core + 5 ai tests green; docs/features/ingestion.md added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 13:11:50 -04:00
Croissant Le Doux
14200edb60 feat: scaffold outreach engine monorepo on the novelpad-desktop stack
Workspaces: config (copied), outreach-core (schema + actions/queries +
hard gates), outreach-ai (Gemini client + embeddings copies, profiler and
mission-fit-judge agent stubs), outreach-worker (DBOS executor with
nightly ingest + hourly expiry workflows), outreach-review (RR7 review
queue v0). Initial drizzle migration incl. pgvector extension.

Stack contract: Yarn 4.5.0 + Turbo, Node 22.16, Drizzle 0.44.6 +
pgvector, DBOS 4.17.6, @google/genai on Vertex, gemini-embedding-001
@1536, React Router v7. Files copied from novelpad-desktop carry
provenance headers @ 62c56b87.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 11:08:24 -04:00