@cliwant/mcp-sam-gov 0.2.1 → 1.0.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/LICENSE +21 -21
- package/README.ja.md +48 -16
- package/README.ko.md +48 -16
- package/README.md +279 -67
- package/dist/attachments.d.ts +63 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +576 -0
- package/dist/attachments.js.map +1 -0
- package/dist/bls.d.ts +267 -0
- package/dist/bls.d.ts.map +1 -0
- package/dist/bls.js +1549 -0
- package/dist/bls.js.map +1 -0
- package/dist/cache.d.ts +42 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +64 -0
- package/dist/cache.js.map +1 -0
- package/dist/census.d.ts +182 -0
- package/dist/census.d.ts.map +1 -0
- package/dist/census.js +576 -0
- package/dist/census.js.map +1 -0
- package/dist/ckan.d.ts +141 -0
- package/dist/ckan.d.ts.map +1 -0
- package/dist/ckan.js +393 -0
- package/dist/ckan.js.map +1 -0
- package/dist/clinicaltrials.d.ts +180 -0
- package/dist/clinicaltrials.d.ts.map +1 -0
- package/dist/clinicaltrials.js +730 -0
- package/dist/clinicaltrials.js.map +1 -0
- package/dist/cms.d.ts +140 -0
- package/dist/cms.d.ts.map +1 -0
- package/dist/cms.js +482 -0
- package/dist/cms.js.map +1 -0
- package/dist/coerce.d.ts +32 -0
- package/dist/coerce.d.ts.map +1 -0
- package/dist/coerce.js +50 -0
- package/dist/coerce.js.map +1 -0
- package/dist/datagov-catalog.d.ts +84 -0
- package/dist/datagov-catalog.d.ts.map +1 -0
- package/dist/datagov-catalog.js +233 -0
- package/dist/datagov-catalog.js.map +1 -0
- package/dist/datagov.d.ts +146 -0
- package/dist/datagov.d.ts.map +1 -0
- package/dist/datagov.js +689 -0
- package/dist/datagov.js.map +1 -0
- package/dist/datagovKey.d.ts +36 -0
- package/dist/datagovKey.d.ts.map +1 -0
- package/dist/datagovKey.js +60 -0
- package/dist/datagovKey.js.map +1 -0
- package/dist/datasource.d.ts +313 -0
- package/dist/datasource.d.ts.map +1 -0
- package/dist/datasource.js +551 -0
- package/dist/datasource.js.map +1 -0
- package/dist/disclosure.d.ts +55 -0
- package/dist/disclosure.d.ts.map +1 -0
- package/dist/disclosure.js +57 -0
- package/dist/disclosure.js.map +1 -0
- package/dist/ecfr.d.ts +4 -2
- package/dist/ecfr.d.ts.map +1 -1
- package/dist/ecfr.js +92 -17
- package/dist/ecfr.js.map +1 -1
- package/dist/echo.d.ts +143 -0
- package/dist/echo.d.ts.map +1 -0
- package/dist/echo.js +424 -0
- package/dist/echo.js.map +1 -0
- package/dist/edgar.d.ts +377 -0
- package/dist/edgar.d.ts.map +1 -0
- package/dist/edgar.js +2418 -0
- package/dist/edgar.js.map +1 -0
- package/dist/errors.d.ts +102 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +247 -0
- package/dist/errors.js.map +1 -0
- package/dist/fac.d.ts +180 -0
- package/dist/fac.d.ts.map +1 -0
- package/dist/fac.js +416 -0
- package/dist/fac.js.map +1 -0
- package/dist/far.d.ts +170 -0
- package/dist/far.d.ts.map +1 -0
- package/dist/far.js +804 -0
- package/dist/far.js.map +1 -0
- package/dist/fdic.d.ts +599 -0
- package/dist/fdic.d.ts.map +1 -0
- package/dist/fdic.js +1624 -0
- package/dist/fdic.js.map +1 -0
- package/dist/federal-register.d.ts +139 -2
- package/dist/federal-register.d.ts.map +1 -1
- package/dist/federal-register.js +432 -15
- package/dist/federal-register.js.map +1 -1
- package/dist/fema.d.ts +181 -0
- package/dist/fema.d.ts.map +1 -0
- package/dist/fema.js +436 -0
- package/dist/fema.js.map +1 -0
- package/dist/fpds.d.ts +108 -0
- package/dist/fpds.d.ts.map +1 -0
- package/dist/fpds.js +519 -0
- package/dist/fpds.js.map +1 -0
- package/dist/gao.d.ts +64 -0
- package/dist/gao.d.ts.map +1 -0
- package/dist/gao.js +640 -0
- package/dist/gao.js.map +1 -0
- package/dist/govinfo.d.ts +111 -0
- package/dist/govinfo.d.ts.map +1 -0
- package/dist/govinfo.js +422 -0
- package/dist/govinfo.js.map +1 -0
- package/dist/grants.d.ts +27 -4
- package/dist/grants.d.ts.map +1 -1
- package/dist/grants.js +114 -11
- package/dist/grants.js.map +1 -1
- package/dist/gsa-csv.d.ts +249 -0
- package/dist/gsa-csv.d.ts.map +1 -0
- package/dist/gsa-csv.js +784 -0
- package/dist/gsa-csv.js.map +1 -0
- package/dist/integrity.d.ts +212 -0
- package/dist/integrity.d.ts.map +1 -0
- package/dist/integrity.js +707 -0
- package/dist/integrity.js.map +1 -0
- package/dist/meta.d.ts +165 -0
- package/dist/meta.d.ts.map +1 -0
- package/dist/meta.js +162 -0
- package/dist/meta.js.map +1 -0
- package/dist/nih.d.ts +117 -0
- package/dist/nih.d.ts.map +1 -0
- package/dist/nih.js +291 -0
- package/dist/nih.js.map +1 -0
- package/dist/nppes.d.ts +157 -0
- package/dist/nppes.d.ts.map +1 -0
- package/dist/nppes.js +648 -0
- package/dist/nppes.js.map +1 -0
- package/dist/nsf.d.ts +176 -0
- package/dist/nsf.d.ts.map +1 -0
- package/dist/nsf.js +554 -0
- package/dist/nsf.js.map +1 -0
- package/dist/nvd.d.ts +176 -0
- package/dist/nvd.d.ts.map +1 -0
- package/dist/nvd.js +912 -0
- package/dist/nvd.js.map +1 -0
- package/dist/ofac.d.ts +205 -0
- package/dist/ofac.d.ts.map +1 -0
- package/dist/ofac.js +919 -0
- package/dist/ofac.js.map +1 -0
- package/dist/pricing.d.ts +110 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/pricing.js +843 -0
- package/dist/pricing.js.map +1 -0
- package/dist/sam-gov/client.d.ts +60 -2
- package/dist/sam-gov/client.d.ts.map +1 -1
- package/dist/sam-gov/client.js +320 -54
- package/dist/sam-gov/client.js.map +1 -1
- package/dist/sam-gov/index.d.ts +1 -1
- package/dist/sam-gov/index.d.ts.map +1 -1
- package/dist/sam-gov/index.js +1 -1
- package/dist/sam-gov/index.js.map +1 -1
- package/dist/sam-gov/types.d.ts +24 -0
- package/dist/sam-gov/types.d.ts.map +1 -1
- package/dist/sba.d.ts +72 -0
- package/dist/sba.d.ts.map +1 -0
- package/dist/sba.js +281 -0
- package/dist/sba.js.map +1 -0
- package/dist/server.d.ts +14 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3897 -295
- package/dist/server.js.map +1 -1
- package/dist/snapshot.d.ts +98 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +146 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/socrata.d.ts +157 -0
- package/dist/socrata.d.ts.map +1 -0
- package/dist/socrata.js +448 -0
- package/dist/socrata.js.map +1 -0
- package/dist/treasury.d.ts +143 -0
- package/dist/treasury.d.ts.map +1 -0
- package/dist/treasury.js +436 -0
- package/dist/treasury.js.map +1 -0
- package/dist/usaspending.d.ts +260 -65
- package/dist/usaspending.d.ts.map +1 -1
- package/dist/usaspending.js +1664 -228
- package/dist/usaspending.js.map +1 -1
- package/dist/usitc.d.ts +142 -0
- package/dist/usitc.d.ts.map +1 -0
- package/dist/usitc.js +339 -0
- package/dist/usitc.js.map +1 -0
- package/package.json +24 -2
- package/src/attachments.ts +652 -0
- package/src/bls.ts +1943 -0
- package/src/cache.ts +73 -0
- package/src/census.ts +735 -0
- package/src/ckan.ts +495 -0
- package/src/clinicaltrials.ts +923 -0
- package/src/cms.ts +634 -0
- package/src/coerce.ts +47 -0
- package/src/datagov-catalog.ts +296 -0
- package/src/datagov.ts +907 -0
- package/src/datagovKey.ts +68 -0
- package/src/datasource.ts +721 -0
- package/src/disclosure.ts +61 -0
- package/src/ecfr.ts +231 -127
- package/src/echo.ts +496 -0
- package/src/edgar.ts +3014 -0
- package/src/errors.ts +303 -0
- package/src/fac.ts +529 -0
- package/src/far.ts +1007 -0
- package/src/fdic.ts +2052 -0
- package/src/federal-register.ts +706 -191
- package/src/fema.ts +541 -0
- package/src/fpds.ts +620 -0
- package/src/gao.ts +744 -0
- package/src/govinfo.ts +497 -0
- package/src/grants.ts +290 -155
- package/src/gsa-csv.ts +992 -0
- package/src/integrity.ts +928 -0
- package/src/meta.ts +292 -0
- package/src/nih.ts +375 -0
- package/src/nppes.ts +834 -0
- package/src/nsf.ts +706 -0
- package/src/nvd.ts +1124 -0
- package/src/ofac.ts +1166 -0
- package/src/pricing.ts +1075 -0
- package/src/sam-gov/client.ts +345 -63
- package/src/sam-gov/index.ts +5 -1
- package/src/sam-gov/types.ts +22 -0
- package/src/sba.ts +357 -0
- package/src/server.ts +4559 -327
- package/src/snapshot.ts +192 -0
- package/src/socrata.ts +532 -0
- package/src/treasury.ts +575 -0
- package/src/usaspending.ts +2680 -925
- package/src/usitc.ts +420 -0
package/src/treasury.ts
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* US Treasury — Fiscal Data API wrappers (keyless).
|
|
3
|
+
*
|
|
4
|
+
* First non-SAM macro/fiscal source (ADR-0002). Fully PUBLIC, KEYLESS — the
|
|
5
|
+
* Treasury Fiscal Data service (https://fiscaldata.treasury.gov) needs no API
|
|
6
|
+
* key and documents no numeric rate limit. Base:
|
|
7
|
+
* https://api.fiscaldata.treasury.gov/services/api/fiscal_service/
|
|
8
|
+
*
|
|
9
|
+
* Three clean layers (seed the future common DataSource port, R2):
|
|
10
|
+
* fetch — `getTreasury(path, query)` builds the URL + envelope, reusing
|
|
11
|
+
* errors.ts retry/timeout/taxonomy.
|
|
12
|
+
* map — per-dataset PURE mappers Row → domain object, coercing string
|
|
13
|
+
* amounts through the shared `num()`.
|
|
14
|
+
* meta — `treasuryMeta(...)` hands `{ totalAvailable, returned, pagination }`
|
|
15
|
+
* to `withMeta`; meta.ts's `buildMeta` DERIVES complete/truncated.
|
|
16
|
+
*
|
|
17
|
+
* HONESTY TRAPS (live-verified 2026-07-10 — see ADR-0002 Review outcome v2):
|
|
18
|
+
* F1/F9 — `meta.count` / `meta['total-count']` / `meta['total-pages']` are
|
|
19
|
+
* JSON NUMBERS (not strings). `totalAvailable = meta['total-count']`
|
|
20
|
+
* directly; never `num()` them.
|
|
21
|
+
* F2 — VALUE fields are inconsistently typed across datasets (debt/rate
|
|
22
|
+
* amounts are strings; even `rates_of_exchange.exchange_rate`, whose
|
|
23
|
+
* dataType metadata says NUMBER, arrives as a string on the wire).
|
|
24
|
+
* Row value fields are typed `string | number | null` and ALWAYS go
|
|
25
|
+
* through `num()`.
|
|
26
|
+
* F3 — `num(x)` returns null (NEVER 0) for absent values. The literal
|
|
27
|
+
* string "null" is common (early history + MTS parent/summary rows);
|
|
28
|
+
* returning 0 would be a data-absence-as-zero masquerade.
|
|
29
|
+
* F4 — `mts_table_1` mixes child line-items (real amounts) with
|
|
30
|
+
* fiscal-year PARENT/SUMMARY rows whose amounts are all "null".
|
|
31
|
+
* `monthlyStatement` excludes them by default via the LIVE-VERIFIED
|
|
32
|
+
* server-side filter `current_month_gross_outly_amt:gt:0` (3039 →
|
|
33
|
+
* 2769 rows; the 270 dropped rows are exactly the `parent_id="null"`
|
|
34
|
+
* header rows).
|
|
35
|
+
* F5 — `queryDataset` accepts only an ENUM of 5 confirmed paths (no free
|
|
36
|
+
* path) — removes the SSRF surface for this slice.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
getJson,
|
|
41
|
+
driftError,
|
|
42
|
+
throughPathChain,
|
|
43
|
+
CircuitBreaker,
|
|
44
|
+
type ResiliencePath,
|
|
45
|
+
type Provenance,
|
|
46
|
+
} from "./datasource.js";
|
|
47
|
+
import { snapshotPath } from "./snapshot.js";
|
|
48
|
+
import { num, str } from "./coerce.js";
|
|
49
|
+
import { memoize } from "./cache.js";
|
|
50
|
+
import { withMeta, type MetaBundle, type ResponseMeta } from "./meta.js";
|
|
51
|
+
|
|
52
|
+
// Re-export the shared honesty coercion (single audited copy now lives in
|
|
53
|
+
// ./coerce.js — ADR-0005 v2 FIX-C) so existing importers and the fault suite's
|
|
54
|
+
// num-parity guard keep resolving `num` from this module.
|
|
55
|
+
export { num };
|
|
56
|
+
|
|
57
|
+
const BASE =
|
|
58
|
+
"https://api.fiscaldata.treasury.gov/services/api/fiscal_service";
|
|
59
|
+
|
|
60
|
+
// ─── Resilience wiring (ADR-0045 Phase 2 pilot — INERT by default) ─────────
|
|
61
|
+
// The Treasury live host, and a per-host circuit breaker keyed on the FIXED set
|
|
62
|
+
// {this host} (bounded — m3-regression). The breaker is CONSULTED only by
|
|
63
|
+
// `throughPathChain` for a ≥2-path chain; when no snapshot is configured the
|
|
64
|
+
// chain is single-path (live only) and the breaker is a pure no-op. See
|
|
65
|
+
// datasource.ts §"RESILIENCE PORT".
|
|
66
|
+
const TREASURY_HOST = "api.fiscaldata.treasury.gov";
|
|
67
|
+
let treasuryBreaker = new CircuitBreaker([TREASURY_HOST]);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Test-only: reset the resilience circuit breaker between OFFLINE fixtures (the
|
|
71
|
+
* breaker is module-level process state; a fresh instance isolates cases). Not a
|
|
72
|
+
* runtime API — mirrors the `_reset*Cache` convention in the fault suite.
|
|
73
|
+
*/
|
|
74
|
+
export function _resetTreasuryBreakerForTests(): void {
|
|
75
|
+
treasuryBreaker = new CircuitBreaker([TREASURY_HOST]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Confirmed dataset paths (F5 allowlist enum) ──────────────────
|
|
79
|
+
// Live-verified 2026-07-10: path + total-count + key fields (ADR-0002 §Context).
|
|
80
|
+
export const TREASURY_DATASETS = {
|
|
81
|
+
debt_to_penny: "/v2/accounting/od/debt_to_penny",
|
|
82
|
+
avg_interest_rates: "/v2/accounting/od/avg_interest_rates",
|
|
83
|
+
mts_table_1: "/v1/accounting/mts/mts_table_1",
|
|
84
|
+
rates_of_exchange: "/v1/accounting/od/rates_of_exchange",
|
|
85
|
+
debt_outstanding: "/v2/accounting/od/debt_outstanding",
|
|
86
|
+
} as const;
|
|
87
|
+
|
|
88
|
+
export type TreasuryDatasetKey = keyof typeof TREASURY_DATASETS;
|
|
89
|
+
|
|
90
|
+
// ─── Envelope types (F1 counts are numbers; F2 values string|number|null) ──
|
|
91
|
+
export type TreasuryValue = string | number | null;
|
|
92
|
+
export type TreasuryRow = Record<string, TreasuryValue>;
|
|
93
|
+
|
|
94
|
+
export type TreasuryEnvelopeMeta = {
|
|
95
|
+
/** rows in THIS page — a JSON number (F1). */
|
|
96
|
+
count: number;
|
|
97
|
+
/** the truthful grand total for the query — a JSON number (F1). */
|
|
98
|
+
"total-count": number;
|
|
99
|
+
/** a JSON number (F1). */
|
|
100
|
+
"total-pages": number;
|
|
101
|
+
labels?: Record<string, string>;
|
|
102
|
+
dataTypes?: Record<string, string>;
|
|
103
|
+
dataFormats?: Record<string, string>;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type TreasuryEnvelope<Row = TreasuryRow> = {
|
|
107
|
+
data: Row[];
|
|
108
|
+
meta: TreasuryEnvelopeMeta;
|
|
109
|
+
links?: {
|
|
110
|
+
self?: string;
|
|
111
|
+
first?: string;
|
|
112
|
+
prev?: string | null;
|
|
113
|
+
next?: string | null;
|
|
114
|
+
last?: string;
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
type TreasuryQuery = {
|
|
119
|
+
fields?: string;
|
|
120
|
+
filter?: string;
|
|
121
|
+
sort?: string;
|
|
122
|
+
pageSize: number;
|
|
123
|
+
pageNumber: number;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// ─── HONESTY-CRITICAL coercions (F1/F2/F3) ────────────────────────
|
|
127
|
+
// `num`/`str` are the shared, audited null-never-0 coercions in ./coerce.js
|
|
128
|
+
// (imported above, `num` re-exported). F3: `num` returns null (NEVER 0) for
|
|
129
|
+
// absent values — the literal string "null" (early history + MTS parent rows),
|
|
130
|
+
// ""/whitespace (Number("") is 0!), and "(-)"/"-" all become null.
|
|
131
|
+
|
|
132
|
+
// ─── fetch layer ──────────────────────────────────────────────────
|
|
133
|
+
/**
|
|
134
|
+
* GET one Treasury Fiscal Data page. Reuses errors.ts (retry/backoff/timeout +
|
|
135
|
+
* the 429/5xx/404/400 taxonomy: an invalid `fields`/`filter` column ⇒ upstream
|
|
136
|
+
* 400 ⇒ `invalid_input`, surfaced as an error, never a silent drop). Throws a
|
|
137
|
+
* `schema_drift` ToolErrorCarrier if the envelope's `total-count` is not a
|
|
138
|
+
* number (so a future upstream retype can't silently corrupt `_meta`).
|
|
139
|
+
*/
|
|
140
|
+
async function getTreasury<Row = TreasuryRow>(
|
|
141
|
+
path: string,
|
|
142
|
+
query: TreasuryQuery,
|
|
143
|
+
snapshotKey?: string,
|
|
144
|
+
): Promise<{ env: TreasuryEnvelope<Row>; provenance: Provenance }> {
|
|
145
|
+
const params = new URLSearchParams();
|
|
146
|
+
if (query.fields) params.set("fields", query.fields);
|
|
147
|
+
if (query.filter) params.set("filter", query.filter);
|
|
148
|
+
if (query.sort) params.set("sort", query.sort);
|
|
149
|
+
params.set("page[size]", String(query.pageSize));
|
|
150
|
+
params.set("page[number]", String(query.pageNumber));
|
|
151
|
+
const url = `${BASE}${path}?${params.toString()}`;
|
|
152
|
+
const label = "treasury:" + path;
|
|
153
|
+
// ★ADR-0045 Phase 2 pilot — route the fetch through `throughPathChain` INSTEAD
|
|
154
|
+
// of a bare getJson. The LIVE path is byte-identical to the prior hand-rolled
|
|
155
|
+
// fetch (init === { signal }, no headers/redirect — the same getJson call).
|
|
156
|
+
const livePath: ResiliencePath<TreasuryEnvelope<Row>> = {
|
|
157
|
+
host: TREASURY_HOST,
|
|
158
|
+
provenance: { dataPath: "live" },
|
|
159
|
+
run: () => getJson<TreasuryEnvelope<Row>>(url, { label }),
|
|
160
|
+
};
|
|
161
|
+
// The snapshot fallback is added ONLY when (a) this call declares a canned
|
|
162
|
+
// snapshot key AND (b) SAMGOV_SNAPSHOT_BASE_URL is configured (else
|
|
163
|
+
// snapshotPath returns null). ★When either is absent the chain is SINGLE-ENTRY
|
|
164
|
+
// (live only) ⇒ throughPathChain fast-paths (no breaker consult, no overhead)
|
|
165
|
+
// ⇒ behavior byte-identical to before this ADR (the INERT guarantee).
|
|
166
|
+
const snap = snapshotKey
|
|
167
|
+
? snapshotPath<TreasuryEnvelope<Row>>(snapshotKey)
|
|
168
|
+
: null;
|
|
169
|
+
const paths = snap ? [livePath, snap] : [livePath];
|
|
170
|
+
const { body: env, provenance } = await throughPathChain<
|
|
171
|
+
TreasuryEnvelope<Row>
|
|
172
|
+
>(paths, treasuryBreaker);
|
|
173
|
+
// Drift guard applies to BOTH paths (a malformed snapshot fails as loudly as a
|
|
174
|
+
// malformed live body) — schema_drift is non-retryable, so it does not fall
|
|
175
|
+
// through nor trip the breaker.
|
|
176
|
+
if (
|
|
177
|
+
!env ||
|
|
178
|
+
typeof env.meta !== "object" ||
|
|
179
|
+
env.meta === null ||
|
|
180
|
+
typeof env.meta["total-count"] !== "number" ||
|
|
181
|
+
!Array.isArray(env.data)
|
|
182
|
+
) {
|
|
183
|
+
throw driftError(
|
|
184
|
+
label,
|
|
185
|
+
`treasury:${path} returned an unexpected envelope shape (meta['total-count'] must be a number and data an array).`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return { env, provenance };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── meta layer ───────────────────────────────────────────────────
|
|
192
|
+
const STRING_COERCION_NOTE =
|
|
193
|
+
"Treasury value/amount fields arrive as strings (or, for some datasets, numbers) and are coerced via num(): the literal string \"null\" and empty values become null (NOT 0) — a null amount means 'no value reported', never zero.";
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Build the partial `_meta`. Passes `totalAvailable` (= the numeric
|
|
197
|
+
* `total-count`, NEVER data.length/count/page-size) + offset-pagination, and
|
|
198
|
+
* lets meta.ts's `buildMeta` DERIVE `complete`/`truncated` from those signals
|
|
199
|
+
* (do NOT recompute here).
|
|
200
|
+
*/
|
|
201
|
+
function treasuryMeta(opts: {
|
|
202
|
+
env: TreasuryEnvelope;
|
|
203
|
+
path: string;
|
|
204
|
+
pageNumber: number;
|
|
205
|
+
pageSize: number;
|
|
206
|
+
notes: string[];
|
|
207
|
+
filtersApplied?: string[];
|
|
208
|
+
fieldsUnavailable?: string[];
|
|
209
|
+
/**
|
|
210
|
+
* The access path that served this response (ADR-0045 P5). When the live
|
|
211
|
+
* upstream answered (`dataPath:"live"`, the default and — with no snapshot
|
|
212
|
+
* configured — the ONLY path) the dataPath/asOf fields are OMITTED, so `_meta`
|
|
213
|
+
* is byte-identical to before this ADR. They are threaded ONLY for a NON-live
|
|
214
|
+
* (snapshot) body, which makes buildMeta emit a staleness note, gate `complete`
|
|
215
|
+
* off, and qualify `totalAvailable` as an as-of figure.
|
|
216
|
+
*/
|
|
217
|
+
provenance?: Provenance;
|
|
218
|
+
}): Partial<ResponseMeta> {
|
|
219
|
+
const totalAvailable = opts.env.meta["total-count"];
|
|
220
|
+
const returned = opts.env.data.length;
|
|
221
|
+
const offset = (opts.pageNumber - 1) * opts.pageSize;
|
|
222
|
+
const hasMore = offset + returned < totalAvailable;
|
|
223
|
+
const meta: Partial<ResponseMeta> = {
|
|
224
|
+
source: `api.fiscaldata.treasury.gov (keyless) ${opts.path}`,
|
|
225
|
+
keylessMode: true,
|
|
226
|
+
returned,
|
|
227
|
+
totalAvailable,
|
|
228
|
+
pagination: {
|
|
229
|
+
offset,
|
|
230
|
+
limit: opts.pageSize,
|
|
231
|
+
hasMore,
|
|
232
|
+
nextOffset: hasMore ? offset + returned : null,
|
|
233
|
+
},
|
|
234
|
+
filtersApplied: opts.filtersApplied ?? [],
|
|
235
|
+
filtersDropped: [],
|
|
236
|
+
fieldsUnavailable: opts.fieldsUnavailable ?? [],
|
|
237
|
+
notes: opts.notes,
|
|
238
|
+
};
|
|
239
|
+
// P5 threading (ADR-0045 B2): surface dataPath/asOf ONLY when NON-live, so a
|
|
240
|
+
// live response omits the keys and stays byte-identical. No `??` default.
|
|
241
|
+
if (opts.provenance && opts.provenance.dataPath !== "live") {
|
|
242
|
+
meta.dataPath = opts.provenance.dataPath;
|
|
243
|
+
if (opts.provenance.asOf !== undefined) meta.asOf = opts.provenance.asOf;
|
|
244
|
+
}
|
|
245
|
+
return meta;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Build `record_date` gte/lte filter clauses from optional ISO dates. */
|
|
249
|
+
function dateFilters(startDate?: string, endDate?: string): string[] {
|
|
250
|
+
const f: string[] = [];
|
|
251
|
+
if (startDate) f.push(`record_date:gte:${startDate}`);
|
|
252
|
+
if (endDate) f.push(`record_date:lte:${endDate}`);
|
|
253
|
+
return f;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** UTC "today minus N months" as YYYY-MM-DD, for trailing-window defaults. */
|
|
257
|
+
function isoMonthsAgo(months: number): string {
|
|
258
|
+
const d = new Date();
|
|
259
|
+
d.setUTCMonth(d.getUTCMonth() - months);
|
|
260
|
+
return d.toISOString().slice(0, 10);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The most recent `record_date` for a dataset (memoized 5-min — slow-changing
|
|
265
|
+
* reference read). Used to pin the "latest" period for the per-date-breakdown
|
|
266
|
+
* datasets (avg_interest_rates) where a single-row page would bleed across
|
|
267
|
+
* months.
|
|
268
|
+
*/
|
|
269
|
+
async function latestRecordDate(path: string): Promise<string | null> {
|
|
270
|
+
return memoize(`treasury:latestdate:${path}`, async () => {
|
|
271
|
+
const { env } = await getTreasury(path, {
|
|
272
|
+
fields: "record_date",
|
|
273
|
+
sort: "-record_date",
|
|
274
|
+
pageSize: 1,
|
|
275
|
+
pageNumber: 1,
|
|
276
|
+
});
|
|
277
|
+
return str(env.data[0]?.record_date);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ─── Tool: queryDataset (escape hatch, F5 enum-only) ──────────────
|
|
282
|
+
/**
|
|
283
|
+
* Generic escape hatch over the 5 confirmed datasets (covers debt_outstanding +
|
|
284
|
+
* rates_of_exchange without a dedicated tool). Rows are passed through RAW
|
|
285
|
+
* (value fields as strings exactly as upstream provides) — the `_meta` note
|
|
286
|
+
* warns that the literal string "null" means "no value", never zero.
|
|
287
|
+
*/
|
|
288
|
+
export async function queryDataset(args: {
|
|
289
|
+
dataset: TreasuryDatasetKey;
|
|
290
|
+
fields?: string;
|
|
291
|
+
filter?: string;
|
|
292
|
+
sort?: string;
|
|
293
|
+
pageSize?: number;
|
|
294
|
+
pageNumber?: number;
|
|
295
|
+
}): Promise<MetaBundle> {
|
|
296
|
+
const path = TREASURY_DATASETS[args.dataset];
|
|
297
|
+
const pageSize = args.pageSize ?? 100;
|
|
298
|
+
const pageNumber = args.pageNumber ?? 1;
|
|
299
|
+
const { env, provenance } = await getTreasury(path, {
|
|
300
|
+
fields: args.fields,
|
|
301
|
+
filter: args.filter,
|
|
302
|
+
sort: args.sort,
|
|
303
|
+
pageSize,
|
|
304
|
+
pageNumber,
|
|
305
|
+
});
|
|
306
|
+
const filtersApplied: string[] = [];
|
|
307
|
+
if (args.filter) filtersApplied.push("filter");
|
|
308
|
+
if (args.fields) filtersApplied.push("fields");
|
|
309
|
+
return withMeta(
|
|
310
|
+
{ dataset: args.dataset, path, rows: env.data },
|
|
311
|
+
treasuryMeta({
|
|
312
|
+
env,
|
|
313
|
+
path,
|
|
314
|
+
pageNumber,
|
|
315
|
+
pageSize,
|
|
316
|
+
filtersApplied,
|
|
317
|
+
provenance,
|
|
318
|
+
notes: [
|
|
319
|
+
"Raw pass-through: value/amount fields are the upstream strings (or numbers) verbatim — the literal string \"null\" or an empty value means 'no value reported', NOT zero. Parse client-side.",
|
|
320
|
+
],
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ─── Tool: debtToPenny ────────────────────────────────────────────
|
|
326
|
+
const DEBT_TO_PENNY_FIELDS =
|
|
327
|
+
"record_date,tot_pub_debt_out_amt,debt_held_public_amt,intragov_hold_amt";
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Daily total US public debt ("Debt to the Penny"). `latest` (default true) ⇒
|
|
331
|
+
* the single most-recent day; else the `startDate/endDate` range, newest first.
|
|
332
|
+
* Amounts are USD, coerced via num() (null, never 0, for absent).
|
|
333
|
+
*/
|
|
334
|
+
export async function debtToPenny(args: {
|
|
335
|
+
latest?: boolean;
|
|
336
|
+
startDate?: string;
|
|
337
|
+
endDate?: string;
|
|
338
|
+
pageSize?: number;
|
|
339
|
+
pageNumber?: number;
|
|
340
|
+
}): Promise<MetaBundle> {
|
|
341
|
+
const path = TREASURY_DATASETS.debt_to_penny;
|
|
342
|
+
const latest = args.latest ?? true;
|
|
343
|
+
const pageNumber = args.pageNumber ?? 1;
|
|
344
|
+
const pageSize = latest ? 1 : args.pageSize ?? 100;
|
|
345
|
+
const filter = latest
|
|
346
|
+
? undefined
|
|
347
|
+
: dateFilters(args.startDate, args.endDate).join(",") || undefined;
|
|
348
|
+
// ★Snapshot fallback ONLY for the canned "latest" read (a single, well-defined
|
|
349
|
+
// most-recent-day snapshot the builder can pre-fetch). Range/paginated reads
|
|
350
|
+
// pass no key ⇒ live only. Even for latest, the snapshot is INERT unless
|
|
351
|
+
// SAMGOV_SNAPSHOT_BASE_URL is configured (snapshotPath returns null).
|
|
352
|
+
const snapshotKey = latest ? "treasury_debt_to_penny_latest" : undefined;
|
|
353
|
+
const { env, provenance } = await getTreasury(
|
|
354
|
+
path,
|
|
355
|
+
{
|
|
356
|
+
fields: DEBT_TO_PENNY_FIELDS,
|
|
357
|
+
filter,
|
|
358
|
+
sort: "-record_date",
|
|
359
|
+
pageSize,
|
|
360
|
+
pageNumber,
|
|
361
|
+
},
|
|
362
|
+
snapshotKey,
|
|
363
|
+
);
|
|
364
|
+
const data = {
|
|
365
|
+
records: env.data.map((r) => ({
|
|
366
|
+
recordDate: str(r.record_date),
|
|
367
|
+
totalPublicDebtOutstanding: num(r.tot_pub_debt_out_amt),
|
|
368
|
+
debtHeldByPublic: num(r.debt_held_public_amt),
|
|
369
|
+
intragovernmentalHoldings: num(r.intragov_hold_amt),
|
|
370
|
+
})),
|
|
371
|
+
};
|
|
372
|
+
const filtersApplied: string[] = [];
|
|
373
|
+
if (!latest && (args.startDate || args.endDate)) filtersApplied.push("recordDate");
|
|
374
|
+
return withMeta(
|
|
375
|
+
data,
|
|
376
|
+
treasuryMeta({
|
|
377
|
+
env,
|
|
378
|
+
path,
|
|
379
|
+
pageNumber,
|
|
380
|
+
pageSize,
|
|
381
|
+
filtersApplied,
|
|
382
|
+
provenance,
|
|
383
|
+
notes: [
|
|
384
|
+
latest
|
|
385
|
+
? "latest=true returns only the single most-recent day (page[size]=1)."
|
|
386
|
+
: "Range mode: rows filtered by record_date, sorted newest-first.",
|
|
387
|
+
STRING_COERCION_NOTE,
|
|
388
|
+
],
|
|
389
|
+
}),
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ─── Tool: monthlyStatement (MTS table 1) — F4 summary-row exclusion ──
|
|
394
|
+
const MTS_FIELDS =
|
|
395
|
+
"record_date,classification_desc,parent_id,line_code_nbr,current_month_gross_rcpt_amt,current_month_gross_outly_amt,current_month_dfct_sur_amt";
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Monthly Treasury Statement (MTS) table 1 — federal receipts, outlays, and the
|
|
399
|
+
* deficit/surplus by month. `startDate/endDate` (ISO) filter record_date;
|
|
400
|
+
* default is the trailing ~12 months.
|
|
401
|
+
*
|
|
402
|
+
* F4 — SUMMARY-ROW EXCLUSION (live-verified 2026-07-10): mts_table_1 interleaves
|
|
403
|
+
* child line-items (real amounts) with fiscal-year PARENT/SUMMARY header rows
|
|
404
|
+
* whose `parent_id` is the string "null" and whose amount fields are all the
|
|
405
|
+
* string "null". `excludeSummaryRows` (default true) appends the server-side
|
|
406
|
+
* filter `current_month_gross_outly_amt:gt:0`, which drops EXACTLY those parent
|
|
407
|
+
* rows (total-count 3039 → 2769; the 270 dropped rows == the parent_id="null"
|
|
408
|
+
* set). Because the exclusion is server-side, `total-count` (and hence
|
|
409
|
+
* `_meta.totalAvailable` / pagination) reflect the child rows only — no
|
|
410
|
+
* client-side filtering that would desync `returned` from the envelope count.
|
|
411
|
+
*/
|
|
412
|
+
export async function monthlyStatement(args: {
|
|
413
|
+
startDate?: string;
|
|
414
|
+
endDate?: string;
|
|
415
|
+
excludeSummaryRows?: boolean;
|
|
416
|
+
pageSize?: number;
|
|
417
|
+
pageNumber?: number;
|
|
418
|
+
}): Promise<MetaBundle> {
|
|
419
|
+
const path = TREASURY_DATASETS.mts_table_1;
|
|
420
|
+
const excludeSummary = args.excludeSummaryRows ?? true;
|
|
421
|
+
const pageNumber = args.pageNumber ?? 1;
|
|
422
|
+
const pageSize = args.pageSize ?? 100;
|
|
423
|
+
// Default to the trailing ~12 months when no explicit window is given.
|
|
424
|
+
const startDate =
|
|
425
|
+
args.startDate ?? (args.endDate ? undefined : isoMonthsAgo(12));
|
|
426
|
+
const filters = dateFilters(startDate, args.endDate);
|
|
427
|
+
if (excludeSummary) filters.push("current_month_gross_outly_amt:gt:0");
|
|
428
|
+
const filter = filters.length ? filters.join(",") : undefined;
|
|
429
|
+
const { env, provenance } = await getTreasury(path, {
|
|
430
|
+
fields: MTS_FIELDS,
|
|
431
|
+
filter,
|
|
432
|
+
sort: "-record_date,line_code_nbr",
|
|
433
|
+
pageSize,
|
|
434
|
+
pageNumber,
|
|
435
|
+
});
|
|
436
|
+
const data = {
|
|
437
|
+
records: env.data.map((r) => ({
|
|
438
|
+
recordDate: str(r.record_date),
|
|
439
|
+
classification: str(r.classification_desc),
|
|
440
|
+
grossReceipts: num(r.current_month_gross_rcpt_amt),
|
|
441
|
+
grossOutlays: num(r.current_month_gross_outly_amt),
|
|
442
|
+
deficitSurplus: num(r.current_month_dfct_sur_amt),
|
|
443
|
+
})),
|
|
444
|
+
};
|
|
445
|
+
const filtersApplied: string[] = [];
|
|
446
|
+
if (startDate || args.endDate) filtersApplied.push("recordDate");
|
|
447
|
+
if (excludeSummary) filtersApplied.push("excludeSummaryRows");
|
|
448
|
+
const notes: string[] = [
|
|
449
|
+
excludeSummary
|
|
450
|
+
? "Summary/parent rows excluded (default): the server-side filter current_month_gross_outly_amt:gt:0 drops the fiscal-year header rows (parent_id=\"null\", all amounts \"null\"). totalAvailable reflects child line-item rows only. Pass excludeSummaryRows=false to include them."
|
|
451
|
+
: "excludeSummaryRows=false: fiscal-year parent/summary rows (parent_id=\"null\") ARE included; their amount fields are all null — do NOT read those null-amount rows as data.",
|
|
452
|
+
STRING_COERCION_NOTE,
|
|
453
|
+
];
|
|
454
|
+
return withMeta(
|
|
455
|
+
data,
|
|
456
|
+
treasuryMeta({
|
|
457
|
+
env,
|
|
458
|
+
path,
|
|
459
|
+
pageNumber,
|
|
460
|
+
pageSize,
|
|
461
|
+
filtersApplied,
|
|
462
|
+
notes,
|
|
463
|
+
provenance,
|
|
464
|
+
}),
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ─── Tool: avgInterestRates ───────────────────────────────────────
|
|
469
|
+
const AVG_INTEREST_FIELDS =
|
|
470
|
+
"record_date,security_type_desc,security_desc,avg_interest_rate_amt";
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Average interest rate the Treasury pays by security type/description.
|
|
474
|
+
* `latest` (default true) returns the most-recent month's full breakdown across
|
|
475
|
+
* security types (pinned to the latest record_date, memoized — a slow-changing
|
|
476
|
+
* reference read); else the `startDate/endDate` range. Optional `securityType`
|
|
477
|
+
* narrows by `security_type_desc` (e.g. "Marketable", "Non-marketable").
|
|
478
|
+
* `avg_interest_rate_amt` is a PERCENT, coerced via num() (null, never 0).
|
|
479
|
+
*/
|
|
480
|
+
export async function avgInterestRates(args: {
|
|
481
|
+
securityType?: string;
|
|
482
|
+
latest?: boolean;
|
|
483
|
+
startDate?: string;
|
|
484
|
+
endDate?: string;
|
|
485
|
+
pageSize?: number;
|
|
486
|
+
pageNumber?: number;
|
|
487
|
+
}): Promise<MetaBundle> {
|
|
488
|
+
const latest = args.latest ?? true;
|
|
489
|
+
if (latest) {
|
|
490
|
+
const key = `treasury:avg_interest_rates:latest:${args.securityType ?? "*"}`;
|
|
491
|
+
return memoize(key, () => avgInterestRatesLatest(args.securityType));
|
|
492
|
+
}
|
|
493
|
+
return avgInterestRatesQuery({
|
|
494
|
+
securityType: args.securityType,
|
|
495
|
+
startDate: args.startDate,
|
|
496
|
+
endDate: args.endDate,
|
|
497
|
+
pageSize: args.pageSize ?? 100,
|
|
498
|
+
pageNumber: args.pageNumber ?? 1,
|
|
499
|
+
latest: false,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function avgInterestRatesLatest(
|
|
504
|
+
securityType?: string,
|
|
505
|
+
): Promise<MetaBundle> {
|
|
506
|
+
const path = TREASURY_DATASETS.avg_interest_rates;
|
|
507
|
+
const maxDate = await latestRecordDate(path);
|
|
508
|
+
return avgInterestRatesQuery({
|
|
509
|
+
securityType,
|
|
510
|
+
startDate: maxDate ?? undefined,
|
|
511
|
+
endDate: maxDate ?? undefined,
|
|
512
|
+
pageSize: 100,
|
|
513
|
+
pageNumber: 1,
|
|
514
|
+
latest: true,
|
|
515
|
+
latestDate: maxDate,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async function avgInterestRatesQuery(opts: {
|
|
520
|
+
securityType?: string;
|
|
521
|
+
startDate?: string;
|
|
522
|
+
endDate?: string;
|
|
523
|
+
pageSize: number;
|
|
524
|
+
pageNumber: number;
|
|
525
|
+
latest: boolean;
|
|
526
|
+
latestDate?: string | null;
|
|
527
|
+
}): Promise<MetaBundle> {
|
|
528
|
+
const path = TREASURY_DATASETS.avg_interest_rates;
|
|
529
|
+
const filters = dateFilters(opts.startDate, opts.endDate);
|
|
530
|
+
if (opts.securityType) {
|
|
531
|
+
filters.push(`security_type_desc:eq:${opts.securityType}`);
|
|
532
|
+
}
|
|
533
|
+
const filter = filters.length ? filters.join(",") : undefined;
|
|
534
|
+
const { env, provenance } = await getTreasury(path, {
|
|
535
|
+
fields: AVG_INTEREST_FIELDS,
|
|
536
|
+
filter,
|
|
537
|
+
sort: "-record_date,security_type_desc",
|
|
538
|
+
pageSize: opts.pageSize,
|
|
539
|
+
pageNumber: opts.pageNumber,
|
|
540
|
+
});
|
|
541
|
+
const data = {
|
|
542
|
+
records: env.data.map((r) => ({
|
|
543
|
+
recordDate: str(r.record_date),
|
|
544
|
+
securityType: str(r.security_type_desc),
|
|
545
|
+
securityDescription: str(r.security_desc),
|
|
546
|
+
avgInterestRatePercent: num(r.avg_interest_rate_amt),
|
|
547
|
+
})),
|
|
548
|
+
};
|
|
549
|
+
const filtersApplied: string[] = [];
|
|
550
|
+
if (opts.startDate || opts.endDate) filtersApplied.push("recordDate");
|
|
551
|
+
if (opts.securityType) filtersApplied.push("securityType");
|
|
552
|
+
const notes: string[] = [];
|
|
553
|
+
if (opts.latest) {
|
|
554
|
+
notes.push(
|
|
555
|
+
opts.latestDate
|
|
556
|
+
? `latest=true: pinned to the most-recent record_date (${opts.latestDate}); served from a 5-minute TTL cache.`
|
|
557
|
+
: "latest=true: could not resolve the most-recent record_date (upstream returned no rows).",
|
|
558
|
+
);
|
|
559
|
+
} else {
|
|
560
|
+
notes.push("Range mode: rows filtered by record_date, sorted newest-first.");
|
|
561
|
+
}
|
|
562
|
+
notes.push(STRING_COERCION_NOTE);
|
|
563
|
+
return withMeta(
|
|
564
|
+
data,
|
|
565
|
+
treasuryMeta({
|
|
566
|
+
env,
|
|
567
|
+
path,
|
|
568
|
+
pageNumber: opts.pageNumber,
|
|
569
|
+
pageSize: opts.pageSize,
|
|
570
|
+
filtersApplied,
|
|
571
|
+
notes,
|
|
572
|
+
provenance,
|
|
573
|
+
}),
|
|
574
|
+
);
|
|
575
|
+
}
|