@cliwant/mcp-sam-gov 1.2.0 → 1.4.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 +22 -9
- package/README.ko.md +22 -9
- package/README.md +70 -12
- package/dist/bea.d.ts +105 -0
- package/dist/bea.d.ts.map +1 -0
- package/dist/bea.js +303 -0
- package/dist/bea.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/dol.d.ts +118 -0
- package/dist/dol.d.ts.map +1 -0
- package/dist/dol.js +421 -0
- package/dist/dol.js.map +1 -0
- package/dist/epa-envirofacts.d.ts +97 -0
- package/dist/epa-envirofacts.d.ts.map +1 -0
- package/dist/epa-envirofacts.js +292 -0
- package/dist/epa-envirofacts.js.map +1 -0
- package/dist/fred.d.ts +1 -1
- package/dist/fred.js +1 -1
- package/dist/keys.d.ts +11 -8
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +55 -8
- package/dist/keys.js.map +1 -1
- package/dist/lda.d.ts +105 -0
- package/dist/lda.d.ts.map +1 -0
- package/dist/lda.js +317 -0
- package/dist/lda.js.map +1 -0
- 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/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/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.d.ts +133 -0
- package/dist/openfda.d.ts.map +1 -0
- package/dist/openfda.js +402 -0
- package/dist/openfda.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +872 -6
- package/dist/server.js.map +1 -1
- package/package.json +2 -1
- package/src/bea.ts +372 -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/dol.ts +515 -0
- package/src/epa-envirofacts.ts +342 -0
- package/src/fred.ts +1 -1
- package/src/keys.ts +60 -8
- package/src/lda.ts +385 -0
- package/src/nhtsa.ts +352 -0
- package/src/nonprofit.ts +460 -0
- package/src/openfda-device.ts +356 -0
- package/src/openfda.ts +495 -0
- package/src/server.ts +995 -6
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cms-utilization.ts — CMS Medicare Physician & Other Practitioners "by Provider
|
|
3
|
+
* and Service" utilization (`data.cms.gov`, the data-API v1 dataset endpoint;
|
|
4
|
+
* ADR-0061). KEYLESS.
|
|
5
|
+
*
|
|
6
|
+
* WHAT IT ADDS: `cms_medicare_provider_services` — a healthcare-market /
|
|
7
|
+
* competitor-utilization lane: for a given provider (NPI) or state, what Medicare
|
|
8
|
+
* Part-B services (HCPCS) did providers render, to how many beneficiaries, at what
|
|
9
|
+
* submitted / Medicare-allowed / Medicare-paid amounts. The demand-side complement
|
|
10
|
+
* to NPPES (who the providers ARE) — this is what they actually BILL.
|
|
11
|
+
*
|
|
12
|
+
* ★THE TWO-REQUEST PATTERN (the load-bearing P1 honesty — MIRRORS epa-envirofacts):
|
|
13
|
+
* the data-API's `/data` slice is a bare JSON array that reports NO total. So the
|
|
14
|
+
* EXACT total for a filter comes from a SEPARATE count sub-query — the identical
|
|
15
|
+
* `filter[...]` on the `/data-viewer/stats` endpoint returns
|
|
16
|
+
* `{ "data": { "found_rows": N, "total_rows": M } }`. This tool runs the stats
|
|
17
|
+
* count FIRST (best-effort) then the data slice: totalAvailable = found_rows (P1,
|
|
18
|
+
* the per-filter EXACT total), NEVER the returned rows' length. If the stats
|
|
19
|
+
* sub-query fails or is absent, totalAvailable falls to null + a disclosing note
|
|
20
|
+
* (never a length-faked total) and the data slice still returns.
|
|
21
|
+
*
|
|
22
|
+
* ★THE FILTER-REQUIRED INPUT GUARD: the table is 9.78M rows. An all-empty query
|
|
23
|
+
* (no npi, no state) is REFUSED with invalid_input (0 fetch) — providerType /
|
|
24
|
+
* hcpcsCode alone are NOT enough to scope; a caller MUST pin npi OR state.
|
|
25
|
+
*
|
|
26
|
+
* The module writes ZERO fetch/coercion/error/meta code — it REUSES `getJson`
|
|
27
|
+
* (redirect:"error") / `driftError` (datasource.ts), `str`/`num` (coerce.ts,
|
|
28
|
+
* null-never-empty-string / null-never-0), and `withMeta`·`buildMeta` (meta.ts,
|
|
29
|
+
* offset pagination + totalAvailable). It MIRRORS census-economic.ts's fixed-host
|
|
30
|
+
* SSRF idiom (a single host const + a post-construction hostname/protocol assertion
|
|
31
|
+
* + redirect:"error") and epa-envirofacts.ts's count-first two-request pattern +
|
|
32
|
+
* schema_drift catch-ladder (ToolErrorCarrier rethrow FIRST so a 5xx keeps its
|
|
33
|
+
* taxonomy → SyntaxError→driftError → bare rethrow).
|
|
34
|
+
*
|
|
35
|
+
* GET https://data.cms.gov/data-api/v1/dataset/{uuid}/data-viewer/stats
|
|
36
|
+
* ?filter[Rndrng_Prvdr_State_Abrvtn]=VA (the COUNT sub-query)
|
|
37
|
+
* → { "data": { "found_rows": 278254, "total_rows": 9781673 } }
|
|
38
|
+
* GET https://data.cms.gov/data-api/v1/dataset/{uuid}/data
|
|
39
|
+
* ?size=&offset=&filter[Rndrng_Prvdr_State_Abrvtn]=VA (the DATA slice)
|
|
40
|
+
* → [ { Rndrng_NPI, Rndrng_Prvdr_Last_Org_Name, …, Avg_Mdcr_Pymt_Amt }, … ]
|
|
41
|
+
*
|
|
42
|
+
* ★ SSRF: the host is a compile-time literal (`CMS_HOST`); the dataset UUID + the
|
|
43
|
+
* endpoint paths are MODULE literals. Every USER filter value rides as a
|
|
44
|
+
* URLSearchParams VALUE (`filter[Col]=Val`) — URLSearchParams encodes the bracket
|
|
45
|
+
* key AND the value, so a value can never break out into the path or inject a
|
|
46
|
+
* parameter. npi is `^\d{10}$`; state `^[A-Za-z]{2}$`; hcpcsCode `^[A-Za-z0-9]{1,10}$`;
|
|
47
|
+
* providerType is a bounded free-text charclass; size/offset are coerced to
|
|
48
|
+
* integers. A post-construction hostname/protocol assertion + `redirect:"error"`
|
|
49
|
+
* fail closed on any off-host 3xx.
|
|
50
|
+
*
|
|
51
|
+
* ★ PII NOTE: this is public PROVIDER-level AGGREGATE data — no patient identifiers.
|
|
52
|
+
* Provider name / practice address / NPI is public professional information (the
|
|
53
|
+
* same public surface as NPPES), so it is fine to surface.
|
|
54
|
+
*
|
|
55
|
+
* ★ HONESTY (ADR-0061 P1–P5, live-verified 2026-07-15 on data.cms.gov):
|
|
56
|
+
* [input] require npi OR state — an all-empty query is REFUSED (0 fetch) so the
|
|
57
|
+
* whole 9.78M-row table is never scanned.
|
|
58
|
+
* [P1] totalAvailable = the stats sub-query's found_rows (EXACT — e.g. VA =
|
|
59
|
+
* 278254), NOT the slice length. hasMore = offset+returned < total. Stats
|
|
60
|
+
* fails/absent ⇒ totalAvailable:null + a disclosing note.
|
|
61
|
+
* [P2] an empty array ⇒ honest empty (returned:0). getJson maps a 4xx/5xx via
|
|
62
|
+
* errorFromResponse and THROWS (503 ⇒ upstream_unavailable, 400 ⇒
|
|
63
|
+
* invalid_input, 404 ⇒ not_found); a 200 non-array/non-JSON body ⇒
|
|
64
|
+
* schema_drift (NEVER a fabricated empty).
|
|
65
|
+
* [P3] Tot_ / Avg_ fields via num() (numeric strings → numbers; a real 0 stays 0;
|
|
66
|
+
* absent ⇒ null, never 0-faked); NPI/codes/HCPCS/names as strings
|
|
67
|
+
* (null-never-empty-string).
|
|
68
|
+
* [P4] a data body that is not an array ⇒ driftError; a stats body missing
|
|
69
|
+
* found_rows ⇒ totalAvailable:null (handled, not a crash).
|
|
70
|
+
*/
|
|
71
|
+
import { ToolErrorCarrier } from "./errors.js";
|
|
72
|
+
import { getJson, driftError } from "./datasource.js";
|
|
73
|
+
import { str, num } from "./coerce.js";
|
|
74
|
+
import { withMeta } from "./meta.js";
|
|
75
|
+
// Re-export the shared honesty coercions (single audited copy in ./coerce.js) so a
|
|
76
|
+
// regression fails together across sources. NO local num/str.
|
|
77
|
+
export { num, str };
|
|
78
|
+
// ─── SSRF core: the single fixed host + module-literal path pieces ──
|
|
79
|
+
const CMS_HOST = "data.cms.gov";
|
|
80
|
+
// HOST-only label — surfaces in ToolError.upstreamEndpoint; keyless, so no token
|
|
81
|
+
// can ever appear here.
|
|
82
|
+
const CMS_LABEL = "cms-utilization:data.cms.gov";
|
|
83
|
+
// ★THE DATASET UUID — a SPECIFIC ANNUAL VINTAGE of "Medicare Physician & Other
|
|
84
|
+
// Practitioners — by Provider and Service" (9,781,673 rows at build time, the most
|
|
85
|
+
// recent published year). ★UPDATE YEARLY: CMS publishes a NEW uuid for each new
|
|
86
|
+
// calendar year of utilization; this constant pins ONE vintage. The active vintage
|
|
87
|
+
// is surfaced to the caller in a _meta note (VINTAGE_NOTE) so a consumer never
|
|
88
|
+
// mistakes it for "current" or an unspecified year.
|
|
89
|
+
const CMS_DATASET_UUID = "92396110-2aed-4d63-a6a2-5d6207d46a29";
|
|
90
|
+
// ─── Validation charclasses (SSRF + "verify the input" honesty) ───
|
|
91
|
+
const NPI_RE = /^\d{10}$/; // a 10-digit National Provider Identifier
|
|
92
|
+
const STATE_RE = /^[A-Za-z]{2}$/; // 2-letter state/territory abbreviation
|
|
93
|
+
const HCPCS_RE = /^[A-Za-z0-9]{1,10}$/; // HCPCS/CPT code, e.g. "97110", "G0463"
|
|
94
|
+
// providerType is a specialty label, e.g. "Physical Therapist in Private Practice".
|
|
95
|
+
// It rides as a URLSearchParams VALUE (encoded), so this bound is a sanity guard,
|
|
96
|
+
// not an SSRF necessity: letters/digits/space and common punctuation only.
|
|
97
|
+
const PROVIDER_TYPE_RE = /^[A-Za-z0-9 &.,()/'-]{1,100}$/;
|
|
98
|
+
const SIZE_MIN = 1;
|
|
99
|
+
const SIZE_MAX = 100;
|
|
100
|
+
const SIZE_DEFAULT = 25;
|
|
101
|
+
// ─── Honesty notes (ADR-0061 required set) ────────────────────────
|
|
102
|
+
const VINTAGE_NOTE = `Source dataset: CMS "Medicare Physician & Other Practitioners — by Provider and Service" (data.cms.gov dataset ${CMS_DATASET_UUID}) — a SPECIFIC ANNUAL VINTAGE (the most recent published year at build time), NOT a live/current or a multi-year figure. Amounts and service counts are as-of that reference year. CMS publishes a new dataset id each year.`;
|
|
103
|
+
const AGGREGATE_NOTE = "These are public PROVIDER-level AGGREGATE utilization figures (no patient identifiers). Averages (avgSubmittedCharge / avgMedicareAllowed / avgMedicarePayment) are per-service means for the provider+HCPCS row; totalBeneficiaries is CMS-rounded and suppressed below 11 in the source. This is a utilization snapshot, NOT a fraud, quality, or fitness determination.";
|
|
104
|
+
const COUNT_FALLBACK_NOTE = "The count sub-query (…/data-viewer/stats) failed or did not report found_rows, so totalAvailable is null (unknown) — it was NOT faked from the returned row count. hasMore is a heuristic (a full page ⇒ likely more); re-page with offset to confirm.";
|
|
105
|
+
/**
|
|
106
|
+
* Join the CMS Last_Org_Name + First_Name into one display name. An ORGANIZATION
|
|
107
|
+
* row (entity code "O") carries the org name in Last_Org_Name with an empty
|
|
108
|
+
* First_Name ⇒ just the org name. An INDIVIDUAL carries both ⇒ "Last, First".
|
|
109
|
+
* Either absent ⇒ the present one; both absent ⇒ null (never a fabricated "").
|
|
110
|
+
*/
|
|
111
|
+
export function joinProviderName(lastOrg, first) {
|
|
112
|
+
const last = str(lastOrg);
|
|
113
|
+
const firstName = str(first);
|
|
114
|
+
if (last !== null && firstName !== null)
|
|
115
|
+
return `${last}, ${firstName}`;
|
|
116
|
+
return last ?? firstName ?? null;
|
|
117
|
+
}
|
|
118
|
+
/** Map ONE data-API row → the curated provider-service shape. */
|
|
119
|
+
function mapService(row) {
|
|
120
|
+
const r = (row ?? {});
|
|
121
|
+
return {
|
|
122
|
+
npi: str(r.Rndrng_NPI),
|
|
123
|
+
providerName: joinProviderName(r.Rndrng_Prvdr_Last_Org_Name, r.Rndrng_Prvdr_First_Name),
|
|
124
|
+
credentials: str(r.Rndrng_Prvdr_Crdntls),
|
|
125
|
+
providerType: str(r.Rndrng_Prvdr_Type),
|
|
126
|
+
city: str(r.Rndrng_Prvdr_City),
|
|
127
|
+
state: str(r.Rndrng_Prvdr_State_Abrvtn),
|
|
128
|
+
zip: str(r.Rndrng_Prvdr_Zip5),
|
|
129
|
+
hcpcsCode: str(r.HCPCS_Cd),
|
|
130
|
+
hcpcsDescription: str(r.HCPCS_Desc),
|
|
131
|
+
totalBeneficiaries: num(r.Tot_Benes),
|
|
132
|
+
totalServices: num(r.Tot_Srvcs),
|
|
133
|
+
avgSubmittedCharge: num(r.Avg_Sbmtd_Chrg),
|
|
134
|
+
avgMedicareAllowed: num(r.Avg_Mdcr_Alowd_Amt),
|
|
135
|
+
avgMedicarePayment: num(r.Avg_Mdcr_Pymt_Amt),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// ─── SSRF-guarded fetch (fixed host + hostname assertion + redirect:"error") ──
|
|
139
|
+
/**
|
|
140
|
+
* GET one data.cms.gov JSON resource at a MODULE-BUILT URL (the dataset UUID + the
|
|
141
|
+
* endpoint path are literals; all user filter VALUES are already carried in the
|
|
142
|
+
* URLSearchParams `query`). Asserts the CONSTRUCTED URL's hostname === the fixed
|
|
143
|
+
* host over https, and sets `redirect:"error"` (an off-host 3xx must NOT be
|
|
144
|
+
* followed). Keyless — no headers.
|
|
145
|
+
*/
|
|
146
|
+
async function getCms(path, query) {
|
|
147
|
+
const url = `https://${CMS_HOST}${path}?${query.toString()}`;
|
|
148
|
+
const built = new URL(url);
|
|
149
|
+
if (built.hostname !== CMS_HOST || built.protocol !== "https:") {
|
|
150
|
+
throw new ToolErrorCarrier({
|
|
151
|
+
kind: "invalid_input",
|
|
152
|
+
message: `Constructed CMS data-API URL host ${JSON.stringify(built.hostname)} (${built.protocol}) does not match the fixed host ${JSON.stringify(CMS_HOST)} over https — refusing to fetch (SSRF safety).`,
|
|
153
|
+
retryable: false,
|
|
154
|
+
upstreamEndpoint: CMS_LABEL,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return getJson(built.toString(), { label: CMS_LABEL, redirect: "error" });
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Fetch Medicare Part-B provider-service utilization rows for an NPI / state (+
|
|
161
|
+
* optional providerType / hcpcsCode) → normalized service rows + honest `_meta`.
|
|
162
|
+
* REQUIRES npi OR state (an all-empty query is refused). Runs a stats count
|
|
163
|
+
* sub-query FIRST for the EXACT total (P1), then the data slice; a count failure
|
|
164
|
+
* degrades to totalAvailable:null + a note (never a length-faked total).
|
|
165
|
+
*/
|
|
166
|
+
export async function providerServices(args) {
|
|
167
|
+
// ── [input guard] require npi OR state (never scan the whole 9.78M-row table).
|
|
168
|
+
// providerType / hcpcsCode alone are NOT sufficient to scope. ──
|
|
169
|
+
const hasNpi = args.npi !== undefined && args.npi !== "";
|
|
170
|
+
const hasState = args.state !== undefined && args.state !== "";
|
|
171
|
+
if (!hasNpi && !hasState) {
|
|
172
|
+
throw new ToolErrorCarrier({
|
|
173
|
+
kind: "invalid_input",
|
|
174
|
+
retryable: false,
|
|
175
|
+
message: "cms_medicare_provider_services requires at least `npi` (10-digit) OR `state` (2-letter) — an all-empty query would scan the entire 9.78M-row Medicare utilization table and is refused. providerType/hcpcsCode alone are not enough; add npi or state and retry.",
|
|
176
|
+
upstreamEndpoint: CMS_LABEL,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
// ── Validate + build the filter params (SSRF: charclass + URLSearchParams value).
|
|
180
|
+
// URLSearchParams encodes both the bracket key and the value. ──
|
|
181
|
+
const filters = new URLSearchParams();
|
|
182
|
+
const filtersApplied = [];
|
|
183
|
+
if (hasNpi) {
|
|
184
|
+
const npi = args.npi;
|
|
185
|
+
if (!NPI_RE.test(npi)) {
|
|
186
|
+
throw new ToolErrorCarrier({
|
|
187
|
+
kind: "invalid_input",
|
|
188
|
+
retryable: false,
|
|
189
|
+
message: `Invalid npi ${JSON.stringify(npi)} — expected a 10-digit National Provider Identifier (^\\d{10}$).`,
|
|
190
|
+
upstreamEndpoint: CMS_LABEL,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
filters.set("filter[Rndrng_NPI]", npi);
|
|
194
|
+
filtersApplied.push(`npi:${npi}`);
|
|
195
|
+
}
|
|
196
|
+
if (hasState) {
|
|
197
|
+
const state = args.state;
|
|
198
|
+
if (!STATE_RE.test(state)) {
|
|
199
|
+
throw new ToolErrorCarrier({
|
|
200
|
+
kind: "invalid_input",
|
|
201
|
+
retryable: false,
|
|
202
|
+
message: `Invalid state ${JSON.stringify(state)} — expected a 2-letter state/territory code (^[A-Za-z]{2}$), e.g. "VA".`,
|
|
203
|
+
upstreamEndpoint: CMS_LABEL,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
filters.set("filter[Rndrng_Prvdr_State_Abrvtn]", state.toUpperCase());
|
|
207
|
+
filtersApplied.push(`state:${state.toUpperCase()}`);
|
|
208
|
+
}
|
|
209
|
+
if (args.providerType !== undefined && args.providerType !== "") {
|
|
210
|
+
const pt = args.providerType;
|
|
211
|
+
if (!PROVIDER_TYPE_RE.test(pt)) {
|
|
212
|
+
throw new ToolErrorCarrier({
|
|
213
|
+
kind: "invalid_input",
|
|
214
|
+
retryable: false,
|
|
215
|
+
message: `Invalid providerType ${JSON.stringify(pt)} — allowed: letters, digits, space, & . , ( ) / ' - (≤100 chars).`,
|
|
216
|
+
upstreamEndpoint: CMS_LABEL,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
filters.set("filter[Rndrng_Prvdr_Type]", pt);
|
|
220
|
+
filtersApplied.push(`providerType:${pt}`);
|
|
221
|
+
}
|
|
222
|
+
if (args.hcpcsCode !== undefined && args.hcpcsCode !== "") {
|
|
223
|
+
const hcpcs = args.hcpcsCode;
|
|
224
|
+
if (!HCPCS_RE.test(hcpcs)) {
|
|
225
|
+
throw new ToolErrorCarrier({
|
|
226
|
+
kind: "invalid_input",
|
|
227
|
+
retryable: false,
|
|
228
|
+
message: `Invalid hcpcsCode ${JSON.stringify(hcpcs)} — expected an alphanumeric HCPCS/CPT code (^[A-Za-z0-9]{1,10}$), e.g. "97110" or "G0463".`,
|
|
229
|
+
upstreamEndpoint: CMS_LABEL,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
filters.set("filter[HCPCS_Cd]", hcpcs.toUpperCase());
|
|
233
|
+
filtersApplied.push(`hcpcsCode:${hcpcs.toUpperCase()}`);
|
|
234
|
+
}
|
|
235
|
+
// ── Coerce size / offset to bounded integers (belt-and-suspenders behind the
|
|
236
|
+
// server Zod; a DIRECT handler call bypasses Zod). ──
|
|
237
|
+
let size = SIZE_DEFAULT;
|
|
238
|
+
if (typeof args.size === "number" && Number.isFinite(args.size)) {
|
|
239
|
+
size = Math.trunc(args.size);
|
|
240
|
+
if (size < SIZE_MIN)
|
|
241
|
+
size = SIZE_MIN;
|
|
242
|
+
if (size > SIZE_MAX)
|
|
243
|
+
size = SIZE_MAX;
|
|
244
|
+
}
|
|
245
|
+
let offset = 0;
|
|
246
|
+
if (typeof args.offset === "number" && Number.isFinite(args.offset) && args.offset > 0) {
|
|
247
|
+
offset = Math.trunc(args.offset);
|
|
248
|
+
}
|
|
249
|
+
const dataBase = `/data-api/v1/dataset/${CMS_DATASET_UUID}/data`;
|
|
250
|
+
const statsPath = `/data-api/v1/dataset/${CMS_DATASET_UUID}/data-viewer/stats`;
|
|
251
|
+
// ── (1) The COUNT sub-query FIRST (best-effort — the EXACT total, P1). The stats
|
|
252
|
+
// body is `{ data: { found_rows, total_rows } }`. Any failure (network/5xx/
|
|
253
|
+
// drift/missing field) degrades to totalAvailable:null + a disclosing note; it
|
|
254
|
+
// NEVER throws and NEVER fakes the total from the slice length. ──
|
|
255
|
+
let totalAvailable = null;
|
|
256
|
+
let countFailed = false;
|
|
257
|
+
try {
|
|
258
|
+
const statsBody = await getCms(statsPath, filters);
|
|
259
|
+
const dataObj = statsBody !== null &&
|
|
260
|
+
typeof statsBody === "object" &&
|
|
261
|
+
typeof statsBody.data === "object" &&
|
|
262
|
+
statsBody.data !== null
|
|
263
|
+
? statsBody.data
|
|
264
|
+
: undefined;
|
|
265
|
+
if (dataObj !== undefined) {
|
|
266
|
+
const t = num(dataObj.found_rows);
|
|
267
|
+
if (t !== null && t >= 0) {
|
|
268
|
+
totalAvailable = t;
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
countFailed = true; // present body but no usable found_rows (P4)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
countFailed = true; // stats body not the expected { data: {…} } shape
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
countFailed = true; // any count error ⇒ degrade, never propagate (P1)
|
|
280
|
+
}
|
|
281
|
+
// ── (2) The DATA slice — the authoritative request (a bare JSON array). ──
|
|
282
|
+
const dataQuery = new URLSearchParams(filters);
|
|
283
|
+
dataQuery.set("size", String(size));
|
|
284
|
+
dataQuery.set("offset", String(offset));
|
|
285
|
+
// Catch-ladder (epa-envirofacts / datagov-catalog shape): preserve the
|
|
286
|
+
// 4xx/5xx/timeout ToolErrorCarrier taxonomy FIRST; reclassify a 200 non-JSON
|
|
287
|
+
// SyntaxError to schema_drift SECOND; bare-rethrow LAST.
|
|
288
|
+
let body;
|
|
289
|
+
try {
|
|
290
|
+
body = await getCms(dataBase, dataQuery);
|
|
291
|
+
}
|
|
292
|
+
catch (e) {
|
|
293
|
+
if (e instanceof ToolErrorCarrier)
|
|
294
|
+
throw e;
|
|
295
|
+
if (e instanceof SyntaxError)
|
|
296
|
+
throw driftError(CMS_LABEL, "CMS data-API returned a non-JSON body at HTTP 200 — schema drift (never read as an empty result).");
|
|
297
|
+
throw e;
|
|
298
|
+
}
|
|
299
|
+
// [P4] the data body MUST be an array (a non-array 200 is drift, never a
|
|
300
|
+
// fabricated empty).
|
|
301
|
+
if (!Array.isArray(body)) {
|
|
302
|
+
throw driftError(CMS_LABEL, "CMS data-API shape drift — the /data response must be a JSON array of provider-service rows.");
|
|
303
|
+
}
|
|
304
|
+
const services = body.map(mapService);
|
|
305
|
+
const returned = services.length;
|
|
306
|
+
// ── [P1] pagination. total known ⇒ hasMore = offset+returned < total; total
|
|
307
|
+
// unknown (count degraded) ⇒ a full page is the honest heuristic for "more". ──
|
|
308
|
+
const hasMore = totalAvailable !== null
|
|
309
|
+
? offset + returned < totalAvailable
|
|
310
|
+
: returned === size;
|
|
311
|
+
const nextOffset = hasMore ? offset + returned : null;
|
|
312
|
+
const notes = [VINTAGE_NOTE, AGGREGATE_NOTE];
|
|
313
|
+
if (countFailed)
|
|
314
|
+
notes.push(COUNT_FALLBACK_NOTE);
|
|
315
|
+
const meta = {
|
|
316
|
+
source: `${CMS_HOST} CMS Medicare Physician & Other Practitioners — by Provider and Service (keyless)`,
|
|
317
|
+
keylessMode: true,
|
|
318
|
+
returned,
|
|
319
|
+
totalAvailable,
|
|
320
|
+
filtersApplied,
|
|
321
|
+
filtersDropped: [],
|
|
322
|
+
fieldsUnavailable: [],
|
|
323
|
+
pagination: { offset, limit: size, hasMore, nextOffset },
|
|
324
|
+
notes,
|
|
325
|
+
};
|
|
326
|
+
return withMeta({ services }, meta);
|
|
327
|
+
}
|
|
328
|
+
//# sourceMappingURL=cms-utilization.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cms-utilization.js","sourceRoot":"","sources":["../src/cms-utilization.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* courtlistener.ts — US FEDERAL COURT OPINIONS via CourtListener (ADR-0055) — the
|
|
3
|
+
* LITIGATION / case-law lane. Federal court decisions (opinions/clusters) — who
|
|
4
|
+
* sued whom in which court, the nature of suit, the disposition — the judicial
|
|
5
|
+
* signal no contract/spending/lobbying source carries (e.g. bid-protest and
|
|
6
|
+
* contract-claim opinions from the US Court of Federal Claims `uscfc` and the
|
|
7
|
+
* Federal Circuit `cafc`).
|
|
8
|
+
*
|
|
9
|
+
* ★ PROVENANCE — THIS IS NOT A .gov API (must be disclosed). The DATA is US federal
|
|
10
|
+
* court PUBLIC RECORDS, but the API is **CourtListener**, operated by the **Free
|
|
11
|
+
* Law Project** (a non-profit) which republishes those records KEYLESS. The .gov
|
|
12
|
+
* primary source (PACER) is PAYWALLED. So every response's `_meta.source` AND a
|
|
13
|
+
* note name CourtListener/Free Law Project and disclose the PACER-paywall — the
|
|
14
|
+
* tool never presents itself as a government API.
|
|
15
|
+
*
|
|
16
|
+
* ★ THIS IS A KEYLESS TOOL WITH AN *OPTIONAL* RATE-LIMIT TOKEN (the lda.ts /
|
|
17
|
+
* socrata app-token lineage, NOT the census/fred/bea key-REQUIRED lineage).
|
|
18
|
+
* Anonymous GETs return HTTP 200 — it works with NO token. A free
|
|
19
|
+
* COURTLISTENER_API_TOKEN only RAISES the shared rate limit; when set it rides
|
|
20
|
+
* ONLY as the `Authorization: Token <value>` request header (never the
|
|
21
|
+
* URL/label/_meta/notes/log — the K-test). When unset, NO auth header is sent
|
|
22
|
+
* (genuine keyless). This mirrors lda.ts's optional-Authorization discipline.
|
|
23
|
+
*
|
|
24
|
+
* GET https://www.courtlistener.com/api/rest/v4/search/
|
|
25
|
+
* ?q=&court=&filed_after=&filed_before=&type=o&order_by=<order>[&cursor=]
|
|
26
|
+
* → { count, next, previous, results:[{ caseName, court, court_id, dateFiled,
|
|
27
|
+
* docketNumber, suitNature, status, judge, citation, absolute_url, … }] }
|
|
28
|
+
*
|
|
29
|
+
* ★ HONESTY (ADR-0055 P1–P5):
|
|
30
|
+
* [P1] totalAvailable = `count` (the API's REAL total for the filter, e.g. the
|
|
31
|
+
* uscfc opinion corpus ~10595) — NEVER results.length. ★CURSOR pagination:
|
|
32
|
+
* `next` is a FULL URL carrying an opaque `cursor=` param (NOT page/offset).
|
|
33
|
+
* We EXTRACT the `cursor` value out of `next` and return it as `nextCursor`
|
|
34
|
+
* (offset/nextOffset null — a numeric offset is meaningless); hasMore = next
|
|
35
|
+
* is a non-null string. CourtListener v4 stops counting on deep cursor pages
|
|
36
|
+
* (`count:null`) — that is DISCLOSED (totalAvailable:null + a note), never
|
|
37
|
+
* fabricated as results.length.
|
|
38
|
+
* [P2] a genuine no-match (results:[]) ⇒ honest empty (returned:0). A 400 (bad
|
|
39
|
+
* param) ⇒ invalid_input surfacing the API's message. A 429 (unauth
|
|
40
|
+
* throttle) ⇒ rate_limited THROW (Retry-After honored, never routed around).
|
|
41
|
+
* A 5xx/timeout ⇒ upstream_unavailable THROW. A 200 non-JSON ⇒ schema_drift.
|
|
42
|
+
* [P3] dates are strings; `citation` may be an array/object ⇒ flattened to a safe
|
|
43
|
+
* string / string[] (never fabricated); judge / natureOfSuit / docketNumber
|
|
44
|
+
* are null when absent (never empty-string); every scalar null-never-empty.
|
|
45
|
+
* [P4] `results` non-array, or `count` neither a number NOR null ⇒ driftError
|
|
46
|
+
* (never a fabricated empty/total).
|
|
47
|
+
* [K-test] OPTIONAL token: when COURTLISTENER_API_TOKEN is set it rides ONLY the
|
|
48
|
+
* `Authorization: Token …` header — NEVER the URL/label/_meta/notes/log.
|
|
49
|
+
* Unset ⇒ anonymous (no auth header at all).
|
|
50
|
+
* [SSRF] fixed host `www.courtlistener.com`; a post-construction hostname/protocol
|
|
51
|
+
* assert + `redirect:"error"`; `court` charclass `^[a-z0-9]+$`; the dates
|
|
52
|
+
* charclass `^\d{4}-\d{2}-\d{2}$`; every VALUE rides URLSearchParams. The
|
|
53
|
+
* `cursor` is opaque but charclass-guarded; and when we EXTRACT the cursor
|
|
54
|
+
* from a `next` URL we RE-ASSERT that URL's host is courtlistener.com — an
|
|
55
|
+
* off-host `next` is REFUSED (schema_drift), never followed.
|
|
56
|
+
*/
|
|
57
|
+
import { type MetaBundle } from "./meta.js";
|
|
58
|
+
export declare const COURTLISTENER_HOST = "www.courtlistener.com";
|
|
59
|
+
export declare const COURTLISTENER_CURSOR_RE: RegExp;
|
|
60
|
+
/**
|
|
61
|
+
* The optional Authorization header (keyless-first, lda.ts/socrata app-token
|
|
62
|
+
* lineage). Present ONLY when COURTLISTENER_API_TOKEN is set (non-blank); the value
|
|
63
|
+
* is NEVER logged / never placed in the URL, label, `_meta`, or a note. When unset,
|
|
64
|
+
* `{}` (no header ⇒ genuine anonymous).
|
|
65
|
+
*/
|
|
66
|
+
export declare function courtlistenerAuthHeader(): Record<string, string>;
|
|
67
|
+
/** true iff a CourtListener token is configured (for the `_meta` note — never the value). */
|
|
68
|
+
export declare function courtlistenerTokenPresent(): boolean;
|
|
69
|
+
export type CourtlistenerOpinion = {
|
|
70
|
+
caseName: string | null;
|
|
71
|
+
court: string | null;
|
|
72
|
+
courtId: string | null;
|
|
73
|
+
dateFiled: string | null;
|
|
74
|
+
docketNumber: string | null;
|
|
75
|
+
natureOfSuit: string | null;
|
|
76
|
+
status: string | null;
|
|
77
|
+
judge: string | null;
|
|
78
|
+
citation: string | string[] | null;
|
|
79
|
+
absoluteUrl: string | null;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Flatten CourtListener's `citation` (which may be a string, an array of strings,
|
|
83
|
+
* or an object) to a safe `string | string[] | null` — NEVER fabricated. An array
|
|
84
|
+
* ⇒ the non-null strings (or null when none); a string ⇒ that string; an object ⇒
|
|
85
|
+
* its non-null string VALUES (or null when none); absent ⇒ null. `[object Object]`
|
|
86
|
+
* can never leak (we never `String()` an object).
|
|
87
|
+
*/
|
|
88
|
+
export declare function flattenCitation(x: unknown): string | string[] | null;
|
|
89
|
+
/**
|
|
90
|
+
* Extract the opaque `cursor` value from a CourtListener `next` URL. RE-ASSERTS the
|
|
91
|
+
* `next` URL's host is courtlistener.com — an OFF-HOST `next` is REFUSED (drift),
|
|
92
|
+
* never parsed/followed (SSRF: a poisoned `next` could otherwise steer the next
|
|
93
|
+
* page off-host). Returns the decoded cursor string, or null when `next` is null /
|
|
94
|
+
* carries no cursor. A non-string / unparseable `next` ⇒ null (treated as "no more").
|
|
95
|
+
*/
|
|
96
|
+
export declare function extractNextCursor(next: unknown, label: string): string | null;
|
|
97
|
+
export type CourtlistenerSearchOpinionsArgs = {
|
|
98
|
+
query?: string;
|
|
99
|
+
court?: string;
|
|
100
|
+
dateFiledAfter?: string;
|
|
101
|
+
dateFiledBefore?: string;
|
|
102
|
+
natureOfSuit?: string;
|
|
103
|
+
cursor?: string;
|
|
104
|
+
order?: string;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Search US federal court opinions via CourtListener (`/api/rest/v4/search/`,
|
|
108
|
+
* type=o) → curated opinion rows + honest `_meta`. KEYLESS (an optional
|
|
109
|
+
* COURTLISTENER_API_TOKEN only raises the rate limit, sent as the Authorization
|
|
110
|
+
* header only). ★PROVENANCE: this is CourtListener/Free Law Project (a non-profit),
|
|
111
|
+
* NOT a .gov API — PACER (the .gov source) is paywalled. totalAvailable is the
|
|
112
|
+
* API's REAL `count`; CURSOR pagination (nextCursor extracted from `next`).
|
|
113
|
+
*/
|
|
114
|
+
export declare function searchOpinions(args: CourtlistenerSearchOpinionsArgs): Promise<MetaBundle>;
|
|
115
|
+
//# sourceMappingURL=courtlistener.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"courtlistener.d.ts","sourceRoot":"","sources":["../src/courtlistener.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAKH,OAAO,EAAY,KAAK,UAAU,EAAqB,MAAM,WAAW,CAAC;AAGzE,eAAO,MAAM,kBAAkB,0BAA0B,CAAC;AAgB1D,eAAO,MAAM,uBAAuB,QAAmC,CAAC;AAiBxE;;;;;GAKG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAIhE;AAED,6FAA6F;AAC7F,wBAAgB,yBAAyB,IAAI,OAAO,CAGnD;AAGD,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC;IACnC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAcpE;AA4BD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAqB7E;AAGD,MAAM,MAAM,+BAA+B,GAAG;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,+BAA+B,GACpC,OAAO,CAAC,UAAU,CAAC,CAiMrB"}
|