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>
@novelpad/outreach-core
Domain/data package for the HelmDocs grant-match outreach engine: the Drizzle schema for grants, orgs, org profiles, contacts, matches, and pipeline events, plus the actions/queries that read and write them.
Modeled on packages/core in novelpad-desktop,
following the same action/query pattern:
every read lives in a domain's queries/, every write in its actions/,
both barrel-exported, and route handlers/workflows/hooks never touch
db.select()/db.insert() directly.
Domains
grants/— open grant opportunities (Grants.gov, NH state, 990-PF, PND RSS, Candid). Ingestion upserts onsourceUrl; an hourly sweep expires anything past its close date.orgs/— NH nonprofits, plusorg_profiles(LLM/agent-researched enrichment: mission, programs, known funders, budget band, citation sources).matches/— a scored(org, grant)pair.hard-gates.tsis the deterministic, DB-free pass/fail predicate (entity eligibility, geography, ≥21-day runway, ≥$10k ceiling, application-form support) that runs before the weighted LLM subscoring pass — see its module doc for why it's kept pure.pipeline/— outreach lifecycle events (enrolled→sent→opened/replied/... →demo_booked→converted), synced from Apollo and recorded internally.
Deliberate deviations from @novelpad/core
This package is server-only — there is no client app, so several things
@novelpad/core does for its PGlite/OPFS client don't apply here:
- No PGlite, no client schema, no sync. There's exactly one database
handle type (
NpOutreachDatabase/NpOutreachTransactioninsrc/db/db.ts), not the client/server pair (NpClientDatabasevsNpDatabase)@novelpad/coreneeds to support both a local and a remote copy of the same data. - No
novelpad_core_devexport condition.@novelpad/core'spackage.jsonuses a customnovelpad_core_devcondition so dev builds resolve straight tosrc/*.tswithout a build step. This package'sexportsmap only has plaintypes/defaultconditions — runyarn build(oryarn devfor a watch build) before consuming it from a sibling workspace in dev. - No
build:migration-filesbundling step.@novelpad/core'screate:migrationalso runs a script that bundles generated SQL into a TS module the client can import for its OPFS-based local migrator. There is no local migrator here, sobin/create-migrationonly runsdrizzle-kit generateagainst the single server config. - Root
index.tsis intentionally small. It exports only the schema, DB types, and the purehard-gatespredicate — not domain client barrels, since every domain here is server-only. Everything else lives behindindex.server.ts/ the./serverexport condition.
Scripts
yarn build # tsc
yarn dev # tsc --watch
yarn typecheck # tsc --noEmit
yarn test # vitest run
yarn create:migration <name> # drizzle-kit generate against drizzle.config.ts
yarn migrate # drizzle-kit migrate
yarn studio # drizzle-kit studio