@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.
Files changed (91) hide show
  1. package/README.ja.md +22 -9
  2. package/README.ko.md +22 -9
  3. package/README.md +70 -12
  4. package/dist/bea.d.ts +105 -0
  5. package/dist/bea.d.ts.map +1 -0
  6. package/dist/bea.js +303 -0
  7. package/dist/bea.js.map +1 -0
  8. package/dist/census-economic.d.ts +1 -1
  9. package/dist/census-economic.d.ts.map +1 -1
  10. package/dist/census-economic.js +12 -6
  11. package/dist/census-economic.js.map +1 -1
  12. package/dist/cms-facility.d.ts +112 -0
  13. package/dist/cms-facility.d.ts.map +1 -0
  14. package/dist/cms-facility.js +311 -0
  15. package/dist/cms-facility.js.map +1 -0
  16. package/dist/cms-hospital.d.ts +105 -0
  17. package/dist/cms-hospital.d.ts.map +1 -0
  18. package/dist/cms-hospital.js +290 -0
  19. package/dist/cms-hospital.js.map +1 -0
  20. package/dist/cms-supplier.d.ts +133 -0
  21. package/dist/cms-supplier.d.ts.map +1 -0
  22. package/dist/cms-supplier.js +414 -0
  23. package/dist/cms-supplier.js.map +1 -0
  24. package/dist/cms-utilization.d.ts +113 -0
  25. package/dist/cms-utilization.d.ts.map +1 -0
  26. package/dist/cms-utilization.js +328 -0
  27. package/dist/cms-utilization.js.map +1 -0
  28. package/dist/courtlistener.d.ts +115 -0
  29. package/dist/courtlistener.d.ts.map +1 -0
  30. package/dist/courtlistener.js +398 -0
  31. package/dist/courtlistener.js.map +1 -0
  32. package/dist/cpsc.d.ts +81 -0
  33. package/dist/cpsc.d.ts.map +1 -0
  34. package/dist/cpsc.js +283 -0
  35. package/dist/cpsc.js.map +1 -0
  36. package/dist/dol.d.ts +118 -0
  37. package/dist/dol.d.ts.map +1 -0
  38. package/dist/dol.js +421 -0
  39. package/dist/dol.js.map +1 -0
  40. package/dist/epa-envirofacts.d.ts +97 -0
  41. package/dist/epa-envirofacts.d.ts.map +1 -0
  42. package/dist/epa-envirofacts.js +292 -0
  43. package/dist/epa-envirofacts.js.map +1 -0
  44. package/dist/fred.d.ts +1 -1
  45. package/dist/fred.js +1 -1
  46. package/dist/keys.d.ts +11 -8
  47. package/dist/keys.d.ts.map +1 -1
  48. package/dist/keys.js +55 -8
  49. package/dist/keys.js.map +1 -1
  50. package/dist/lda.d.ts +105 -0
  51. package/dist/lda.d.ts.map +1 -0
  52. package/dist/lda.js +317 -0
  53. package/dist/lda.js.map +1 -0
  54. package/dist/nhtsa.d.ts +91 -0
  55. package/dist/nhtsa.d.ts.map +1 -0
  56. package/dist/nhtsa.js +263 -0
  57. package/dist/nhtsa.js.map +1 -0
  58. package/dist/nonprofit.d.ts +116 -0
  59. package/dist/nonprofit.d.ts.map +1 -0
  60. package/dist/nonprofit.js +342 -0
  61. package/dist/nonprofit.js.map +1 -0
  62. package/dist/openfda-device.d.ts +85 -0
  63. package/dist/openfda-device.d.ts.map +1 -0
  64. package/dist/openfda-device.js +277 -0
  65. package/dist/openfda-device.js.map +1 -0
  66. package/dist/openfda.d.ts +133 -0
  67. package/dist/openfda.d.ts.map +1 -0
  68. package/dist/openfda.js +402 -0
  69. package/dist/openfda.js.map +1 -0
  70. package/dist/server.d.ts.map +1 -1
  71. package/dist/server.js +872 -6
  72. package/dist/server.js.map +1 -1
  73. package/package.json +2 -1
  74. package/src/bea.ts +372 -0
  75. package/src/census-economic.ts +12 -6
  76. package/src/cms-facility.ts +379 -0
  77. package/src/cms-hospital.ts +344 -0
  78. package/src/cms-supplier.ts +527 -0
  79. package/src/cms-utilization.ts +389 -0
  80. package/src/courtlistener.ts +465 -0
  81. package/src/cpsc.ts +333 -0
  82. package/src/dol.ts +515 -0
  83. package/src/epa-envirofacts.ts +342 -0
  84. package/src/fred.ts +1 -1
  85. package/src/keys.ts +60 -8
  86. package/src/lda.ts +385 -0
  87. package/src/nhtsa.ts +352 -0
  88. package/src/nonprofit.ts +460 -0
  89. package/src/openfda-device.ts +356 -0
  90. package/src/openfda.ts +495 -0
  91. package/src/server.ts +995 -6
