@cliwant/mcp-sam-gov 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +20 -12
- package/README.ko.md +20 -12
- package/README.md +62 -14
- package/dist/bea.d.ts +1 -1
- package/dist/bea.js +1 -1
- package/dist/cbp-border.d.ts +51 -0
- package/dist/cbp-border.d.ts.map +1 -0
- package/dist/cbp-border.js +123 -0
- package/dist/cbp-border.js.map +1 -0
- package/dist/census-economic.d.ts +1 -1
- package/dist/census-economic.d.ts.map +1 -1
- package/dist/census-economic.js +12 -6
- package/dist/census-economic.js.map +1 -1
- package/dist/cms-facility.d.ts +112 -0
- package/dist/cms-facility.d.ts.map +1 -0
- package/dist/cms-facility.js +311 -0
- package/dist/cms-facility.js.map +1 -0
- package/dist/cms-hospital.d.ts +105 -0
- package/dist/cms-hospital.d.ts.map +1 -0
- package/dist/cms-hospital.js +290 -0
- package/dist/cms-hospital.js.map +1 -0
- package/dist/cms-supplier.d.ts +133 -0
- package/dist/cms-supplier.d.ts.map +1 -0
- package/dist/cms-supplier.js +414 -0
- package/dist/cms-supplier.js.map +1 -0
- package/dist/cms-utilization.d.ts +113 -0
- package/dist/cms-utilization.d.ts.map +1 -0
- package/dist/cms-utilization.js +328 -0
- package/dist/cms-utilization.js.map +1 -0
- package/dist/courtlistener.d.ts +115 -0
- package/dist/courtlistener.d.ts.map +1 -0
- package/dist/courtlistener.js +398 -0
- package/dist/courtlistener.js.map +1 -0
- package/dist/cpsc.d.ts +81 -0
- package/dist/cpsc.d.ts.map +1 -0
- package/dist/cpsc.js +283 -0
- package/dist/cpsc.js.map +1 -0
- package/dist/datagov-catalog.d.ts.map +1 -1
- package/dist/datagov-catalog.js +16 -2
- package/dist/datagov-catalog.js.map +1 -1
- package/dist/dol.d.ts +2 -2
- package/dist/dol.js +5 -5
- package/dist/dol.js.map +1 -1
- package/dist/ecfr.d.ts +2 -2
- package/dist/ecfr.d.ts.map +1 -1
- package/dist/ecfr.js +24 -10
- package/dist/ecfr.js.map +1 -1
- package/dist/edgar.d.ts.map +1 -1
- package/dist/edgar.js +26 -6
- package/dist/edgar.js.map +1 -1
- package/dist/epa-envirofacts.d.ts +97 -0
- package/dist/epa-envirofacts.d.ts.map +1 -0
- package/dist/epa-envirofacts.js +305 -0
- package/dist/epa-envirofacts.js.map +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +11 -0
- package/dist/errors.js.map +1 -1
- package/dist/far.d.ts.map +1 -1
- package/dist/far.js +3 -1
- package/dist/far.js.map +1 -1
- package/dist/federal-register.d.ts +2 -2
- package/dist/federal-register.d.ts.map +1 -1
- package/dist/federal-register.js +26 -10
- package/dist/federal-register.js.map +1 -1
- package/dist/fema.d.ts +36 -0
- package/dist/fema.d.ts.map +1 -1
- package/dist/fema.js +124 -0
- package/dist/fema.js.map +1 -1
- package/dist/fred.d.ts +1 -1
- package/dist/fred.js +1 -1
- package/dist/gov-domains.d.ts +66 -0
- package/dist/gov-domains.d.ts.map +1 -0
- package/dist/gov-domains.js +211 -0
- package/dist/gov-domains.js.map +1 -0
- package/dist/keys.d.ts +6 -5
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +25 -6
- package/dist/keys.js.map +1 -1
- package/dist/nhtsa.d.ts +91 -0
- package/dist/nhtsa.d.ts.map +1 -0
- package/dist/nhtsa.js +263 -0
- package/dist/nhtsa.js.map +1 -0
- package/dist/nist-controls.d.ts +48 -0
- package/dist/nist-controls.d.ts.map +1 -0
- package/dist/nist-controls.js +174 -0
- package/dist/nist-controls.js.map +1 -0
- package/dist/nonprofit.d.ts +116 -0
- package/dist/nonprofit.d.ts.map +1 -0
- package/dist/nonprofit.js +342 -0
- package/dist/nonprofit.js.map +1 -0
- package/dist/nws-weather.d.ts +57 -0
- package/dist/nws-weather.d.ts.map +1 -0
- package/dist/nws-weather.js +131 -0
- package/dist/nws-weather.js.map +1 -0
- package/dist/openfda-device.d.ts +85 -0
- package/dist/openfda-device.d.ts.map +1 -0
- package/dist/openfda-device.js +277 -0
- package/dist/openfda-device.js.map +1 -0
- package/dist/openfda-drugsfda.d.ts +72 -0
- package/dist/openfda-drugsfda.d.ts.map +1 -0
- package/dist/openfda-drugsfda.js +230 -0
- package/dist/openfda-drugsfda.js.map +1 -0
- package/dist/openfda.d.ts +133 -0
- package/dist/openfda.d.ts.map +1 -0
- package/dist/openfda.js +425 -0
- package/dist/openfda.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +996 -16
- package/dist/server.js.map +1 -1
- package/dist/treasury.d.ts +2 -0
- package/dist/treasury.d.ts.map +1 -1
- package/dist/treasury.js +7 -0
- package/dist/treasury.js.map +1 -1
- package/dist/usaspending.d.ts +32 -1
- package/dist/usaspending.d.ts.map +1 -1
- package/dist/usaspending.js +143 -16
- package/dist/usaspending.js.map +1 -1
- package/package.json +3 -2
- package/src/bea.ts +1 -1
- package/src/cbp-border.ts +177 -0
- package/src/census-economic.ts +12 -6
- package/src/cms-facility.ts +379 -0
- package/src/cms-hospital.ts +344 -0
- package/src/cms-supplier.ts +527 -0
- package/src/cms-utilization.ts +389 -0
- package/src/courtlistener.ts +465 -0
- package/src/cpsc.ts +333 -0
- package/src/datagov-catalog.ts +18 -2
- package/src/dol.ts +5 -5
- package/src/ecfr.ts +27 -10
- package/src/edgar.ts +39 -7
- package/src/epa-envirofacts.ts +358 -0
- package/src/errors.ts +11 -0
- package/src/far.ts +3 -1
- package/src/federal-register.ts +29 -10
- package/src/fema.ts +139 -0
- package/src/fred.ts +1 -1
- package/src/gov-domains.ts +237 -0
- package/src/keys.ts +27 -6
- package/src/nhtsa.ts +352 -0
- package/src/nist-controls.ts +219 -0
- package/src/nonprofit.ts +460 -0
- package/src/nws-weather.ts +167 -0
- package/src/openfda-device.ts +356 -0
- package/src/openfda-drugsfda.ts +313 -0
- package/src/openfda.ts +518 -0
- package/src/server.ts +1127 -27
- package/src/treasury.ts +7 -0
- package/src/usaspending.ts +189 -17
package/src/treasury.ts
CHANGED
|
@@ -83,6 +83,13 @@ export const TREASURY_DATASETS = {
|
|
|
83
83
|
mts_table_1: "/v1/accounting/mts/mts_table_1",
|
|
84
84
|
rates_of_exchange: "/v1/accounting/od/rates_of_exchange",
|
|
85
85
|
debt_outstanding: "/v2/accounting/od/debt_outstanding",
|
|
86
|
+
// interest_expense = ACTUAL interest PAID on the debt (debt-service cost by
|
|
87
|
+
// security type), distinct from avg_interest_rates (rates only). LIVE-VERIFIED
|
|
88
|
+
// 2026-07-16 (total-count 7245, truthful pagination). tror = the Treasury Report
|
|
89
|
+
// on Receivables — federal receivables + delinquent-debt collections BY AGENCY
|
|
90
|
+
// (debt-collection contracting / agency financial-management signal; total 3953).
|
|
91
|
+
interest_expense: "/v2/accounting/od/interest_expense",
|
|
92
|
+
tror: "/v2/debt/tror",
|
|
86
93
|
} as const;
|
|
87
94
|
|
|
88
95
|
export type TreasuryDatasetKey = keyof typeof TREASURY_DATASETS;
|
package/src/usaspending.ts
CHANGED
|
@@ -39,6 +39,31 @@ const USAS = "https://api.usaspending.gov/api/v2";
|
|
|
39
39
|
|
|
40
40
|
export type UsasFilters = Record<string, unknown>;
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Build the awarding-agency filter for spending_by_category / spending_by_award —
|
|
44
|
+
* ALWAYS a canonical NAME match. A purely-numeric value is a toptier CODE (e.g.
|
|
45
|
+
* "036"), which the sibling code-based tools (usas_get_agency_profile /
|
|
46
|
+
* usas_get_agency_budget_function) take but which silently matches NOTHING here —
|
|
47
|
+
* returning a confidently-wrong empty (e.g. "VA had $0 subagency spending" when VA
|
|
48
|
+
* actually has $66.87B). Fail loud instead of faking a zero (dogfooding 2026-07-16;
|
|
49
|
+
* same confidently-wrong-empty class as the subaward drift). No real agency NAME is
|
|
50
|
+
* purely numeric, so the /^\d+$/ test never rejects a legitimate name.
|
|
51
|
+
*/
|
|
52
|
+
function awardingAgencyFilter(
|
|
53
|
+
agency: string,
|
|
54
|
+
): { type: string; tier: string; name: string }[] {
|
|
55
|
+
if (/^\d+$/.test(agency.trim())) {
|
|
56
|
+
throw new ToolErrorCarrier({
|
|
57
|
+
kind: "invalid_input",
|
|
58
|
+
retryable: false,
|
|
59
|
+
message: `agency ${JSON.stringify(
|
|
60
|
+
agency,
|
|
61
|
+
)} looks like a toptier CODE, but this filter matches the canonical agency NAME (e.g. "Department of Veterans Affairs") — a code silently matches nothing here and would return a false empty. Resolve the name via usas_lookup_agency or usas_list_toptier_agencies, or use a code-based tool (usas_get_agency_profile / usas_get_agency_budget_function) that takes a toptierCode.`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return [{ type: "awarding", tier: "toptier", name: agency }];
|
|
65
|
+
}
|
|
66
|
+
|
|
42
67
|
function buildFilters(args: {
|
|
43
68
|
agency?: string;
|
|
44
69
|
naics?: string;
|
|
@@ -48,9 +73,7 @@ function buildFilters(args: {
|
|
|
48
73
|
}): UsasFilters {
|
|
49
74
|
const filters: UsasFilters = { award_type_codes: ["A", "B", "C", "D"] };
|
|
50
75
|
if (args.agency) {
|
|
51
|
-
filters.agencies =
|
|
52
|
-
{ type: "awarding", tier: "toptier", name: args.agency },
|
|
53
|
-
];
|
|
76
|
+
filters.agencies = awardingAgencyFilter(args.agency);
|
|
54
77
|
}
|
|
55
78
|
if (args.naics) filters.naics_codes = [args.naics];
|
|
56
79
|
if (args.fiscalYear) {
|
|
@@ -180,6 +203,20 @@ function categoryAggregateMeta(opts: {
|
|
|
180
203
|
`Capped at the top ${opts.limit} categories by amount; more categories may exist. This endpoint reports no grand total, so the true number of categories is unknown (totalAvailable is null, NOT the returned count). These extra categories are NOT page-reachable — all six callers post page:1 with NO offset/page input (nextOffset is null). Raise limit (up to 50) or narrow filters to see the rest.`,
|
|
181
204
|
);
|
|
182
205
|
}
|
|
206
|
+
// Confidently-wrong-empty guard (dogfooding 2026-07-16): a SET agency filter that
|
|
207
|
+
// yields ZERO rows is ambiguous — a genuine zero OR a NAME that didn't match the
|
|
208
|
+
// exact canonical toptier name (an abbreviation / mis-case / partial / typo
|
|
209
|
+
// silently matches nothing on this NAME filter, so "$0" reads as authoritative
|
|
210
|
+
// when the name simply missed). Disclose the ambiguity. (A numeric CODE is already
|
|
211
|
+
// loud-failed by awardingAgencyFilter; this catches the wrong-NAME residual.)
|
|
212
|
+
const agencyFilterSet =
|
|
213
|
+
Array.isArray((opts.filters as { agencies?: unknown[] } | undefined)?.agencies) &&
|
|
214
|
+
(((opts.filters as { agencies?: unknown[] }).agencies?.length ?? 0) > 0);
|
|
215
|
+
if (agencyFilterSet && opts.returned === 0) {
|
|
216
|
+
notes.push(
|
|
217
|
+
`The agency filter matched NO records. This filter requires the EXACT canonical toptier agency NAME (e.g. "Department of Veterans Affairs"): an abbreviation, mis-cased, partial, or mistyped name silently matches nothing here. Verify the exact name via usas_list_toptier_agencies or usas_lookup_agency. If the name is correct, this is a genuine zero for this slice.`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
183
220
|
if (opts.extraNotes) notes.push(...opts.extraNotes);
|
|
184
221
|
return {
|
|
185
222
|
source: opts.source,
|
|
@@ -672,20 +709,24 @@ export async function searchAwardsByRecipient(args: {
|
|
|
672
709
|
// ─── Subaward enumeration ─────────────────────────────────────────
|
|
673
710
|
|
|
674
711
|
export async function searchSubawards(args: {
|
|
675
|
-
|
|
712
|
+
subRecipientName?: string;
|
|
676
713
|
agency?: string;
|
|
677
714
|
naics?: string;
|
|
678
715
|
fiscalYear?: number;
|
|
679
716
|
limit?: number;
|
|
680
717
|
}) {
|
|
681
718
|
const filters = buildFilters(args);
|
|
682
|
-
|
|
683
|
-
|
|
719
|
+
// recipient_search_text on a subawards spending_by_award search matches the
|
|
720
|
+
// SUBAWARDEE name (live-verified 2026-07-16), so this param is subRecipientName —
|
|
721
|
+
// NOT a prime filter (the old `primeRecipientName` name was inverted).
|
|
722
|
+
if (args.subRecipientName) {
|
|
723
|
+
filters.recipient_search_text = [args.subRecipientName];
|
|
684
724
|
}
|
|
685
725
|
type Resp = {
|
|
686
726
|
results?: {
|
|
687
727
|
"Sub-Award ID"?: string;
|
|
688
728
|
"Sub-Award Recipient"?: string;
|
|
729
|
+
"Sub-Awardee Name"?: string;
|
|
689
730
|
"Sub-Award Amount"?: number;
|
|
690
731
|
"Sub-Award Date"?: string;
|
|
691
732
|
NAICS?: { code?: string; description?: string };
|
|
@@ -705,6 +746,13 @@ export async function searchSubawards(args: {
|
|
|
705
746
|
filters,
|
|
706
747
|
fields: [
|
|
707
748
|
"Sub-Award ID",
|
|
749
|
+
// DRIFT FIX (dogfooding 2026-07-15): USAspending's spending_by_award now
|
|
750
|
+
// returns the subawardee under "Sub-Awardee Name"; the legacy
|
|
751
|
+
// "Sub-Award Recipient" field is still echoed but is ALWAYS null. Request
|
|
752
|
+
// BOTH and prefer the live one (the endpoint echoes an unknown field as
|
|
753
|
+
// null, so requesting both is safe). Without this, every subRecipient was
|
|
754
|
+
// silently null — the supply-chain/teaming lane could not name any sub.
|
|
755
|
+
"Sub-Awardee Name",
|
|
708
756
|
"Sub-Award Recipient",
|
|
709
757
|
"Sub-Award Amount",
|
|
710
758
|
"Sub-Award Date",
|
|
@@ -720,13 +768,13 @@ export async function searchSubawards(args: {
|
|
|
720
768
|
const data = {
|
|
721
769
|
subawards: results.map((r) => ({
|
|
722
770
|
subAwardId: r["Sub-Award ID"] ?? "",
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
//
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
|
|
729
|
-
|
|
771
|
+
// The subawardee name — read from the CURRENT field "Sub-Awardee Name",
|
|
772
|
+
// falling back to the legacy "Sub-Award Recipient" (now always null upstream).
|
|
773
|
+
// A genuinely absent name in BOTH ⇒ null (honest absence, never a fabricated
|
|
774
|
+
// "(name redacted)" — consistent with the null-never-fabricate discipline
|
|
775
|
+
// `amount` uses below).
|
|
776
|
+
subRecipient:
|
|
777
|
+
r["Sub-Awardee Name"] ?? r["Sub-Award Recipient"] ?? null,
|
|
730
778
|
// F2 (P3 null-never-0): an ABSENT Sub-Award Amount → null, NEVER a
|
|
731
779
|
// fabricated $0. A genuine 0 still survives (`??` fires only on null).
|
|
732
780
|
amount: r["Sub-Award Amount"] ?? null,
|
|
@@ -1364,7 +1412,7 @@ export async function searchRecompetes(args: {
|
|
|
1364
1412
|
const filtersApplied: string[] = ["awardType(contracts A/B/C/D)"];
|
|
1365
1413
|
const filtersDropped: string[] = [];
|
|
1366
1414
|
if (args.agency) {
|
|
1367
|
-
filters.agencies =
|
|
1415
|
+
filters.agencies = awardingAgencyFilter(args.agency);
|
|
1368
1416
|
filtersApplied.push("agency");
|
|
1369
1417
|
}
|
|
1370
1418
|
if (args.naics) {
|
|
@@ -1730,6 +1778,20 @@ export async function spendingOverTime(args: {
|
|
|
1730
1778
|
`Completeness for group='${group}': this endpoint returns no pagination envelope, and no-truncation is verified only for fiscal_year granularity — a very long ${group} series could in principle be capped server-side without a signal. Confirm the span (${spanStart ?? "?"} … ${spanEnd ?? "?"}) covers your expected range.`,
|
|
1731
1779
|
);
|
|
1732
1780
|
}
|
|
1781
|
+
// Confidently-wrong-empty guard (dogfooding 2026-07-16): a SET agency filter that
|
|
1782
|
+
// yields an ALL-ZERO timeline is ambiguous — a genuine zero OR a NAME that missed
|
|
1783
|
+
// the exact canonical toptier name (an abbreviation / mis-case / typo silently
|
|
1784
|
+
// matches nothing, producing a false "$0 every period" that reads as authoritative
|
|
1785
|
+
// "this agency never spent on X"). Disclose it. (A numeric CODE is already
|
|
1786
|
+
// loud-failed by awardingAgencyFilter; this catches the wrong-NAME residual.)
|
|
1787
|
+
if (
|
|
1788
|
+
args.agency &&
|
|
1789
|
+
(timeline.length === 0 || timeline.every((t) => t.total === 0))
|
|
1790
|
+
) {
|
|
1791
|
+
notes.push(
|
|
1792
|
+
`The agency filter produced an ALL-ZERO timeline. This filter requires the EXACT canonical toptier agency NAME (e.g. "Department of Veterans Affairs"): an abbreviation, mis-cased, partial, or mistyped name silently matches nothing, yielding a false "$0 in every period". Verify the exact name via usas_list_toptier_agencies or usas_lookup_agency. If the name is correct, the agency genuinely had no CONTRACT obligations in this span.`,
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1733
1795
|
|
|
1734
1796
|
return withMeta(
|
|
1735
1797
|
{ group: json.group ?? group, timeline },
|
|
@@ -1842,9 +1904,7 @@ export async function searchCfdaSpending(args: {
|
|
|
1842
1904
|
award_type_codes: ["02", "03", "04", "05"], // grants
|
|
1843
1905
|
};
|
|
1844
1906
|
if (args.agency) {
|
|
1845
|
-
filters.agencies =
|
|
1846
|
-
{ type: "awarding", tier: "toptier", name: args.agency },
|
|
1847
|
-
];
|
|
1907
|
+
filters.agencies = awardingAgencyFilter(args.agency);
|
|
1848
1908
|
}
|
|
1849
1909
|
if (args.fiscalYear) {
|
|
1850
1910
|
filters.time_period = [
|
|
@@ -2625,6 +2685,118 @@ export async function glossary(args: { limit?: number; search?: string }) {
|
|
|
2625
2685
|
});
|
|
2626
2686
|
}
|
|
2627
2687
|
|
|
2688
|
+
// ─── Disaster / emergency-fund spending (DEFC axis) ───────────────
|
|
2689
|
+
/**
|
|
2690
|
+
* List the Disaster Emergency Fund Codes (DEFC) — the supplemental-appropriation
|
|
2691
|
+
* tags (COVID-19 relief, IIJA/infrastructure, and other emergency laws) that
|
|
2692
|
+
* usas_disaster_spending filters on. GET references/def_codes/ (keyless). Returns the
|
|
2693
|
+
* COMPLETE code set (no pagination), each with `group` ('covid_19' | 'infrastructure'
|
|
2694
|
+
* | null), title, and public law. Discovery front-door for usas_disaster_spending.
|
|
2695
|
+
*/
|
|
2696
|
+
export async function listDisasterCodes() {
|
|
2697
|
+
return memoize("usas:def_codes", async () => {
|
|
2698
|
+
type Resp = {
|
|
2699
|
+
codes?: {
|
|
2700
|
+
code?: string;
|
|
2701
|
+
disaster?: string | null;
|
|
2702
|
+
title?: string;
|
|
2703
|
+
public_law?: string;
|
|
2704
|
+
}[];
|
|
2705
|
+
};
|
|
2706
|
+
const json = await getUsas<Resp>("references/def_codes/");
|
|
2707
|
+
const results = json.codes ?? [];
|
|
2708
|
+
const codes = results.map((c) => ({
|
|
2709
|
+
code: c.code ?? "",
|
|
2710
|
+
// The supplemental-law GROUP: 'covid_19' (COVID relief), 'infrastructure'
|
|
2711
|
+
// (IIJA), or null (other emergency appropriations). NOT fabricated when absent.
|
|
2712
|
+
group: c.disaster ?? null,
|
|
2713
|
+
title: c.title ?? "",
|
|
2714
|
+
publicLaw: typeof c.public_law === "string" ? c.public_law : null,
|
|
2715
|
+
}));
|
|
2716
|
+
return withMeta(
|
|
2717
|
+
{ codes },
|
|
2718
|
+
referenceMeta({
|
|
2719
|
+
source: "usaspending.gov/api/v2 references/def_codes",
|
|
2720
|
+
returned: codes.length,
|
|
2721
|
+
limit: codes.length,
|
|
2722
|
+
totalAvailable: codes.length,
|
|
2723
|
+
// The endpoint takes no `limit` and returns the COMPLETE DEFC set → complete.
|
|
2724
|
+
limitHonored: false,
|
|
2725
|
+
extraNotes: [
|
|
2726
|
+
"Complete list of Disaster Emergency Fund Codes (DEFC). group 'covid_19' = COVID-19 relief appropriations; 'infrastructure' = IIJA (Infrastructure Investment and Jobs Act); null = other supplemental/emergency appropriations. Pass one or more `code` values to usas_disaster_spending's `defCodes` to see spending tagged to those funds.",
|
|
2727
|
+
],
|
|
2728
|
+
}),
|
|
2729
|
+
);
|
|
2730
|
+
});
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
/**
|
|
2734
|
+
* Disaster / emergency-fund spending BY GEOGRAPHY — obligations or outlays tagged to
|
|
2735
|
+
* one or more Disaster Emergency Fund Codes (DEFC: COVID-19, IIJA, etc.), broken out
|
|
2736
|
+
* per state / county / congressional district. POST disaster/spending_by_geography/
|
|
2737
|
+
* (keyless). Answers "which geographies captured COVID/IIJA relief money" — a
|
|
2738
|
+
* distinct axis the standard award search does not expose. `defCodes` REQUIRED
|
|
2739
|
+
* (discover them via usas_list_disaster_codes). The geography endpoint returns the
|
|
2740
|
+
* COMPLETE set of geo units (no pagination) ⇒ totalAvailable = returned, complete.
|
|
2741
|
+
* amount/perCapita are number|null (a real 0 stays 0 — some DEFCs report $0
|
|
2742
|
+
* obligations with a nonzero awardCount; absent → null, never a fabricated 0).
|
|
2743
|
+
*/
|
|
2744
|
+
export async function disasterSpending(args: {
|
|
2745
|
+
defCodes: string[];
|
|
2746
|
+
spendingType?: "obligation" | "outlay";
|
|
2747
|
+
geoLayer?: "state" | "county" | "district";
|
|
2748
|
+
}): Promise<MetaBundle> {
|
|
2749
|
+
const spendingType = args.spendingType ?? "obligation";
|
|
2750
|
+
const geoLayer = args.geoLayer ?? "state";
|
|
2751
|
+
type Resp = {
|
|
2752
|
+
geo_layer?: string;
|
|
2753
|
+
results?: {
|
|
2754
|
+
amount?: number;
|
|
2755
|
+
display_name?: string;
|
|
2756
|
+
shape_code?: string;
|
|
2757
|
+
population?: number | null;
|
|
2758
|
+
per_capita?: number;
|
|
2759
|
+
award_count?: number;
|
|
2760
|
+
}[];
|
|
2761
|
+
};
|
|
2762
|
+
const json = await postUsas<Resp>("disaster/spending_by_geography/", {
|
|
2763
|
+
filter: { def_codes: args.defCodes },
|
|
2764
|
+
spending_type: spendingType,
|
|
2765
|
+
geo_layer: geoLayer,
|
|
2766
|
+
});
|
|
2767
|
+
const results = json.results ?? [];
|
|
2768
|
+
// null-never-0: a real 0 (a DEFC that obligated nothing in that geo) stays 0; an
|
|
2769
|
+
// absent/non-number amount is null (never a fabricated 0).
|
|
2770
|
+
const nn = (v: unknown): number | null => (typeof v === "number" ? v : null);
|
|
2771
|
+
const geographies = results.map((r) => ({
|
|
2772
|
+
name: r.display_name ?? "",
|
|
2773
|
+
code: r.shape_code ?? null,
|
|
2774
|
+
amount: nn(r.amount),
|
|
2775
|
+
awardCount: nn(r.award_count),
|
|
2776
|
+
population: nn(r.population),
|
|
2777
|
+
perCapita: nn(r.per_capita),
|
|
2778
|
+
}));
|
|
2779
|
+
return withMeta(
|
|
2780
|
+
{ spendingType, geoLayer, defCodes: args.defCodes, geographies },
|
|
2781
|
+
{
|
|
2782
|
+
source: "usaspending.gov/api/v2 disaster/spending_by_geography",
|
|
2783
|
+
keylessMode: true,
|
|
2784
|
+
returned: geographies.length,
|
|
2785
|
+
// The endpoint returns EVERY geo unit for the layer (no cursor/paging) → the
|
|
2786
|
+
// returned set IS complete; totalAvailable = returned, never a fabricated cap.
|
|
2787
|
+
totalAvailable: geographies.length,
|
|
2788
|
+
truncated: false,
|
|
2789
|
+
filtersApplied: ["defCodes", "spendingType", "geoLayer"],
|
|
2790
|
+
filtersDropped: [],
|
|
2791
|
+
notes: [
|
|
2792
|
+
"Spending tagged to the given Disaster Emergency Fund Codes (DEFC), per geography. amount/perCapita are number|null (a real 0 stays 0; absent → null).",
|
|
2793
|
+
"obligation vs outlay differ: some DEFCs (e.g. IIJA/infrastructure) report $0 OBLIGATIONS by geography while awardCount is nonzero — a $0 with awards is NOT 'no activity'; try spendingType:'outlay' and read awardCount alongside the amount.",
|
|
2794
|
+
"This returns the COMPLETE set of geo units for the layer (no pagination) — totalAvailable is the returned count, not a fabricated total.",
|
|
2795
|
+
],
|
|
2796
|
+
} satisfies Partial<ResponseMeta>,
|
|
2797
|
+
);
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2628
2800
|
export async function listToptierAgencies(args: { limit?: number }) {
|
|
2629
2801
|
const limit = args.limit ?? 50;
|
|
2630
2802
|
return memoize(`usas:toptier:${limit}`, async () => {
|