Fixes the federal mismatch class (boys' camp × NIH research center): - Peer precedent: federalPrecedent paginates USASpending (≤500 awards/ program) and name-matches every recipient against primary-ICP NH registry orgs (shared normalizeOrgNameForMatching, also used by the self-match gate). The 25-pt precedent tiers now key off program_state_peer_award_count — Dartmouth renewals and SBIR LLCs no longer grant precedent to community nonprofits. Raw count + peer- annotated award list stay as review evidence (peer badges, peers-first). - Mission-fit floor (12/30, grants_gov only): below it a match is stored with fit_viable=false and hidden from the pending queue, hero selection, and easy-win. Foundation-synthesized grants exempt (generic synopses). - Mission-fit judge live (judgeMatches, 06:15, 200/night best-first): JUDGE_MODEL reads the synopsis against the org profile with an explicit ignore-eligibility-breadth instruction; graded verdict with required citations; deterministic verdict→points map (27/18/8/0) sets missionFit, total, easy-win, and viability. Verdicts survive nightly re-scores via an upsert splice and re-enter the judge queue when the org profile is re-researched (org_profiles.updated_at). First sweep: 81/149 programs have NH history, only 6 have peer history; queue-head judging zeroes the research-mechanism garbage (mismatch) while surfacing genuine strong fits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
163 lines
4.5 KiB
TypeScript
163 lines
4.5 KiB
TypeScript
/**
|
|
* Supervised one-off workflow runner.
|
|
*
|
|
* DATABASE_URL=... node --import tsx/esm src/run-once.ts <workflow> [...]
|
|
*
|
|
* where <workflow> is one or more of: ingestGrants, ingestPndRss,
|
|
* ingestNhdojOrgs, enrichOrgs, expireGrants — or `all` for the standard
|
|
* first-run order (grants → pnd → nhdoj → expire → enrich).
|
|
*
|
|
* Boots exactly like main.ts (same deps injection, same DBOS launch — see
|
|
* that file's boot-order comment), runs the requested workflows through
|
|
* their durable DBOS handles sequentially, then shuts down. Scheduled crons
|
|
* ARE active while this process lives; runs are short enough that this
|
|
* doesn't matter in practice.
|
|
*/
|
|
import { DBOS } from '@dbos-inc/dbos-sdk';
|
|
import { DrizzleDataSource } from '@dbos-inc/drizzle-datasource';
|
|
import { schema } from '@novelpad/outreach-core';
|
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
import pg from 'pg';
|
|
|
|
import {
|
|
runEmbedGrantsNow,
|
|
setEmbedGrantsDeps,
|
|
} from './workflows/embed-grants.js';
|
|
import { runEnrichOrgsNow, setEnrichOrgsDeps } from './workflows/enrich-orgs.js';
|
|
import {
|
|
runExpireGrantsNow,
|
|
setExpireGrantsDeps,
|
|
} from './workflows/expire-grants.js';
|
|
import {
|
|
runFederalPrecedentNow,
|
|
setFederalPrecedentDeps,
|
|
} from './workflows/federal-precedent.js';
|
|
import {
|
|
runIngestGrantsNow,
|
|
setIngestGrantsDeps,
|
|
} from './workflows/ingest-grants.js';
|
|
import {
|
|
runIngest990PfNow,
|
|
setIngest990pfDeps,
|
|
} from './workflows/ingest-990pf.js';
|
|
import {
|
|
runIngestNhdojOrgsNow,
|
|
setIngestNhdojOrgsDeps,
|
|
} from './workflows/ingest-nhdoj-orgs.js';
|
|
import {
|
|
runJudgeMatchesNow,
|
|
setJudgeMatchesDeps,
|
|
} from './workflows/judge-matches.js';
|
|
import {
|
|
runMatchGrantsNow,
|
|
setMatchGrantsDeps,
|
|
} from './workflows/match-grants.js';
|
|
import {
|
|
runProfileOrgsNow,
|
|
setProfileOrgsDeps,
|
|
} from './workflows/profile-orgs.js';
|
|
import {
|
|
runIngestPndRssNow,
|
|
setIngestPndRssDeps,
|
|
} from './workflows/ingest-pnd-rss.js';
|
|
|
|
const RUNNERS: Record<string, () => Promise<void>> = {
|
|
ingestGrants: runIngestGrantsNow,
|
|
ingestPndRss: runIngestPndRssNow,
|
|
ingestNhdojOrgs: runIngestNhdojOrgsNow,
|
|
enrichOrgs: runEnrichOrgsNow,
|
|
expireGrants: runExpireGrantsNow,
|
|
embedGrants: runEmbedGrantsNow,
|
|
matchGrants: runMatchGrantsNow,
|
|
ingest990pf: runIngest990PfNow,
|
|
federalPrecedent: runFederalPrecedentNow,
|
|
profileOrgs: runProfileOrgsNow,
|
|
judgeMatches: runJudgeMatchesNow,
|
|
};
|
|
|
|
const FIRST_RUN_ORDER = [
|
|
'ingestGrants',
|
|
'ingestPndRss',
|
|
'ingestNhdojOrgs',
|
|
'expireGrants',
|
|
'enrichOrgs',
|
|
'ingest990pf',
|
|
'federalPrecedent',
|
|
'embedGrants',
|
|
'matchGrants',
|
|
'profileOrgs',
|
|
'judgeMatches',
|
|
];
|
|
|
|
if (process.env.DATABASE_URL == null) {
|
|
throw new Error('run-once: DATABASE_URL is required');
|
|
}
|
|
|
|
const requested = process.argv.slice(2);
|
|
const names = requested.includes('all') ? FIRST_RUN_ORDER : requested;
|
|
if (names.length === 0 || names.some((n) => RUNNERS[n] == null)) {
|
|
console.error(
|
|
`Usage: run-once.ts <workflow...>\nKnown workflows: ${Object.keys(RUNNERS).join(', ')}, all`,
|
|
);
|
|
process.exit(2);
|
|
}
|
|
|
|
const { Pool } = pg;
|
|
const dbosClientConfig = {
|
|
connectionString: process.env.DATABASE_URL,
|
|
application_name: 'helmdocs-outreach-run-once',
|
|
} satisfies pg.ClientConfig;
|
|
const pool = new Pool({
|
|
...dbosClientConfig,
|
|
max: Number(process.env.PG_POOL_MAX ?? 20),
|
|
});
|
|
const db = drizzle(pool, { schema });
|
|
|
|
async function main() {
|
|
await DrizzleDataSource.initializeDBOSSchema(dbosClientConfig);
|
|
|
|
setIngestGrantsDeps({ db });
|
|
setExpireGrantsDeps({ db });
|
|
setIngestPndRssDeps({ db });
|
|
setIngestNhdojOrgsDeps({ db });
|
|
setEnrichOrgsDeps({ db });
|
|
setEmbedGrantsDeps({ db });
|
|
setMatchGrantsDeps({ db });
|
|
setIngest990pfDeps({ db });
|
|
setFederalPrecedentDeps({ db });
|
|
setProfileOrgsDeps({ db });
|
|
setJudgeMatchesDeps({ db });
|
|
|
|
DBOS.setConfig({
|
|
name: 'helmdocs-outreach-worker',
|
|
systemDatabasePool: pool,
|
|
runAdminServer: false,
|
|
});
|
|
await DBOS.launch();
|
|
|
|
let failed = false;
|
|
for (const name of names) {
|
|
console.log(`\n=== run-once: ${name} ===`);
|
|
const startedAt = Date.now();
|
|
try {
|
|
await RUNNERS[name]!();
|
|
console.log(
|
|
`=== run-once: ${name} OK in ${((Date.now() - startedAt) / 1000).toFixed(1)}s ===`,
|
|
);
|
|
} catch (err) {
|
|
failed = true;
|
|
console.error(`=== run-once: ${name} FAILED ===`, err);
|
|
}
|
|
}
|
|
|
|
// DBOS.shutdown() ends the pool we handed it via systemDatabasePool —
|
|
// a second pool.end() here throws "Called end on pool more than once".
|
|
await DBOS.shutdown();
|
|
process.exit(failed ? 1 : 0);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('[run-once] fatal:', err);
|
|
process.exit(1);
|
|
});
|