@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
package/src/nhtsa.ts
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nhtsa.ts — NHTSA VEHICLE SAFETY (api.nhtsa.gov) — the vehicle / parts /
|
|
3
|
+
* fleet supplier PRODUCT-SAFETY vetting lane (ADR-0057). Two keyless tools:
|
|
4
|
+
* • nhtsa_recalls — /recalls/recallsByVehicle?make=&model=&modelYear=
|
|
5
|
+
* • nhtsa_complaints — /complaints/complaintsByVehicle?make=&model=&modelYear=
|
|
6
|
+
* The cross-agency product-safety family alongside openFDA (medical) / CPSC
|
|
7
|
+
* (consumer goods): a manufacturer/component/safety-signal history for B2G
|
|
8
|
+
* supplier vetting.
|
|
9
|
+
*
|
|
10
|
+
* ★ KEYLESS — there is NO API key at all (no parameter, no header). This module
|
|
11
|
+
* touches NO key seam (no KEY_REGISTRY / keys.ts / API_KEYS.md). It REUSES the
|
|
12
|
+
* shared `getJson` (redirect:"error") / `driftError` fetch envelope, the `num`/
|
|
13
|
+
* `str` coercions (null-never-empty-string; a genuine 0 stays 0), and
|
|
14
|
+
* `withMeta`/`buildMeta` — and mirrors datagov-catalog.ts's fixed-host SSRF
|
|
15
|
+
* idiom + schema_drift catch-ladder verbatim.
|
|
16
|
+
*
|
|
17
|
+
* ★ PII — the complaints upstream response carries a `vin` field (an individual
|
|
18
|
+
* vehicle identifier). It is DELIBERATELY OMITTED from the curated output
|
|
19
|
+
* entirely — never surfaced, logged, or stored. The B2G value is the
|
|
20
|
+
* manufacturer / component / safety signal, NOT the VIN.
|
|
21
|
+
*
|
|
22
|
+
* ★ THE HONESTY PILLARS (P1-P4, live-verified 2026-07-15):
|
|
23
|
+
* P1: totalAvailable = `Count` (recalls) / `count` (complaints) — the REAL total.
|
|
24
|
+
* NHTSA returns the COMPLETE filtered set (no pagination), so in the normal
|
|
25
|
+
* case Count === results.length ⇒ complete:true. totalAvailable is NEVER
|
|
26
|
+
* fabricated: a PRESENT numeric Count is trusted verbatim; a MISSING Count
|
|
27
|
+
* falls back to results.length WITH an honest note (never invented).
|
|
28
|
+
* P2: results:[] (Count 0) ⇒ an HONEST EMPTY (returned:0, complete:true) — a bad
|
|
29
|
+
* make/model that returns 200+Count 0 is an honest no-match, NOT an error. A
|
|
30
|
+
* 4xx ⇒ invalid_input; a 5xx/timeout ⇒ THROW (never a fake empty); a 200
|
|
31
|
+
* non-JSON body ⇒ schema_drift.
|
|
32
|
+
* P3: booleans (crash/fire/parkIt/parkOutSide/overTheAirUpdate) preserved AS
|
|
33
|
+
* booleans (a non-boolean ⇒ null, never a fabricated false); counts
|
|
34
|
+
* (numberOfInjuries/numberOfDeaths) via `num` (a genuine 0 stays 0, NEVER
|
|
35
|
+
* null-for-0); dates as strings via `str`; Count/count via `num`.
|
|
36
|
+
* P4: `results` non-array ⇒ driftError; a Count/count that is PRESENT but a
|
|
37
|
+
* non-number ⇒ driftError (a broken total contract, never a fabricated empty).
|
|
38
|
+
* SSRF: fixed host `api.nhtsa.gov` (compile-time literal) + post-construction
|
|
39
|
+
* hostname/protocol assertion + redirect:"error"; make/model ride
|
|
40
|
+
* URLSearchParams (module-built, no raw passthrough); modelYear is
|
|
41
|
+
* ^\d{4}$; make/model are charclass-validated (letters/digits/space/hyphen,
|
|
42
|
+
* so a `../` or `%` can never reach the fixed path).
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { ToolErrorCarrier } from "./errors.js";
|
|
46
|
+
import { getJson, driftError } from "./datasource.js";
|
|
47
|
+
import { num, str } from "./coerce.js";
|
|
48
|
+
import { withMeta, type MetaBundle, type ResponseMeta } from "./meta.js";
|
|
49
|
+
|
|
50
|
+
// ─── Fixed endpoint (SSRF core — compile-time CONSTANTS) ──────────
|
|
51
|
+
export const NHTSA_HOST = "api.nhtsa.gov";
|
|
52
|
+
const RECALLS_PATH = "/recalls/recallsByVehicle";
|
|
53
|
+
const COMPLAINTS_PATH = "/complaints/complaintsByVehicle";
|
|
54
|
+
// HOST+path-only labels (→ ToolError.upstreamEndpoint). Keyless ⇒ no token can
|
|
55
|
+
// ever appear here regardless, but the labels stay host+path for consistency.
|
|
56
|
+
const RECALLS_LABEL = "nhtsa:/recalls/recallsByVehicle";
|
|
57
|
+
const COMPLAINTS_LABEL = "nhtsa:/complaints/complaintsByVehicle";
|
|
58
|
+
|
|
59
|
+
// ─── Input validation grammar (SSRF + injection guard) ────────────
|
|
60
|
+
// modelYear: exactly 4 digits. make/model: letters/digits/space/hyphen only —
|
|
61
|
+
// rejects `../`, `%`, `/`, `.`, quotes, so a value can never break out of the
|
|
62
|
+
// URLSearchParams-encoded query onto the fixed host/path.
|
|
63
|
+
export const NHTSA_MODEL_YEAR_RE = /^\d{4}$/;
|
|
64
|
+
export const NHTSA_MAKE_MODEL_RE = /^[A-Za-z0-9 -]+$/;
|
|
65
|
+
|
|
66
|
+
const KEYLESS_NOTE =
|
|
67
|
+
"NHTSA is a keyless public API (api.nhtsa.gov) — no API key is required or accepted.";
|
|
68
|
+
const COMPLETE_SET_NOTE =
|
|
69
|
+
"NHTSA returns the COMPLETE set of matching records for this make/model/modelYear (no pagination) — totalAvailable is the upstream Count, and returned should equal it.";
|
|
70
|
+
|
|
71
|
+
// ─── Shared coercions ─────────────────────────────────────────────
|
|
72
|
+
/** A genuine boolean preserved; anything else ⇒ null (never a fabricated false). */
|
|
73
|
+
function bool(x: unknown): boolean | null {
|
|
74
|
+
return typeof x === "boolean" ? x : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── Shared input validation ──────────────────────────────────────
|
|
78
|
+
export type NhtsaVehicleArgs = {
|
|
79
|
+
make: string;
|
|
80
|
+
model: string;
|
|
81
|
+
modelYear: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Validate the shared make/model/modelYear inputs (belt-and-suspenders behind the
|
|
86
|
+
* server Zod; a DIRECT handler call bypasses Zod). Rejects a bad value PRE-fetch
|
|
87
|
+
* (0 network call) so a `../`/`%` can never reach the fixed host/path.
|
|
88
|
+
*/
|
|
89
|
+
function validateVehicleArgs(args: NhtsaVehicleArgs, label: string): void {
|
|
90
|
+
const checks: Array<[string, string, RegExp]> = [
|
|
91
|
+
["make", args.make, NHTSA_MAKE_MODEL_RE],
|
|
92
|
+
["model", args.model, NHTSA_MAKE_MODEL_RE],
|
|
93
|
+
["modelYear", args.modelYear, NHTSA_MODEL_YEAR_RE],
|
|
94
|
+
];
|
|
95
|
+
for (const [name, value, re] of checks) {
|
|
96
|
+
if (typeof value !== "string" || !re.test(value)) {
|
|
97
|
+
throw new ToolErrorCarrier({
|
|
98
|
+
kind: "invalid_input",
|
|
99
|
+
retryable: false,
|
|
100
|
+
message:
|
|
101
|
+
name === "modelYear"
|
|
102
|
+
? `Invalid modelYear ${JSON.stringify(value)} — expected a 4-digit year (^\\d{4}$), e.g. "2020".`
|
|
103
|
+
: `Invalid ${name} ${JSON.stringify(value)} — expected letters/digits/space/hyphen only (^[A-Za-z0-9 -]+$), e.g. "honda".`,
|
|
104
|
+
upstreamEndpoint: label,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── SSRF-guarded fetch (fixed host + hostname assertion + redirect) ──
|
|
111
|
+
/**
|
|
112
|
+
* GET one NHTSA JSON resource on the FIXED host. Builds
|
|
113
|
+
* `https://api.nhtsa.gov${path}?${params}`, asserts the CONSTRUCTED URL's
|
|
114
|
+
* hostname === the fixed host over https (belt-and-suspenders), and sets
|
|
115
|
+
* `redirect:"error"` (fail closed on any off-host 3xx). Keyless — no header/token.
|
|
116
|
+
*/
|
|
117
|
+
async function getNhtsa(
|
|
118
|
+
path: string,
|
|
119
|
+
label: string,
|
|
120
|
+
params: URLSearchParams,
|
|
121
|
+
): Promise<unknown> {
|
|
122
|
+
const url = `https://${NHTSA_HOST}${path}?${params.toString()}`;
|
|
123
|
+
const built = new URL(url);
|
|
124
|
+
if (built.hostname !== NHTSA_HOST || built.protocol !== "https:") {
|
|
125
|
+
throw new ToolErrorCarrier({
|
|
126
|
+
kind: "invalid_input",
|
|
127
|
+
retryable: false,
|
|
128
|
+
message: `Constructed NHTSA URL host ${JSON.stringify(built.hostname)} (${built.protocol}) does not match the fixed host ${JSON.stringify(NHTSA_HOST)} over https — refusing to fetch (SSRF safety).`,
|
|
129
|
+
upstreamEndpoint: label,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return getJson(url, { label, redirect: "error" });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Build the shared make/model/modelYear query (module-built; no raw passthrough). */
|
|
136
|
+
function vehicleParams(args: NhtsaVehicleArgs): URLSearchParams {
|
|
137
|
+
const params = new URLSearchParams();
|
|
138
|
+
params.set("make", args.make);
|
|
139
|
+
params.set("model", args.model);
|
|
140
|
+
params.set("modelYear", args.modelYear);
|
|
141
|
+
return params;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Fetch + parse a NHTSA resource, mirroring datagov-catalog's catch-ladder: a
|
|
146
|
+
* ToolErrorCarrier (host-assert / 4xx-5xx taxonomy) rethrows FIRST (preserving its
|
|
147
|
+
* kind); a 200 non-JSON `.json()` SyntaxError reclassifies to schema_drift; a bare
|
|
148
|
+
* error rethrows LAST.
|
|
149
|
+
*/
|
|
150
|
+
async function fetchNhtsa(
|
|
151
|
+
path: string,
|
|
152
|
+
label: string,
|
|
153
|
+
args: NhtsaVehicleArgs,
|
|
154
|
+
): Promise<unknown> {
|
|
155
|
+
try {
|
|
156
|
+
return await getNhtsa(path, label, vehicleParams(args));
|
|
157
|
+
} catch (e) {
|
|
158
|
+
if (e instanceof ToolErrorCarrier) throw e;
|
|
159
|
+
if (e instanceof SyntaxError)
|
|
160
|
+
throw driftError(
|
|
161
|
+
label,
|
|
162
|
+
`NHTSA ${label} returned a non-JSON body at HTTP 200 — schema drift (never read as an empty result).`,
|
|
163
|
+
);
|
|
164
|
+
throw e;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Resolve the total from a Count/count field (P1/P4). A PRESENT numeric value is
|
|
170
|
+
* trusted verbatim; a MISSING (undefined/null) value falls back to results.length
|
|
171
|
+
* WITH an honest note (never fabricated); a PRESENT non-number ⇒ driftError (a
|
|
172
|
+
* broken total contract). Returns the total + the fallback flag.
|
|
173
|
+
*/
|
|
174
|
+
function resolveTotal(
|
|
175
|
+
rawCount: unknown,
|
|
176
|
+
returned: number,
|
|
177
|
+
label: string,
|
|
178
|
+
): { total: number; fellBack: boolean } {
|
|
179
|
+
if (rawCount === undefined || rawCount === null) {
|
|
180
|
+
// P1 fallback — missing Count ⇒ results.length + an honest note.
|
|
181
|
+
return { total: returned, fellBack: true };
|
|
182
|
+
}
|
|
183
|
+
const n = num(rawCount);
|
|
184
|
+
if (n === null) {
|
|
185
|
+
// P4 — a PRESENT non-number Count is a broken contract, never a fake empty.
|
|
186
|
+
throw driftError(
|
|
187
|
+
label,
|
|
188
|
+
`NHTSA ${label} shape drift — the total count field is present but non-numeric.`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return { total: n, fellBack: false };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ─── Curated row shapes ───────────────────────────────────────────
|
|
195
|
+
export type NhtsaRecall = {
|
|
196
|
+
campaignNumber: string | null;
|
|
197
|
+
manufacturer: string | null;
|
|
198
|
+
component: string | null;
|
|
199
|
+
summary: string | null;
|
|
200
|
+
consequence: string | null;
|
|
201
|
+
remedy: string | null;
|
|
202
|
+
reportReceivedDate: string | null;
|
|
203
|
+
parkIt: boolean | null;
|
|
204
|
+
parkOutside: boolean | null;
|
|
205
|
+
overTheAirUpdate: boolean | null;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export type NhtsaComplaint = {
|
|
209
|
+
odiNumber: string | null;
|
|
210
|
+
manufacturer: string | null;
|
|
211
|
+
component: string | null;
|
|
212
|
+
summary: string | null;
|
|
213
|
+
crash: boolean | null;
|
|
214
|
+
fire: boolean | null;
|
|
215
|
+
numberOfInjuries: number | null;
|
|
216
|
+
numberOfDeaths: number | null;
|
|
217
|
+
dateOfIncident: string | null;
|
|
218
|
+
dateComplaintFiled: string | null;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/** Map ONE /recallsByVehicle row → the curated recall shape. Booleans via `bool`. */
|
|
222
|
+
function mapRecall(row: unknown): NhtsaRecall {
|
|
223
|
+
const r = (row ?? {}) as Record<string, unknown>;
|
|
224
|
+
return {
|
|
225
|
+
campaignNumber: str(r.NHTSACampaignNumber),
|
|
226
|
+
manufacturer: str(r.Manufacturer),
|
|
227
|
+
component: str(r.Component),
|
|
228
|
+
summary: str(r.Summary),
|
|
229
|
+
consequence: str(r.Consequence),
|
|
230
|
+
remedy: str(r.Remedy),
|
|
231
|
+
reportReceivedDate: str(r.ReportReceivedDate),
|
|
232
|
+
parkIt: bool(r.parkIt),
|
|
233
|
+
parkOutside: bool(r.parkOutSide),
|
|
234
|
+
overTheAirUpdate: bool(r.overTheAirUpdate),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Map ONE /complaintsByVehicle row → the curated complaint shape. ★The `vin` field
|
|
240
|
+
* is DELIBERATELY OMITTED (PII — never read into the output). Counts via `num` (a
|
|
241
|
+
* genuine 0 stays 0); booleans via `bool`.
|
|
242
|
+
*/
|
|
243
|
+
function mapComplaint(row: unknown): NhtsaComplaint {
|
|
244
|
+
const r = (row ?? {}) as Record<string, unknown>;
|
|
245
|
+
return {
|
|
246
|
+
odiNumber: str(r.odiNumber),
|
|
247
|
+
manufacturer: str(r.manufacturer),
|
|
248
|
+
component: str(r.components),
|
|
249
|
+
summary: str(r.summary),
|
|
250
|
+
crash: bool(r.crash),
|
|
251
|
+
fire: bool(r.fire),
|
|
252
|
+
numberOfInjuries: num(r.numberOfInjuries),
|
|
253
|
+
numberOfDeaths: num(r.numberOfDeaths),
|
|
254
|
+
dateOfIncident: str(r.dateOfIncident),
|
|
255
|
+
dateComplaintFiled: str(r.dateComplaintFiled),
|
|
256
|
+
// ★ NO vin — the PII field is never surfaced, logged, or stored.
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const FILTERS_APPLIED = ["make", "model", "modelYear"];
|
|
261
|
+
|
|
262
|
+
// ─── Tool: nhtsa_recalls ──────────────────────────────────────────
|
|
263
|
+
/**
|
|
264
|
+
* Fetch NHTSA safety RECALLS for a make/model/modelYear → curated recall rows +
|
|
265
|
+
* honest `_meta`. KEYLESS. totalAvailable = the upstream `Count` (the REAL total —
|
|
266
|
+
* NHTSA returns the complete set, no pagination). A no-match (Count 0) ⇒ an honest
|
|
267
|
+
* empty; a 4xx ⇒ invalid_input; a 5xx/timeout ⇒ THROW; a 200 non-JSON ⇒ drift.
|
|
268
|
+
*/
|
|
269
|
+
export async function recalls(args: NhtsaVehicleArgs): Promise<MetaBundle> {
|
|
270
|
+
validateVehicleArgs(args, RECALLS_LABEL);
|
|
271
|
+
const body = await fetchNhtsa(RECALLS_PATH, RECALLS_LABEL, args);
|
|
272
|
+
|
|
273
|
+
const b = (body ?? {}) as { Count?: unknown; results?: unknown };
|
|
274
|
+
// P4 — results MUST be an array (a missing/string/null results is drift).
|
|
275
|
+
if (!Array.isArray(b.results)) {
|
|
276
|
+
throw driftError(
|
|
277
|
+
RECALLS_LABEL,
|
|
278
|
+
`NHTSA ${RECALLS_LABEL} shape drift — results must be an array.`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
const recalls = (b.results as unknown[]).map(mapRecall);
|
|
282
|
+
const returned = recalls.length;
|
|
283
|
+
const { total, fellBack } = resolveTotal(b.Count, returned, RECALLS_LABEL);
|
|
284
|
+
|
|
285
|
+
const notes: string[] = [KEYLESS_NOTE, COMPLETE_SET_NOTE];
|
|
286
|
+
if (fellBack)
|
|
287
|
+
notes.push(
|
|
288
|
+
"NHTSA did not report a Count field — totalAvailable falls back to the number of returned rows (results.length); the true total may differ.",
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
return withMeta(
|
|
292
|
+
{ recalls },
|
|
293
|
+
{
|
|
294
|
+
source: `${NHTSA_HOST} /recalls/recallsByVehicle (NHTSA vehicle safety recalls; keyless)`,
|
|
295
|
+
keylessMode: true,
|
|
296
|
+
returned,
|
|
297
|
+
totalAvailable: total,
|
|
298
|
+
filtersApplied: FILTERS_APPLIED,
|
|
299
|
+
filtersDropped: [],
|
|
300
|
+
fieldsUnavailable: [],
|
|
301
|
+
notes,
|
|
302
|
+
} satisfies Partial<ResponseMeta>,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ─── Tool: nhtsa_complaints ───────────────────────────────────────
|
|
307
|
+
/**
|
|
308
|
+
* Fetch NHTSA consumer COMPLAINTS for a make/model/modelYear → curated complaint
|
|
309
|
+
* rows (★NO vin — PII omitted) + honest `_meta`. KEYLESS. totalAvailable = the
|
|
310
|
+
* upstream `count` (the REAL total). A no-match ⇒ honest empty; a 4xx ⇒
|
|
311
|
+
* invalid_input; a 5xx/timeout ⇒ THROW; a 200 non-JSON ⇒ drift.
|
|
312
|
+
*/
|
|
313
|
+
export async function complaints(args: NhtsaVehicleArgs): Promise<MetaBundle> {
|
|
314
|
+
validateVehicleArgs(args, COMPLAINTS_LABEL);
|
|
315
|
+
const body = await fetchNhtsa(COMPLAINTS_PATH, COMPLAINTS_LABEL, args);
|
|
316
|
+
|
|
317
|
+
const b = (body ?? {}) as { count?: unknown; results?: unknown };
|
|
318
|
+
// P4 — results MUST be an array (a missing/string/null results is drift).
|
|
319
|
+
if (!Array.isArray(b.results)) {
|
|
320
|
+
throw driftError(
|
|
321
|
+
COMPLAINTS_LABEL,
|
|
322
|
+
`NHTSA ${COMPLAINTS_LABEL} shape drift — results must be an array.`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const complaints = (b.results as unknown[]).map(mapComplaint);
|
|
326
|
+
const returned = complaints.length;
|
|
327
|
+
const { total, fellBack } = resolveTotal(b.count, returned, COMPLAINTS_LABEL);
|
|
328
|
+
|
|
329
|
+
const notes: string[] = [
|
|
330
|
+
KEYLESS_NOTE,
|
|
331
|
+
COMPLETE_SET_NOTE,
|
|
332
|
+
"The NHTSA complaint VIN (an individual vehicle identifier) is intentionally EXCLUDED from this output (PII). The B2G signal is the manufacturer/component/crash/fire/injury/death safety history.",
|
|
333
|
+
];
|
|
334
|
+
if (fellBack)
|
|
335
|
+
notes.push(
|
|
336
|
+
"NHTSA did not report a count field — totalAvailable falls back to the number of returned rows (results.length); the true total may differ.",
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
return withMeta(
|
|
340
|
+
{ complaints },
|
|
341
|
+
{
|
|
342
|
+
source: `${NHTSA_HOST} /complaints/complaintsByVehicle (NHTSA vehicle safety complaints; keyless)`,
|
|
343
|
+
keylessMode: true,
|
|
344
|
+
returned,
|
|
345
|
+
totalAvailable: total,
|
|
346
|
+
filtersApplied: FILTERS_APPLIED,
|
|
347
|
+
filtersDropped: [],
|
|
348
|
+
fieldsUnavailable: [],
|
|
349
|
+
notes,
|
|
350
|
+
} satisfies Partial<ResponseMeta>,
|
|
351
|
+
);
|
|
352
|
+
}
|