Files
grant-outreach-engine/apps/outreach-review
Croissant Le Doux 4f35dcdb8d feat(profiler): Stage 4 grounded research profiler — cited profiles replace NTEE stubs
profileOrgs (nightly 05:45, 25/run, easy-win orgs best-first per the
plan's coarse-match-gates-profiling rule): two-pass Gemini flash —
Google Search-grounded research (groundingMetadata = citation universe)
then JSON-schema extraction citing only from it. Overwrites the stub
profile (mission/programs/geography/funders/staff/news/sources/
confidence); re-embeds mission fit from researched text unless identity
unconfirmed or confidence <0.5 (then the NTEE embedding stays —
unverified research must not steer scoring). Detail page renders
researched programs w/ source links, funders, staff.

Fixes: org_profiles.confidence is float4 — 0.2 stores as 0.20000000298
so 'confidence <= 0.2' excluded every stub (epsilon comparison); RR7
loader serialization turns unknown into never (hoisted casts).

Live: 55 orgs profiled (41 research-grade, 2 low-confidence, 0 failed).
Scoring effect verified both directions: Annie's Angels x NIH mammalian
models fit 19->12; Seacoast Pathways x Foundation for Seacoast Health
fit 25/30 — and the researched knownFunders independently names that
funder. 161 tests green.

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

@novelpad/outreach-review

Human review queue for (org, grant) matches produced by the scoring pipeline in packages/outreach-core. A reviewer sees each pending match — organization, hero grant, total score, easy-win flag — and approves or rejects it. Approved matches are picked up downstream and synced into Apollo sequences.

Auth

There is no authentication in this app, by design. It is a tiny internal tool intended to run behind:

  • Google IAP (Identity-Aware Proxy) in any hosted environment, or
  • localhost only, for local development

Do not add better-auth (or any other auth library) to this workspace. If this app ever needs to leave an IAP-fronted network, that's a deliberate follow-up decision, not something to bolt on ad hoc here.

Development

yarn dev        # react-router dev
yarn build      # react-router build
yarn start      # serve the production build
yarn typecheck  # react-router typegen && tsc --noEmit

Requires DATABASE_URL pointing at the outreach Postgres instance (see packages/outreach-core). Optional: PG_POOL_MAX, PG_CONNECTION_TIMEOUT_MS.

Data contract

This app is a thin consumer of @novelpad/outreach-core. It assumes the following exports (server-side query/action functions live under the ./server subpath, matching the packages/core convention in novelpad-desktop):

// @novelpad/outreach-core
export const schema: /* drizzle schema */;
export type NpOutreachDatabase = /* PgDatabase<..., typeof schema> */;
export interface PendingReviewMatch {
  id: string;
  orgName: string;
  heroGrantTitle: string;
  totalScore: number;
  isEasyWin: boolean;
}
export type MatchReviewDecision = 'approved' | 'rejected';

// @novelpad/outreach-core/server
export function serverListPendingReviewMatches(
  db: NpOutreachDatabase,
): Promise<PendingReviewMatch[]>;
export function serverSetMatchReview(
  db: NpOutreachDatabase,
  matchId: string,
  decision: MatchReviewDecision,
): Promise<void>;

If packages/outreach-core's matches domain (src/matches/queries, src/matches/actions) lands with different names or a different PendingReviewMatch shape, update app/db.server.ts and app/routes/_index.tsx to match rather than reshaping data client-side.