@@ -0,0 +1,356 @@
1
+ /**
2
+ * openfda-device.ts — openFDA 510(k) DEVICE CLEARANCES (api.fda.gov) — the
3
+ * MEDICAL-DEVICE REGULATORY lane (ADR-0056). The FDA's premarket-notification
4
+ * (510(k)) clearances: the applicant, the device, the clearance number (K-number),
5
+ * the decision (date + description), the clearance type, the product code, the
6
+ * advisory committee, and geography.
7
+ *
8
+ * ★ THIS IS THE SAME SOURCE + ENVELOPE + CRUX as openfda.ts (ADR-0054). Same host
9
+ * (api.fda.gov), same response envelope `{ meta:{ results:{ skip, limit, total }},
10
+ * results:[…] }`, same ★no-match→HTTP-404-NOT_FOUND-as-honest-empty crux, same
11
+ * optional query-key K-test, same fixed-host SSRF idiom, same structured-only
12
+ * (no raw Lucene passthrough) search assembly. It REUSES openfda.ts's `fetchOpenfda`
13
+ * (the SSRF-guarded classified fetch) and `readOpenfdaError` (the error-body reader)
14
+ * verbatim — the 404-reclassification logic is NOT reinvented — plus `luceneQuote`
15
+ * (the phrase-escape), `openfdaApiKey` (the optional-key env seam), and `OPENFDA_HOST`.
16
+ *
17
+ * GET https://api.fda.gov/device/510k.json
18
+ * ?search=<lucene>&limit=<1..100>&skip=<offset>[&api_key=<KEY>]
19
+ * → { meta:{ disclaimer, results:{ skip, limit, total }}, results:[ {…} ] }
20
+ * (live total ~175507).
21
+ *
22
+ * ★ HONESTY (mirrors openfda.ts exactly):
23
+ * [P1] totalAvailable = `meta.results.total` EXACT (e.g. 175507), NEVER
24
+ * results.length. skip/limit offset pagination:
25
+ * hasMore = skip + returned < total; nextOffset = hasMore ? skip+returned : null.
26
+ * [★P2] a 404 whose body is `{error:{code:"NOT_FOUND"}}` (a no-match query OR an
27
+ * unknown field) ⇒ HONEST EMPTY (returned:0, totalAvailable:0) — NOT thrown,
28
+ * NOT not_found. Any OTHER 4xx (e.g. a 400 syntax error) ⇒ invalid_input
29
+ * surfacing openFDA's message. 5xx/timeout ⇒ upstream_unavailable THROW. A
30
+ * 200 non-JSON body ⇒ schema_drift. (Reuses openfda.ts's exact fetch path.)
31
+ * [P3] dates (`decision_date`, YYYY-MM-DD) and every scalar surfaced as a STRING
32
+ * via `str` (null-never-empty-string) — no numeric coercion; never fabricated.
33
+ * [P4] `meta.results` or `results` absent / non-array ⇒ driftError (never a
34
+ * fabricated empty).
35
+ * [K-test] OPTIONAL OPENFDA_API_KEY: when set it rides `&api_key=` ONLY (openFDA has
36
+ * no header option — the query-key is inherent). Mitigation: the `label` is
37
+ * host+path ONLY (`openfda:/device/510k`, NO query), so no token reaches
38
+ * ToolError.upstreamEndpoint; `_meta.source` names the MODE only; the key is
39
+ * ABSENT from the serialized {data,_meta}, notes, and any log. Unset ⇒ keyless.
40
+ * [SSRF] fixed host `api.fda.gov` (fetchOpenfda asserts hostname/protocol +
41
+ * redirect:"error"); all filter VALUES are Lucene-escaped + phrase-quoted and
42
+ * ride URLSearchParams `search=`; limit/skip are integers; state is charclass
43
+ * `^[A-Za-z]{2}$`. No raw Lucene passthrough — structured only, injection-safe.
44
+ */
45
+
46
+ import { ToolErrorCarrier, errorFromResponse } from "./errors.js";
47
+ import { driftError } from "./datasource.js";
48
+ import { str } from "./coerce.js";
49
+ import { withMeta, type MetaBundle, type ResponseMeta } from "./meta.js";
50
+ import {
51
+ OPENFDA_HOST,
52
+ fetchOpenfda,
53
+ readOpenfdaError,
54
+ luceneQuote,
55
+ openfdaApiKey,
56
+ } from "./openfda.js";
57
+
58
+ // state filter charclass (a 2-letter US state/territory postal code).
59
+ const STATE_RE = /^[A-Za-z]{2}$/;
60
+
61
+ const DEFAULT_LIMIT = 25;
62
+ const MAX_LIMIT = 100;
63
+
64
+ /** host+path-only label (→ ToolError.upstreamEndpoint); NEVER carries the key. */
65
+ const DEVICE_LABEL = "openfda:/device/510k";
66
+
67
+ // ─── Honesty notes (ADR-0056 required set) ────────────────────────
68
+ const NOT_DETERMINATION_NOTE =
69
+ "openFDA 510(k) device clearances are FDA-published premarket-notification records; treat the decision/date/clearance-type as of the source's last publication. This is reference data, not a live regulatory determination.";
70
+ const KEYLESS_NOTE =
71
+ "Keyless: no OPENFDA_API_KEY is set. openFDA allows ~1000 requests/day without a key; a free key raises the rate limit (get one at https://open.fda.gov/apis/authentication/).";
72
+ const KEYED_NOTE =
73
+ "OPENFDA_API_KEY is set — it rides ONLY the &api_key= query parameter to api.fda.gov (openFDA has no header option), raising the rate limit. Its value is NEVER logged, echoed, or placed in this response.";
74
+ const NO_FILTER_NOTE =
75
+ "No structured filters were applied — this is an unscoped scan of the WHOLE 510(k) device-clearance collection. Add applicant / deviceName / productCode / clearanceType / kNumber / state to scope the result set.";
76
+
77
+ // ─── Curated clearance row shape ──────────────────────────────────
78
+ export type OpenfdaClearance = {
79
+ applicant: string | null;
80
+ deviceName: string | null;
81
+ kNumber: string | null; // the 510(k) clearance number (K-number)
82
+ decisionDate: string | null; // YYYY-MM-DD — preserved as a STRING (P3)
83
+ decisionDescription: string | null;
84
+ clearanceType: string | null;
85
+ productCode: string | null;
86
+ advisoryCommittee: string | null;
87
+ state: string | null;
88
+ };
89
+
90
+ /** Map ONE openFDA 510(k) result row → the curated shape. Every scalar via `str`. */
91
+ function mapClearance(row: unknown): OpenfdaClearance {
92
+ const r = (row ?? {}) as Record<string, unknown>;
93
+ return {
94
+ applicant: str(r.applicant),
95
+ deviceName: str(r.device_name),
96
+ kNumber: str(r.k_number),
97
+ decisionDate: str(r.decision_date),
98
+ decisionDescription: str(r.decision_description),
99
+ clearanceType: str(r.clearance_type),
100
+ productCode: str(r.product_code),
101
+ advisoryCommittee: str(r.advisory_committee),
102
+ state: str(r.state),
103
+ };
104
+ }
105
+
106
+ // ─── Lucene search assembly (structured-only; injection-safe) ─────
107
+ /** The structured filter set → openFDA `field:value` clauses (510(k) fields). */
108
+ export type OpenfdaDeviceFilters = {
109
+ applicant?: string; // → applicant
110
+ deviceName?: string; // → device_name
111
+ productCode?: string; // → product_code
112
+ clearanceType?: string; // → clearance_type
113
+ kNumber?: string; // → k_number
114
+ state?: string; // → state (2-letter)
115
+ };
116
+
117
+ /**
118
+ * Assemble the openFDA `search=` Lucene string from structured 510(k) filters — each
119
+ * value Lucene-escaped + phrase-quoted (reusing openfda.ts's `luceneQuote`), joined by
120
+ * ` AND `. Returns "" when no filter is present (openFDA then returns the whole
121
+ * collection). The clause → field mapping is FIXED here; a caller can never inject a
122
+ * raw field:value (no raw Lucene passthrough).
123
+ */
124
+ export function buildDeviceSearch(f: OpenfdaDeviceFilters): string {
125
+ const clauses: string[] = [];
126
+ if (f.applicant !== undefined)
127
+ clauses.push(`applicant:${luceneQuote(f.applicant)}`);
128
+ if (f.deviceName !== undefined)
129
+ clauses.push(`device_name:${luceneQuote(f.deviceName)}`);
130
+ if (f.productCode !== undefined)
131
+ clauses.push(`product_code:${luceneQuote(f.productCode)}`);
132
+ if (f.clearanceType !== undefined)
133
+ clauses.push(`clearance_type:${luceneQuote(f.clearanceType)}`);
134
+ if (f.kNumber !== undefined)
135
+ clauses.push(`k_number:${luceneQuote(f.kNumber)}`);
136
+ if (f.state !== undefined)
137
+ clauses.push(`state:${luceneQuote(f.state.toUpperCase())}`);
138
+ return clauses.join(" AND ");
139
+ }
140
+
141
+ // ─── Tool: openfda_device_clearances ──────────────────────────────
142
+ export type OpenfdaDeviceClearancesArgs = OpenfdaDeviceFilters & {
143
+ limit?: number; // 1..100 (default 25)
144
+ skip?: number; // offset ≥ 0 (default 0)
145
+ };
146
+
147
+ /**
148
+ * Search openFDA 510(k) DEVICE CLEARANCES with structured filters → curated clearance
149
+ * rows + honest `_meta`. KEYLESS (an OPTIONAL OPENFDA_API_KEY only raises the rate
150
+ * limit). totalAvailable = meta.results.total (EXACT); skip/limit offset pagination.
151
+ * ★A no-match query (openFDA HTTP 404 NOT_FOUND) ⇒ an honest empty, never a throw.
152
+ */
153
+ export async function deviceClearances(
154
+ args: OpenfdaDeviceClearancesArgs,
155
+ ): Promise<MetaBundle> {
156
+ // ── Validate + default the inputs (belt-and-suspenders behind the server Zod; a
157
+ // DIRECT handler call bypasses Zod). ──
158
+ if (args.state !== undefined && !STATE_RE.test(args.state)) {
159
+ throw new ToolErrorCarrier({
160
+ kind: "invalid_input",
161
+ retryable: false,
162
+ message: `Invalid state ${JSON.stringify(args.state)} — expected a 2-letter US state/territory postal code (^[A-Za-z]{2}$), e.g. "CA".`,
163
+ upstreamEndpoint: DEVICE_LABEL,
164
+ });
165
+ }
166
+ const limit = clampLimit(args.limit);
167
+ const skip = clampSkip(args.skip);
168
+
169
+ // ── Assemble the query (structured-only; all VALUES via URLSearchParams — no
170
+ // host/path steer). The OPTIONAL key rides ONLY here in &api_key=. ──
171
+ const filters: OpenfdaDeviceFilters = {
172
+ applicant: args.applicant,
173
+ deviceName: args.deviceName,
174
+ productCode: args.productCode,
175
+ clearanceType: args.clearanceType,
176
+ kNumber: args.kNumber,
177
+ state: args.state,
178
+ };
179
+ const search = buildDeviceSearch(filters);
180
+ const filtersApplied: string[] = [];
181
+ if (args.applicant !== undefined) filtersApplied.push("applicant");
182
+ if (args.deviceName !== undefined) filtersApplied.push("deviceName");
183
+ if (args.productCode !== undefined) filtersApplied.push("productCode");
184
+ if (args.clearanceType !== undefined) filtersApplied.push("clearanceType");
185
+ if (args.kNumber !== undefined) filtersApplied.push("kNumber");
186
+ if (args.state !== undefined) filtersApplied.push("state");
187
+
188
+ const params = new URLSearchParams();
189
+ if (search !== "") params.set("search", search);
190
+ params.set("limit", String(limit));
191
+ params.set("skip", String(skip));
192
+ const key = openfdaApiKey();
193
+ if (key !== undefined) params.set("api_key", key); // OPTIONAL — &api_key= ONLY
194
+
195
+ const url = `https://${OPENFDA_HOST}/device/510k.json?${params.toString()}`;
196
+
197
+ // ── Fetch + classify. ★P2 CRUX: a 404 whose body is {error:{code:"NOT_FOUND"}} is
198
+ // a genuine no-match (or an unknown field) ⇒ an HONEST EMPTY, never a thrown
199
+ // not_found. Any OTHER 4xx (e.g. 400 syntax) ⇒ invalid_input surfacing openFDA's
200
+ // message; a 5xx/429 ⇒ the shared taxonomy THROWS. (fetchOpenfda/readOpenfdaError
201
+ // are the SAME helpers as openfda.ts — the 404-reclassify logic is not reinvented.) ──
202
+ const res = await fetchOpenfda(url, DEVICE_LABEL);
203
+
204
+ if (res.status === 404) {
205
+ const { code, message } = await readOpenfdaError(res);
206
+ if (code === "NOT_FOUND") {
207
+ // Honest empty — NOT thrown, NOT not_found (the openFDA no-match idiom).
208
+ return emptyResult(limit, skip, filtersApplied, key !== undefined);
209
+ }
210
+ // A non-NOT_FOUND 404 ⇒ the shared not_found taxonomy (never a fake-empty).
211
+ throw new ToolErrorCarrier({
212
+ ...errorFromResponse(res, DEVICE_LABEL),
213
+ message: message
214
+ ? `openFDA returned HTTP 404 at ${DEVICE_LABEL}: ${message}`
215
+ : `Resource not found at ${DEVICE_LABEL} (HTTP 404).`,
216
+ });
217
+ }
218
+
219
+ if (res.status >= 400 && res.status < 500 && res.status !== 429) {
220
+ // A 4xx OTHER than 404/429 (e.g. 400 syntax) ⇒ invalid_input surfacing the
221
+ // openFDA error message (a caller-fixable request, never a fake-empty).
222
+ const { message } = await readOpenfdaError(res);
223
+ throw new ToolErrorCarrier({
224
+ kind: "invalid_input",
225
+ retryable: false,
226
+ message: message
227
+ ? `openFDA rejected the request (HTTP ${res.status}) at ${DEVICE_LABEL}: ${message}`
228
+ : `Bad request (HTTP ${res.status}) at ${DEVICE_LABEL}.`,
229
+ upstreamStatus: res.status,
230
+ upstreamEndpoint: DEVICE_LABEL,
231
+ });
232
+ }
233
+
234
+ if (!res.ok) {
235
+ // 429 → rate_limited; 5xx → upstream_unavailable (a DOWN service is NEVER an empty
236
+ // result). Delegated to the shared errors.ts taxonomy.
237
+ throw new ToolErrorCarrier(errorFromResponse(res, DEVICE_LABEL));
238
+ }
239
+
240
+ // ── 200 ⇒ parse JSON; a non-JSON body ⇒ r.json() SyntaxError ⇒ schema_drift (never
241
+ // read as an empty result). ──
242
+ let body: unknown;
243
+ try {
244
+ body = await res.json();
245
+ } catch (e) {
246
+ if (e instanceof SyntaxError) {
247
+ throw driftError(
248
+ DEVICE_LABEL,
249
+ `openFDA ${DEVICE_LABEL} returned a non-JSON body at HTTP 200 — schema drift (never read as an empty result).`,
250
+ );
251
+ }
252
+ throw e;
253
+ }
254
+
255
+ // ── [P4] meta.results (the total carrier) and results (the rows) MUST be present +
256
+ // well-shaped; anything else is drift, never a fabricated empty. ──
257
+ const b = (body ?? {}) as { meta?: unknown; results?: unknown };
258
+ const meta = (b.meta ?? {}) as { results?: unknown };
259
+ const metaResults = meta.results as { total?: unknown } | undefined;
260
+ if (
261
+ metaResults === undefined ||
262
+ metaResults === null ||
263
+ typeof metaResults !== "object"
264
+ ) {
265
+ throw driftError(
266
+ DEVICE_LABEL,
267
+ `openFDA ${DEVICE_LABEL} shape drift — meta.results (the skip/limit/total carrier) is missing.`,
268
+ );
269
+ }
270
+ if (!Array.isArray(b.results)) {
271
+ throw driftError(
272
+ DEVICE_LABEL,
273
+ `openFDA ${DEVICE_LABEL} shape drift — results must be an array.`,
274
+ );
275
+ }
276
+
277
+ const clearances = (b.results as unknown[]).map(mapClearance);
278
+ const returned = clearances.length;
279
+
280
+ // ── [P1] totalAvailable = meta.results.total EXACT (the REAL total), NEVER
281
+ // results.length. skip/limit offset pagination. ──
282
+ const rawTotal = metaResults.total;
283
+ const totalAvailable =
284
+ typeof rawTotal === "number" && Number.isFinite(rawTotal) ? rawTotal : null;
285
+ const hasMore = totalAvailable !== null && skip + returned < totalAvailable;
286
+ const nextOffset = hasMore ? skip + returned : null;
287
+
288
+ const notes: string[] = [NOT_DETERMINATION_NOTE, keyNote(key !== undefined)];
289
+ if (filtersApplied.length === 0) notes.push(NO_FILTER_NOTE);
290
+
291
+ return withMeta(
292
+ { clearances },
293
+ {
294
+ // MODE only — never the key value (K-test).
295
+ source: `${OPENFDA_HOST} /device/510k (openFDA 510(k) device clearances; ${
296
+ key !== undefined ? "OPENFDA_API_KEY rate-limit key applied" : "keyless"
297
+ })`,
298
+ keylessMode: true, // a keyless tool; the optional key only raises the rate limit
299
+ returned,
300
+ totalAvailable,
301
+ filtersApplied,
302
+ filtersDropped: [],
303
+ fieldsUnavailable: [],
304
+ pagination: { offset: skip, limit, hasMore, nextOffset },
305
+ notes,
306
+ } satisfies Partial<ResponseMeta>,
307
+ );
308
+ }
309
+
310
+ // ─── Small helpers ────────────────────────────────────────────────
311
+ function keyNote(hasKey: boolean): string {
312
+ return hasKey ? KEYED_NOTE : KEYLESS_NOTE;
313
+ }
314
+
315
+ /** An honest empty result (★P2: a 404 NOT_FOUND no-match) — returned:0, total:0. */
316
+ function emptyResult(
317
+ limit: number,
318
+ skip: number,
319
+ filtersApplied: string[],
320
+ hasKey: boolean,
321
+ ): MetaBundle {
322
+ return withMeta(
323
+ { clearances: [] as OpenfdaClearance[] },
324
+ {
325
+ source: `${OPENFDA_HOST} /device/510k (openFDA 510(k) device clearances; ${
326
+ hasKey ? "OPENFDA_API_KEY rate-limit key applied" : "keyless"
327
+ })`,
328
+ keylessMode: true,
329
+ returned: 0,
330
+ totalAvailable: 0,
331
+ filtersApplied,
332
+ filtersDropped: [],
333
+ fieldsUnavailable: [],
334
+ pagination: { offset: skip, limit, hasMore: false, nextOffset: null },
335
+ notes: [
336
+ "No 510(k) device clearances matched this query (openFDA returned HTTP 404 NOT_FOUND — the source's honest no-match). This is an exact empty, not an error.",
337
+ NOT_DETERMINATION_NOTE,
338
+ keyNote(hasKey),
339
+ ],
340
+ } satisfies Partial<ResponseMeta>,
341
+ );
342
+ }
343
+
344
+ function clampLimit(v: unknown): number {
345
+ if (typeof v !== "number" || !Number.isFinite(v)) return DEFAULT_LIMIT;
346
+ const n = Math.floor(v);
347
+ if (n < 1) return 1;
348
+ if (n > MAX_LIMIT) return MAX_LIMIT;
349
+ return n;
350
+ }
351
+
352
+ function clampSkip(v: unknown): number {
353
+ if (typeof v !== "number" || !Number.isFinite(v)) return 0;
354
+ const n = Math.floor(v);
355
+ return n < 0 ? 0 : n;
356
+ }