feat(review+990pf): Part XV application info — parse the preselected-only kill signal, surface how-to-apply at review

Parser captures 990-PF Part XV line 2 (OnlyContriToPreselectedInd) even
when the application group is absent. Synthesis skips preselected-only
funders and closes their previously-synthesized grant rows
(serverCloseGrantsForFunders). Match detail page gains a 'How to apply
(funder's own 990-PF, Part XV)' panel — application form, deadlines,
restrictions, red do-not-pitch banner — fed by serverGetMatchDetail.

Backfill re-parse across all 747 NH foundations: 192 are preselected-
only; 62 of 123 synthesized foundation leads were unpitchable by their
funder's own filing and are now closed, with 24,436 stale pending
matches cleaned. Post re-match: 714 easy wins across 327 orgs, every
foundation lead now backed by a funder that actually accepts
applications. 156 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Croissant Le Doux
2026-07-16 21:46:58 -04:00
parent d8ca133e95
commit a92b75b315
7 changed files with 166 additions and 3 deletions

View File

@@ -53,7 +53,15 @@ const SUBSCORE_MAX: Record<string, number> = {
export default function MatchDetail({ loaderData }: Route.ComponentProps) {
const { detail } = loaderData ?? {};
if (detail == null) return null;
const { match, org, grant, funderGivingHistory } = detail;
const { match, org, grant, funderGivingHistory, funderApplicationInfo } =
detail;
const applyInfo = (funderApplicationInfo ?? null) as {
preselectedOnly?: boolean;
recipientName?: string;
formAndInfoAndMaterials?: string;
submissionDeadlines?: string;
restrictionsOnAwards?: string;
} | null;
const subscores = (match.subscores ?? {}) as Record<string, number>;
const rationale = (match.rationale ?? {}) as Record<string, unknown>;
const nhHistory = funderGivingHistory;
@@ -233,6 +241,47 @@ export default function MatchDetail({ loaderData }: Route.ComponentProps) {
)}
</section>
{applyInfo != null && (
<section className="rounded border p-4">
<h2 className="mb-2 text-lg font-semibold">
How to apply (funder's own 990-PF, Part XV)
</h2>
{applyInfo.preselectedOnly === true && (
<p className="mb-2 rounded bg-red-100 p-2 text-sm font-medium text-red-800">
This funder states it only contributes to preselected
organizations and does not accept unsolicited requests — do
not pitch.
</p>
)}
<dl className="space-y-1 text-sm">
{applyInfo.formAndInfoAndMaterials != null && (
<div>
<dt className="inline font-medium">Application form: </dt>
<dd className="inline">{applyInfo.formAndInfoAndMaterials}</dd>
</div>
)}
{applyInfo.submissionDeadlines != null && (
<div>
<dt className="inline font-medium">Deadlines: </dt>
<dd className="inline">{applyInfo.submissionDeadlines}</dd>
</div>
)}
{applyInfo.restrictionsOnAwards != null && (
<div>
<dt className="inline font-medium">Restrictions: </dt>
<dd className="inline">{applyInfo.restrictionsOnAwards}</dd>
</div>
)}
{applyInfo.recipientName != null && (
<div>
<dt className="inline font-medium">Applications to: </dt>
<dd className="inline">{applyInfo.recipientName}</dd>
</div>
)}
</dl>
</section>
)}
<section className="rounded border p-4">
<h2 className="mb-2 text-lg font-semibold">Grant synopsis</h2>
<p className="whitespace-pre-wrap text-sm">{grant.synopsis ?? ''}</p>