@cliwant/mcp-sam-gov 0.3.0 → 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.
Files changed (228) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +48 -16
  3. package/README.ko.md +48 -16
  4. package/README.md +226 -64
  5. package/dist/attachments.d.ts +63 -0
  6. package/dist/attachments.d.ts.map +1 -0
  7. package/dist/attachments.js +576 -0
  8. package/dist/attachments.js.map +1 -0
  9. package/dist/bls.d.ts +267 -0
  10. package/dist/bls.d.ts.map +1 -0
  11. package/dist/bls.js +1549 -0
  12. package/dist/bls.js.map +1 -0
  13. package/dist/cache.d.ts +2 -0
  14. package/dist/cache.d.ts.map +1 -1
  15. package/dist/cache.js +4 -0
  16. package/dist/cache.js.map +1 -1
  17. package/dist/census.d.ts +182 -0
  18. package/dist/census.d.ts.map +1 -0
  19. package/dist/census.js +576 -0
  20. package/dist/census.js.map +1 -0
  21. package/dist/ckan.d.ts +141 -0
  22. package/dist/ckan.d.ts.map +1 -0
  23. package/dist/ckan.js +393 -0
  24. package/dist/ckan.js.map +1 -0
  25. package/dist/clinicaltrials.d.ts +180 -0
  26. package/dist/clinicaltrials.d.ts.map +1 -0
  27. package/dist/clinicaltrials.js +730 -0
  28. package/dist/clinicaltrials.js.map +1 -0
  29. package/dist/cms.d.ts +140 -0
  30. package/dist/cms.d.ts.map +1 -0
  31. package/dist/cms.js +482 -0
  32. package/dist/cms.js.map +1 -0
  33. package/dist/coerce.d.ts +32 -0
  34. package/dist/coerce.d.ts.map +1 -0
  35. package/dist/coerce.js +50 -0
  36. package/dist/coerce.js.map +1 -0
  37. package/dist/datagov-catalog.d.ts +84 -0
  38. package/dist/datagov-catalog.d.ts.map +1 -0
  39. package/dist/datagov-catalog.js +233 -0
  40. package/dist/datagov-catalog.js.map +1 -0
  41. package/dist/datagov.d.ts +146 -0
  42. package/dist/datagov.d.ts.map +1 -0
  43. package/dist/datagov.js +689 -0
  44. package/dist/datagov.js.map +1 -0
  45. package/dist/datagovKey.d.ts +36 -0
  46. package/dist/datagovKey.d.ts.map +1 -0
  47. package/dist/datagovKey.js +60 -0
  48. package/dist/datagovKey.js.map +1 -0
  49. package/dist/datasource.d.ts +313 -0
  50. package/dist/datasource.d.ts.map +1 -0
  51. package/dist/datasource.js +551 -0
  52. package/dist/datasource.js.map +1 -0
  53. package/dist/disclosure.d.ts +55 -0
  54. package/dist/disclosure.d.ts.map +1 -0
  55. package/dist/disclosure.js +57 -0
  56. package/dist/disclosure.js.map +1 -0
  57. package/dist/ecfr.d.ts +4 -2
  58. package/dist/ecfr.d.ts.map +1 -1
  59. package/dist/ecfr.js +74 -1
  60. package/dist/ecfr.js.map +1 -1
  61. package/dist/echo.d.ts +143 -0
  62. package/dist/echo.d.ts.map +1 -0
  63. package/dist/echo.js +424 -0
  64. package/dist/echo.js.map +1 -0
  65. package/dist/edgar.d.ts +377 -0
  66. package/dist/edgar.d.ts.map +1 -0
  67. package/dist/edgar.js +2418 -0
  68. package/dist/edgar.js.map +1 -0
  69. package/dist/errors.d.ts +26 -0
  70. package/dist/errors.d.ts.map +1 -1
  71. package/dist/errors.js +69 -1
  72. package/dist/errors.js.map +1 -1
  73. package/dist/fac.d.ts +180 -0
  74. package/dist/fac.d.ts.map +1 -0
  75. package/dist/fac.js +416 -0
  76. package/dist/fac.js.map +1 -0
  77. package/dist/far.d.ts +170 -0
  78. package/dist/far.d.ts.map +1 -0
  79. package/dist/far.js +804 -0
  80. package/dist/far.js.map +1 -0
  81. package/dist/fdic.d.ts +599 -0
  82. package/dist/fdic.d.ts.map +1 -0
  83. package/dist/fdic.js +1624 -0
  84. package/dist/fdic.js.map +1 -0
  85. package/dist/federal-register.d.ts +139 -2
  86. package/dist/federal-register.d.ts.map +1 -1
  87. package/dist/federal-register.js +419 -3
  88. package/dist/federal-register.js.map +1 -1
  89. package/dist/fema.d.ts +181 -0
  90. package/dist/fema.d.ts.map +1 -0
  91. package/dist/fema.js +436 -0
  92. package/dist/fema.js.map +1 -0
  93. package/dist/fpds.d.ts +108 -0
  94. package/dist/fpds.d.ts.map +1 -0
  95. package/dist/fpds.js +519 -0
  96. package/dist/fpds.js.map +1 -0
  97. package/dist/gao.d.ts +64 -0
  98. package/dist/gao.d.ts.map +1 -0
  99. package/dist/gao.js +640 -0
  100. package/dist/gao.js.map +1 -0
  101. package/dist/govinfo.d.ts +111 -0
  102. package/dist/govinfo.d.ts.map +1 -0
  103. package/dist/govinfo.js +422 -0
  104. package/dist/govinfo.js.map +1 -0
  105. package/dist/grants.d.ts +27 -4
  106. package/dist/grants.d.ts.map +1 -1
  107. package/dist/grants.js +111 -6
  108. package/dist/grants.js.map +1 -1
  109. package/dist/gsa-csv.d.ts +249 -0
  110. package/dist/gsa-csv.d.ts.map +1 -0
  111. package/dist/gsa-csv.js +784 -0
  112. package/dist/gsa-csv.js.map +1 -0
  113. package/dist/integrity.d.ts +212 -0
  114. package/dist/integrity.d.ts.map +1 -0
  115. package/dist/integrity.js +707 -0
  116. package/dist/integrity.js.map +1 -0
  117. package/dist/meta.d.ts +165 -0
  118. package/dist/meta.d.ts.map +1 -0
  119. package/dist/meta.js +162 -0
  120. package/dist/meta.js.map +1 -0
  121. package/dist/nih.d.ts +117 -0
  122. package/dist/nih.d.ts.map +1 -0
  123. package/dist/nih.js +291 -0
  124. package/dist/nih.js.map +1 -0
  125. package/dist/nppes.d.ts +157 -0
  126. package/dist/nppes.d.ts.map +1 -0
  127. package/dist/nppes.js +648 -0
  128. package/dist/nppes.js.map +1 -0
  129. package/dist/nsf.d.ts +176 -0
  130. package/dist/nsf.d.ts.map +1 -0
  131. package/dist/nsf.js +554 -0
  132. package/dist/nsf.js.map +1 -0
  133. package/dist/nvd.d.ts +176 -0
  134. package/dist/nvd.d.ts.map +1 -0
  135. package/dist/nvd.js +912 -0
  136. package/dist/nvd.js.map +1 -0
  137. package/dist/ofac.d.ts +205 -0
  138. package/dist/ofac.d.ts.map +1 -0
  139. package/dist/ofac.js +919 -0
  140. package/dist/ofac.js.map +1 -0
  141. package/dist/pricing.d.ts +110 -0
  142. package/dist/pricing.d.ts.map +1 -0
  143. package/dist/pricing.js +843 -0
  144. package/dist/pricing.js.map +1 -0
  145. package/dist/sam-gov/client.d.ts +60 -2
  146. package/dist/sam-gov/client.d.ts.map +1 -1
  147. package/dist/sam-gov/client.js +320 -54
  148. package/dist/sam-gov/client.js.map +1 -1
  149. package/dist/sam-gov/index.d.ts +1 -1
  150. package/dist/sam-gov/index.d.ts.map +1 -1
  151. package/dist/sam-gov/index.js +1 -1
  152. package/dist/sam-gov/index.js.map +1 -1
  153. package/dist/sam-gov/types.d.ts +24 -0
  154. package/dist/sam-gov/types.d.ts.map +1 -1
  155. package/dist/sba.d.ts +72 -0
  156. package/dist/sba.d.ts.map +1 -0
  157. package/dist/sba.js +281 -0
  158. package/dist/sba.js.map +1 -0
  159. package/dist/server.d.ts +14 -2
  160. package/dist/server.d.ts.map +1 -1
  161. package/dist/server.js +3889 -296
  162. package/dist/server.js.map +1 -1
  163. package/dist/snapshot.d.ts +98 -0
  164. package/dist/snapshot.d.ts.map +1 -0
  165. package/dist/snapshot.js +146 -0
  166. package/dist/snapshot.js.map +1 -0
  167. package/dist/socrata.d.ts +157 -0
  168. package/dist/socrata.d.ts.map +1 -0
  169. package/dist/socrata.js +448 -0
  170. package/dist/socrata.js.map +1 -0
  171. package/dist/treasury.d.ts +143 -0
  172. package/dist/treasury.d.ts.map +1 -0
  173. package/dist/treasury.js +436 -0
  174. package/dist/treasury.js.map +1 -0
  175. package/dist/usaspending.d.ts +260 -65
  176. package/dist/usaspending.d.ts.map +1 -1
  177. package/dist/usaspending.js +1629 -201
  178. package/dist/usaspending.js.map +1 -1
  179. package/dist/usitc.d.ts +142 -0
  180. package/dist/usitc.d.ts.map +1 -0
  181. package/dist/usitc.js +339 -0
  182. package/dist/usitc.js.map +1 -0
  183. package/package.json +24 -2
  184. package/src/attachments.ts +652 -0
  185. package/src/bls.ts +1943 -0
  186. package/src/cache.ts +5 -0
  187. package/src/census.ts +735 -0
  188. package/src/ckan.ts +495 -0
  189. package/src/clinicaltrials.ts +923 -0
  190. package/src/cms.ts +634 -0
  191. package/src/coerce.ts +47 -0
  192. package/src/datagov-catalog.ts +296 -0
  193. package/src/datagov.ts +907 -0
  194. package/src/datagovKey.ts +68 -0
  195. package/src/datasource.ts +721 -0
  196. package/src/disclosure.ts +61 -0
  197. package/src/ecfr.ts +98 -1
  198. package/src/echo.ts +496 -0
  199. package/src/edgar.ts +3014 -0
  200. package/src/errors.ts +84 -1
  201. package/src/fac.ts +529 -0
  202. package/src/far.ts +1007 -0
  203. package/src/fdic.ts +2052 -0
  204. package/src/federal-register.ts +512 -3
  205. package/src/fema.ts +541 -0
  206. package/src/fpds.ts +620 -0
  207. package/src/gao.ts +744 -0
  208. package/src/govinfo.ts +497 -0
  209. package/src/grants.ts +139 -7
  210. package/src/gsa-csv.ts +992 -0
  211. package/src/integrity.ts +928 -0
  212. package/src/meta.ts +292 -0
  213. package/src/nih.ts +375 -0
  214. package/src/nppes.ts +834 -0
  215. package/src/nsf.ts +706 -0
  216. package/src/nvd.ts +1124 -0
  217. package/src/ofac.ts +1166 -0
  218. package/src/pricing.ts +1075 -0
  219. package/src/sam-gov/client.ts +345 -63
  220. package/src/sam-gov/index.ts +5 -1
  221. package/src/sam-gov/types.ts +22 -0
  222. package/src/sba.ts +357 -0
  223. package/src/server.ts +4551 -328
  224. package/src/snapshot.ts +192 -0
  225. package/src/socrata.ts +532 -0
  226. package/src/treasury.ts +575 -0
  227. package/src/usaspending.ts +1946 -214
  228. package/src/usitc.ts +420 -0
@@ -0,0 +1,689 @@
1
+ /**
2
+ * datagov.ts — the api.data.gov KEYED trio (slice 1: Regulations.gov +
3
+ * Congress.gov). ADR-0007.
4
+ *
5
+ * This is the project's FIRST KEYED (non-keyless) source — every prior source
6
+ * (Treasury, EDGAR, Socrata, CKAN) is anonymous. It exercises policy② ("keyless-
7
+ * first, but when a free public key is required, research + automate its
8
+ * issuance"), so the load-bearing concern is a SAFE env-key adapter whose secret
9
+ * can NEVER leak into a ToolError, a URL, a log, or `_meta`. Built on the R2
10
+ * `DataSource` port (ADR-0005) exactly like CKAN — it writes ZERO fetch/coercion/
11
+ * error/meta code (reuses `getJson`/`driftError`/`num`·`str`/`withMeta`) and
12
+ * COPIES (does not import) the Socrata/CKAN SSRF + honesty PATTERN.
13
+ *
14
+ * ★ THE KEY-SECURITY DISCIPLINE (ADR-0007 §2 — the load-bearing rules):
15
+ * 1. The key travels in `headers:{ "X-Api-Key": <key> }` ONLY — NEVER in the
16
+ * URL/query (no `?api_key=`). Live-verified: the `X-Api-Key` header works for
17
+ * BOTH api.regulations.gov and api.congress.gov, so the key is structurally
18
+ * absent from the request URL (→ never in a CDN/proxy/access log, never in
19
+ * `getJson`'s `label`, never in `ToolError.upstreamEndpoint`).
20
+ * 2. `label` is HOST+PATH only (e.g. "regulations:/v4/documents",
21
+ * "congress:/v3/bill") — never the full URL, never a token. This is what
22
+ * reaches `ToolError.upstreamEndpoint`.
23
+ * 3. `_meta.source`/`notes` are host + key-MODE only ("…(DEMO_KEY)" /
24
+ * "…(DATA_GOV_API_KEY)") — never the URL, never the key value, never the
25
+ * `X-Api-Key` header.
26
+ * 4. Headers are never logged/echoed; upstream `next`/`pagination.next` URLs are
27
+ * NEVER surfaced verbatim — pagination is re-derived NUMERICALLY.
28
+ * 5. Never commit the key — read from env only; the DEMO_KEY fallback is a
29
+ * literal public constant (safe in source), the real key never is.
30
+ *
31
+ * ★ KEYLESS-FIRST UX (policy② / §3): DATA_GOV_API_KEY from env, else the public
32
+ * literal "DEMO_KEY" + a `_meta.notes` disclosure of the ~10 req/hr shared ceiling
33
+ * and the free-key signup path. `keylessMode:false` (this is the FIRST source to
34
+ * report it — it is genuinely keyed).
35
+ *
36
+ * ★ SSRF (§4): two fixed hosts (constants, no free host param); all caller params
37
+ * via URLSearchParams; path segments that interpolate caller input (Congress
38
+ * `/v3/bill/{congress}/{billType}/{billNumber}`) are Zod-constrained to int/enum;
39
+ * post-construction `hostname===host && https` assertion; `redirect:"error"`.
40
+ *
41
+ * ★ HONESTY (§5): totals are EXACT integers read from the PRIMARY container
42
+ * (`meta.totalElements` / `pagination.count`) with container guards (a null/absent
43
+ * container or a non-number total → `driftError`, NOT a TypeError/upstream_unavail);
44
+ * the primary array (`data`/`bills`) must be an array or → `driftError`; a
45
+ * genuine-empty (0 results) → complete:true/total:0; an outage/5xx/timeout throws
46
+ * (never a fake empty). Regulations.gov's 40-page/10,000-record HARD CAP is
47
+ * handled with the EDGAR window-cap pattern (B1): `hasMore:true` (more genuinely
48
+ * exists) BUT `nextOffset:null` (no reachable continuation) + a disclosing note.
49
+ */
50
+ import { ToolErrorCarrier } from "./errors.js";
51
+ import { getJson, driftError } from "./datasource.js";
52
+ import { num, str } from "./coerce.js";
53
+ import { withMeta } from "./meta.js";
54
+ // ADR-0010 §2 — the api.data.gov KEY seam is now a single audited home shared with
55
+ // govinfo.ts (the 2nd consumer). This is a pure behavior-identical extraction of
56
+ // datagov's former module-private key helpers (they read process.env at call time,
57
+ // so the move changes nothing — datagov's key handling / _meta / snapshot / K-test
58
+ // all stay green). govinfo.ts imports from the SAME module; neither imports the other.
59
+ import { keyHeader, keyModeLabel, pushKeyNote } from "./datagovKey.js";
60
+ // Re-export the shared honesty coercion (single audited copy in ./coerce.js —
61
+ // ADR-0005 v2 FIX-C) so the fault suite's num-parity guard resolves the SAME
62
+ // `num` from this module (datagov.num === coerce.num === ckan.num === …).
63
+ export { num };
64
+ // ─── Fixed hosts (SSRF core — no free host param) ─────────────────
65
+ export const REGULATIONS_HOST = "api.regulations.gov";
66
+ export const CONGRESS_HOST = "api.congress.gov";
67
+ // ─── Regulations.gov hard pagination cap (§1c / B1, EDGAR-pattern) ─
68
+ // page[number] is hard-capped at 40; the max reachable window is 40 × 250 =
69
+ // 10,000 records. `meta.totalElements` is the REAL total (~1.97M) — far larger.
70
+ // We never trust meta.totalPages/hasNextPage/lastPage (they are cap-relative
71
+ // liars). Mirrors edgar.ts's FTS_WINDOW / FTS_MAX_FROM window cap.
72
+ const REG_MAX_PAGE = 40;
73
+ const REG_MAX_PAGE_SIZE = 250;
74
+ const REG_MAX_RECORDS = REG_MAX_PAGE * REG_MAX_PAGE_SIZE; // 10_000
75
+ // Live-verified accepted Regulations.gov sort fields (non-exhaustive — the API
76
+ // accepts more; these are the ones confirmed with DEMO_KEY, ADR-0007 §1b/m8).
77
+ export const REGULATIONS_SORTS = [
78
+ "-postedDate",
79
+ "postedDate",
80
+ "-lastModifiedDate",
81
+ "lastModifiedDate",
82
+ "-commentEndDate",
83
+ ];
84
+ // Regulations.gov documentType facet (§4 rule 5 — Zod enum, bad values fail
85
+ // locally before fetch).
86
+ export const REGULATIONS_DOCUMENT_TYPES = [
87
+ "Rule",
88
+ "Proposed Rule",
89
+ "Notice",
90
+ "Supporting & Related Material",
91
+ "Other",
92
+ ];
93
+ // Regulations.gov docket-type facet (ADR-0044 §4 rule 5 — Zod enum; a bad value
94
+ // fails LOCALLY as invalid_input before any fetch). Dockets are the rulemaking/
95
+ // nonrulemaking CONTAINER that groups documents + comments under one action.
96
+ export const REGULATIONS_DOCKET_TYPES = ["Rulemaking", "Nonrulemaking"];
97
+ // Docket sort fields (ADR-0044 S5). `-lastModifiedDate` and `title` are
98
+ // DEMO_KEY-verified; `lastModifiedDate` (asc) and `-title` are assumed by JSON:API
99
+ // asc/desc symmetry — LIVE-VERIFY when a non-throttled key is available. An
100
+ // unattested value the API rejects yields an honest invalid_input THROW, so
101
+ // keeping them is safe (mirrors the REGULATIONS_SORTS non-exhaustive note above).
102
+ export const REGULATIONS_DOCKET_SORTS = [
103
+ "-lastModifiedDate",
104
+ "lastModifiedDate",
105
+ "title",
106
+ "-title",
107
+ ];
108
+ // Congress.gov bill-type path enum (§4 — constrains the /v3/bill/{congress}/{type}
109
+ // path segment; a bad value fails locally before any fetch).
110
+ export const CONGRESS_BILL_TYPES = [
111
+ "hr",
112
+ "s",
113
+ "hjres",
114
+ "sjres",
115
+ "hconres",
116
+ "sconres",
117
+ "hres",
118
+ "sres",
119
+ ];
120
+ // ─── Key handling (the load-bearing secret discipline) ────────────
121
+ // `keyHeader`/`keyModeLabel`/`pushKeyNote` (+ `usingDemoKey`, the DEMO_KEY literal,
122
+ // and the disclosure notes) now live in the SHARED `./datagovKey.js` seam (ADR-0010
123
+ // §2) — imported above, byte-identical behavior. GovInfo is their 2nd consumer, so
124
+ // the single-audited-home promotion mirrors the `coerce.ts` precedent for `num`.
125
+ // ─── SSRF-guarded fetch (§4 — fixed host + hostname assertion + redirect) ──
126
+ /**
127
+ * GET one api.data.gov JSON resource. `host` is a fixed module constant; `path`
128
+ * is a fixed base path (or a Zod-constrained interpolation); all caller params go
129
+ * through `params` (URLSearchParams, encoded). Asserts the CONSTRUCTED URL's
130
+ * hostname === host over https (belt-and-suspenders, copied from Socrata/CKAN),
131
+ * sets `redirect:"error"` (a 3xx off an api.data.gov host is anomalous and must
132
+ * NOT be followed — it could carry the X-Api-Key header to a foreign host), and
133
+ * attaches the key ONLY in the X-Api-Key header. `label` is host+path only.
134
+ */
135
+ async function getDatagov(host, path, label, params) {
136
+ const qs = params.toString();
137
+ const url = `https://${host}${path}${qs ? `?${qs}` : ""}`;
138
+ const built = new URL(url);
139
+ if (built.hostname !== host || built.protocol !== "https:") {
140
+ throw new ToolErrorCarrier({
141
+ kind: "invalid_input",
142
+ message: `Constructed api.data.gov URL host ${JSON.stringify(built.hostname)} (${built.protocol}) does not match the fixed host ${JSON.stringify(host)} over https — refusing to fetch (SSRF safety).`,
143
+ retryable: false,
144
+ });
145
+ }
146
+ // Shared fetch envelope (ADR-0005): the key rides in headers ONLY (§2 rule 1);
147
+ // redirect:"error" (§4); host-only label → ToolError.upstreamEndpoint (§2 rule 2).
148
+ return getJson(url, { label, headers: keyHeader(), redirect: "error" });
149
+ }
150
+ // ═══════════════════ Regulations.gov (JSON:API) ═══════════════════
151
+ const REG_DOC_SOURCE = (mode) => `${REGULATIONS_HOST} via Regulations.gov API (${mode})`;
152
+ /** Map one JSON:API `data[]` document into a flat, honesty-coerced row. */
153
+ function mapRegDocument(item) {
154
+ const it = (item ?? {});
155
+ const a = (it.attributes ?? {});
156
+ return {
157
+ id: str(it.id),
158
+ documentType: str(a.documentType),
159
+ title: str(a.title),
160
+ agencyId: str(a.agencyId),
161
+ docketId: str(a.docketId),
162
+ postedDate: str(a.postedDate),
163
+ commentStartDate: str(a.commentStartDate),
164
+ commentEndDate: str(a.commentEndDate),
165
+ openForComment: typeof a.openForComment === "boolean" ? a.openForComment : null,
166
+ withinCommentPeriod: typeof a.withinCommentPeriod === "boolean" ? a.withinCommentPeriod : null,
167
+ frDocNum: str(a.frDocNum),
168
+ objectId: str(a.objectId),
169
+ };
170
+ }
171
+ /** Map one JSON:API `data[]` comment into a flat, honesty-coerced row. */
172
+ function mapRegComment(item) {
173
+ const it = (item ?? {});
174
+ const a = (it.attributes ?? {});
175
+ return {
176
+ id: str(it.id),
177
+ documentType: str(a.documentType),
178
+ title: str(a.title),
179
+ agencyId: str(a.agencyId),
180
+ docketId: str(a.docketId),
181
+ postedDate: str(a.postedDate),
182
+ objectId: str(a.objectId),
183
+ };
184
+ }
185
+ /**
186
+ * The shared Regulations.gov JSON:API search core for `/v4/documents` and
187
+ * `/v4/comments` (identical envelope + identical page[number]≤40 hard cap).
188
+ *
189
+ * ⚠ CAP-VERIFICATION NOTE (m5): the `/v4/documents` 40-page cap (HTTP 400
190
+ * "Maximum value is 40") is LIVE-VERIFIED (ADR-0007 §1c). The `/v4/comments` cap
191
+ * was NOT live-verifiable at build time (the shared DEMO_KEY was rate-limited /
192
+ * HTTP 429 on both hosts), so the SAME cap guard is applied here on the well-
193
+ * founded assumption that the two endpoints share the identical JSON:API paging
194
+ * contract (same host, same envelope, same page[number]/page[size] model).
195
+ * LIVE-VERIFY `GET /v4/comments?page[number]=41` when a non-throttled key is
196
+ * available and adjust REG_MAX_PAGE if it ever differs.
197
+ */
198
+ async function regulationsSearch(endpoint, kind, args) {
199
+ const label = `regulations:${endpoint}`;
200
+ const pageNumber = args.pageNumber ?? 1;
201
+ const pageSize = args.pageSize ?? 25;
202
+ const sort = args.sort ?? "-postedDate";
203
+ // B1 PRE-FETCH window guard (mirror edgar.ts `from >= FTS_MAX_FROM`): reject a
204
+ // beyond-cap page BEFORE any fetch, so a naive agent that computes page 41 gets
205
+ // a clean LOCAL invalid_input, never the upstream HTTP 400. (Zod also caps
206
+ // pageNumber≤40 / pageSize≤250 at the tool boundary — this is defense-in-depth
207
+ // for a direct call.)
208
+ if (pageNumber > REG_MAX_PAGE || pageNumber * pageSize > REG_MAX_RECORDS) {
209
+ throw new ToolErrorCarrier({
210
+ kind: "invalid_input",
211
+ message: `Regulations.gov page[number] (${pageNumber}) × page[size] (${pageSize}) exceeds the API's hard ${REG_MAX_RECORDS}-record / ${REG_MAX_PAGE}-page pagination ceiling. Narrow filters (agencyId/docketId/postedDate) or seek by lastModifiedDate instead of paging past ${REG_MAX_RECORDS} results.`,
212
+ retryable: false,
213
+ upstreamEndpoint: label,
214
+ });
215
+ }
216
+ const searchTerm = args.searchTerm ?? args.query;
217
+ const params = new URLSearchParams();
218
+ const filtersApplied = [];
219
+ // D3 (no-silent-filter): documentType/withinCommentPeriod are documents-ONLY
220
+ // facets. RegulationsSearchInput is SHARED by documents + comments, so a caller
221
+ // can supply them to regulations_search_comments — where /v4/comments does not
222
+ // accept them. Rather than silently ignore them (returning comments as if the
223
+ // facet applied), DISCLOSE them in filtersDropped + a note.
224
+ const filtersDropped = [];
225
+ if (searchTerm) {
226
+ params.set("filter[searchTerm]", searchTerm);
227
+ filtersApplied.push("searchTerm");
228
+ }
229
+ if (args.agencyId) {
230
+ params.set("filter[agencyId]", args.agencyId);
231
+ filtersApplied.push("agencyId");
232
+ }
233
+ if (args.docketId) {
234
+ params.set("filter[docketId]", args.docketId);
235
+ filtersApplied.push("docketId");
236
+ }
237
+ if (kind === "documents" && args.documentType) {
238
+ params.set("filter[documentType]", args.documentType);
239
+ filtersApplied.push("documentType");
240
+ }
241
+ else if (kind === "comments" && args.documentType !== undefined) {
242
+ // Comments are themselves "Public Submission" documents; /v4/comments exposes
243
+ // NO filter[documentType]. Not applied → disclosed, never silently dropped.
244
+ filtersDropped.push("documentType");
245
+ }
246
+ if (kind === "documents" && args.withinCommentPeriod !== undefined) {
247
+ params.set("filter[withinCommentPeriod]", String(args.withinCommentPeriod));
248
+ filtersApplied.push("withinCommentPeriod");
249
+ }
250
+ else if (kind === "comments" && args.withinCommentPeriod !== undefined) {
251
+ // withinCommentPeriod = whether a DOCUMENT is currently open for comment — a
252
+ // documents-only concept; /v4/comments does not accept it.
253
+ filtersDropped.push("withinCommentPeriod");
254
+ }
255
+ if (args.postedDateGe) {
256
+ params.set("filter[postedDate][ge]", args.postedDateGe);
257
+ filtersApplied.push("postedDateGe");
258
+ }
259
+ if (args.postedDateLe) {
260
+ params.set("filter[postedDate][le]", args.postedDateLe);
261
+ filtersApplied.push("postedDateLe");
262
+ }
263
+ params.set("sort", sort);
264
+ params.set("page[number]", String(pageNumber));
265
+ params.set("page[size]", String(pageSize));
266
+ // M1 — the typed catch ladder (fema.ts:262-275 shape) — IDENTICAL to its
267
+ // searchDockets/getDocket siblings. Preserve the 429/404/5xx/400/timeout
268
+ // ToolErrorCarrier taxonomy FIRST (LOAD-BEARING: the DEMO_KEY-10/hr 429→rate_limited
269
+ // frontier would regress to schema_drift under a broader catch); reclassify a 200
270
+ // non-JSON `.json()` SyntaxError to schema_drift SECOND; bare-rethrow LAST.
271
+ let body;
272
+ try {
273
+ body = await getDatagov(REGULATIONS_HOST, endpoint, label, params);
274
+ }
275
+ catch (e) {
276
+ if (e instanceof ToolErrorCarrier)
277
+ throw e;
278
+ if (e instanceof SyntaxError)
279
+ throw driftError(label, "Regulations.gov returned a non-JSON body at HTTP 200 — schema drift.");
280
+ throw e;
281
+ }
282
+ const b = (body ?? {});
283
+ // M2 — `data` MUST be an array (a missing/string/null data is drift, never []).
284
+ if (!Array.isArray(b.data)) {
285
+ throw driftError(label, `regulations shape drift — ${endpoint} response.data must be an array.`);
286
+ }
287
+ // M3 — CONTAINER-guarded total: a null/absent `meta` or a non-number
288
+ // `meta.totalElements` → driftError (NOT a TypeError that would mask drift as
289
+ // upstream_unavailable).
290
+ if (!b.meta || typeof b.meta.totalElements !== "number") {
291
+ throw driftError(label, `regulations shape drift — ${endpoint} meta.totalElements missing/non-number.`);
292
+ }
293
+ const rows = b.data.map(kind === "documents" ? mapRegDocument : mapRegComment);
294
+ const returned = rows.length;
295
+ // EXACT real total — NEVER meta.totalPages*pageSize (§1c: totalPages is capped
296
+ // at 40 = a lie). totalElements is typeof-guarded to be a number above.
297
+ const totalAvailable = num(b.meta.totalElements);
298
+ const offset = (pageNumber - 1) * pageSize;
299
+ // B1 EDGAR-pattern cap: `hasMore` reflects whether MORE genuinely exists (from
300
+ // the real total). `nextOffset` is the next page's record offset ONLY when the
301
+ // next page is still inside the 40-page/10,000-record window; at the ceiling it
302
+ // is null (no actionable continuation) + a disclosing note. Never use
303
+ // meta.hasNextPage/lastPage/totalPages (cap-relative liars).
304
+ const moreExist = totalAvailable !== null && pageNumber * pageSize < totalAvailable;
305
+ const nextPageNumber = pageNumber + 1;
306
+ const nextPageReachable = nextPageNumber <= REG_MAX_PAGE &&
307
+ nextPageNumber * pageSize <= REG_MAX_RECORDS;
308
+ const hasMore = moreExist;
309
+ const nextOffset = moreExist && nextPageReachable ? pageNumber * pageSize : null;
310
+ const notes = [];
311
+ pushKeyNote(notes);
312
+ if (filtersDropped.length > 0) {
313
+ notes.push(`${filtersDropped.join("/")} do not apply to the ${endpoint} (comments) endpoint and were ignored (they are documents-only facets) — results are UNFILTERED on those facets. Use regulations_search_documents to filter by documentType / withinCommentPeriod.`);
314
+ }
315
+ if (moreExist && !nextPageReachable) {
316
+ notes.push(`Reached the API's ${REG_MAX_RECORDS}-record / ${REG_MAX_PAGE}-page pagination ceiling (totalElements=${totalAvailable} total). ~${totalAvailable - REG_MAX_RECORDS} more records exist but are UNREACHABLE via page[number] — narrow filters (agencyId/docketId/postedDate) or seek by lastModifiedDate to reach the rest.`);
317
+ }
318
+ const key = kind === "documents" ? "documents" : "comments";
319
+ return withMeta({ [key]: rows }, {
320
+ source: REG_DOC_SOURCE(keyModeLabel()),
321
+ keylessMode: false,
322
+ returned,
323
+ totalAvailable,
324
+ filtersApplied,
325
+ filtersDropped,
326
+ fieldsUnavailable: [],
327
+ pagination: { offset, limit: pageSize, hasMore, nextOffset },
328
+ notes,
329
+ });
330
+ }
331
+ /** Tool: regulations_search_documents. */
332
+ export async function searchDocuments(args) {
333
+ return regulationsSearch("/v4/documents", "documents", args);
334
+ }
335
+ /** Tool: regulations_search_comments. */
336
+ export async function searchComments(args) {
337
+ return regulationsSearch("/v4/comments", "comments", args);
338
+ }
339
+ /**
340
+ * Map one JSON:API `/v4/dockets` LIST row into a flat, honesty-coerced record
341
+ * (all scalars via `str`, null-never-empty). `highlightedContent` (a search-
342
+ * snippet artifact) is surfaced ONLY when a searchTerm was sent. There is NO
343
+ * `rin` field: the list envelope never carries it (captured fact 4) — surfacing a
344
+ * null `rin` here would read as "no RIN"; `rin` lives ONLY in mapDocketDetail.
345
+ */
346
+ function mapDocketListRow(item, withSearchTerm) {
347
+ const it = (item ?? {});
348
+ const a = (it.attributes ?? {});
349
+ const row = {
350
+ docketId: str(a.docketId),
351
+ title: str(a.title),
352
+ agencyId: str(a.agencyId),
353
+ docketType: str(a.docketType),
354
+ lastModifiedDate: str(a.lastModifiedDate),
355
+ objectId: str(a.objectId),
356
+ id: str(it.id),
357
+ };
358
+ if (withSearchTerm)
359
+ row.highlightedContent = str(a.highlightedContent);
360
+ return row;
361
+ }
362
+ /**
363
+ * Map the `/v4/dockets/{docketId}` DETAIL object into the compliance-relevant
364
+ * subset (captured fact 5). `rin` is null-when-absent (never "") — the cross-
365
+ * source join key to the Federal Register / Unified Agenda. `keywords` → string[]
366
+ * (via `str`+filter; [] only when genuinely absent). DROPS internal/rarely-
367
+ * populated fields (displayProperties/generic/field1/field2/subType/subType2/
368
+ * category/petitionNbr/organization/legacyId) to keep the row focused.
369
+ */
370
+ function mapDocketDetail(data) {
371
+ const it = (data ?? {});
372
+ const a = (it.attributes ?? {});
373
+ const keywords = Array.isArray(a.keywords)
374
+ ? a.keywords
375
+ .map((k) => str(k))
376
+ .filter((k) => k !== null)
377
+ : [];
378
+ return {
379
+ docketId: str(a.docketId),
380
+ title: str(a.title),
381
+ agencyId: str(a.agencyId),
382
+ docketType: str(a.docketType),
383
+ // rin — the cross-source join key; null-when-absent, NEVER "".
384
+ rin: str(a.rin),
385
+ dkAbstract: str(a.dkAbstract),
386
+ keywords,
387
+ program: str(a.program),
388
+ shortTitle: str(a.shortTitle),
389
+ effectiveDate: str(a.effectiveDate),
390
+ modifyDate: str(a.modifyDate),
391
+ objectId: str(a.objectId),
392
+ id: str(it.id),
393
+ };
394
+ }
395
+ /**
396
+ * Tool: regulations_search_dockets (`GET /v4/dockets`). Lists rulemaking/
397
+ * nonrulemaking docket CONTAINERS with the same 40-page/10,000-record ceiling and
398
+ * `totalElements`-exact total doctrine as regulationsSearch.
399
+ *
400
+ * ★ min-5 floor (ADR-0044): the API 400s on page[size]<5. The friendly `limit`
401
+ * exposes 1..250; the wire page[size] is `max(5, limit)` and a `limit<5` returns
402
+ * the first `limit` of the fetched rows client-side (disclosed) — `totalAvailable`
403
+ * stays the EXACT server total.
404
+ */
405
+ export async function searchDockets(args) {
406
+ const label = "regulations:/v4/dockets";
407
+ const limit = args.limit ?? 20;
408
+ const pageNumber = args.pageNumber ?? 1;
409
+ const sort = args.sort ?? "-lastModifiedDate";
410
+ const fetchSize = Math.max(5, limit); // the wire page[size] — NEVER < 5
411
+ const clientSlice = limit < 5;
412
+ // S6 — pre-fetch window guard (mirror regulationsSearch): reject a beyond-ceiling
413
+ // page BEFORE any fetch, so a DIRECT call bypassing the Zod caps gets a clean
414
+ // LOCAL invalid_input rather than burning a scarce DEMO_KEY call on an upstream
415
+ // 400. (Zod also caps pageNumber≤40 / limit≤250 at the tool boundary.)
416
+ if (pageNumber > REG_MAX_PAGE || pageNumber * fetchSize > REG_MAX_RECORDS) {
417
+ throw new ToolErrorCarrier({
418
+ kind: "invalid_input",
419
+ message: `Regulations.gov page[number] (${pageNumber}) × page[size] (${fetchSize}) exceeds the API's hard ${REG_MAX_RECORDS}-record / ${REG_MAX_PAGE}-page pagination ceiling. Narrow filters (agencyId/docketType/lastModifiedDate) or seek by lastModifiedDate instead of paging past ${REG_MAX_RECORDS} results.`,
420
+ retryable: false,
421
+ upstreamEndpoint: label,
422
+ });
423
+ }
424
+ const searchTerm = args.searchTerm ?? args.query;
425
+ const params = new URLSearchParams();
426
+ const filtersApplied = [];
427
+ if (searchTerm) {
428
+ params.set("filter[searchTerm]", searchTerm);
429
+ filtersApplied.push("searchTerm");
430
+ }
431
+ if (args.agencyId) {
432
+ params.set("filter[agencyId]", args.agencyId);
433
+ filtersApplied.push("agencyId");
434
+ }
435
+ if (args.docketType) {
436
+ params.set("filter[docketType]", args.docketType);
437
+ filtersApplied.push("docketType");
438
+ }
439
+ if (args.lastModifiedDateGe) {
440
+ params.set("filter[lastModifiedDate][ge]", args.lastModifiedDateGe);
441
+ filtersApplied.push("lastModifiedDateGe");
442
+ }
443
+ if (args.lastModifiedDateLe) {
444
+ params.set("filter[lastModifiedDate][le]", args.lastModifiedDateLe);
445
+ filtersApplied.push("lastModifiedDateLe");
446
+ }
447
+ params.set("sort", sort);
448
+ params.set("page[number]", String(pageNumber));
449
+ params.set("page[size]", String(fetchSize));
450
+ // M1 — the typed catch ladder (fema.ts:262-275 shape). Preserve the 429/404/5xx/
451
+ // 400/timeout ToolErrorCarrier taxonomy FIRST (LOAD-BEARING: a broader catch
452
+ // would regress the DEMO_KEY-10/hr 429→rate_limited frontier to schema_drift);
453
+ // reclassify a 200 non-JSON `.json()` SyntaxError to schema_drift SECOND; bare-
454
+ // rethrow LAST.
455
+ let body;
456
+ try {
457
+ body = await getDatagov(REGULATIONS_HOST, "/v4/dockets", label, params);
458
+ }
459
+ catch (e) {
460
+ if (e instanceof ToolErrorCarrier)
461
+ throw e;
462
+ if (e instanceof SyntaxError)
463
+ throw driftError(label, "Regulations.gov returned a non-JSON body at HTTP 200 — schema drift.");
464
+ throw e;
465
+ }
466
+ const b = (body ?? {});
467
+ // data[] guard + meta.totalElements container guard (datagov.ts:288-302 idiom).
468
+ if (!Array.isArray(b.data)) {
469
+ throw driftError(label, "regulations shape drift — /v4/dockets response.data must be an array.");
470
+ }
471
+ if (!b.meta || typeof b.meta.totalElements !== "number") {
472
+ throw driftError(label, "regulations shape drift — /v4/dockets meta.totalElements missing/non-number.");
473
+ }
474
+ const rawRows = b.data.map((row) => mapDocketListRow(row, Boolean(searchTerm)));
475
+ const rows = clientSlice ? rawRows.slice(0, limit) : rawRows;
476
+ const returned = rows.length;
477
+ // EXACT real total — NEVER meta.totalPages (a capped-40 sentinel). Typeof-guarded
478
+ // above. UNAFFECTED by the client-slice.
479
+ const totalAvailable = num(b.meta.totalElements);
480
+ const offset = (pageNumber - 1) * fetchSize;
481
+ const moreExist = totalAvailable !== null && offset + returned < totalAvailable;
482
+ const nextPageNumber = pageNumber + 1;
483
+ const nextPageReachable = nextPageNumber <= REG_MAX_PAGE &&
484
+ nextPageNumber * fetchSize <= REG_MAX_RECORDS;
485
+ const hasMore = moreExist;
486
+ // nextOffset is null in the client-slice case (paging by pageNumber would skip
487
+ // the unshown rows — an honest "increase limit to page reliably") and at the
488
+ // ceiling (no reachable continuation) — both surface hasMore:true/nextOffset:null.
489
+ const nextOffset = moreExist && nextPageReachable && !clientSlice ? pageNumber * fetchSize : null;
490
+ const notes = [];
491
+ pushKeyNote(notes);
492
+ if (moreExist && !nextPageReachable) {
493
+ notes.push(`Reached the API's ${REG_MAX_RECORDS}-record / ${REG_MAX_PAGE}-page pagination ceiling (totalElements=${totalAvailable} total). ~${totalAvailable - REG_MAX_RECORDS} more records exist but are UNREACHABLE via page[number] — narrow filters (agencyId/docketType/lastModifiedDate) or seek by lastModifiedDate to reach the rest.`);
494
+ }
495
+ if (clientSlice) {
496
+ notes.push(`limit<5 requested; page[size] was floored to 5 upstream (the API rejects page[size]<5) and the first ${limit} of the ${rawRows.length} fetched rows are returned — totalAvailable is still the EXACT server total. For reliable pagination use limit>=5.`);
497
+ }
498
+ return withMeta({ dockets: rows }, {
499
+ source: REG_DOC_SOURCE(keyModeLabel()),
500
+ keylessMode: false, // M2 — genuinely keyed
501
+ returned,
502
+ totalAvailable,
503
+ filtersApplied,
504
+ filtersDropped: [],
505
+ fieldsUnavailable: [],
506
+ // S3 — pagination.limit is the CALLER's requested count (the honest effective
507
+ // window; the wire floor of 5 is disclosed only in the min-5 note). offset
508
+ // uses fetchSize.
509
+ pagination: { offset, limit, hasMore, nextOffset },
510
+ notes,
511
+ });
512
+ }
513
+ /**
514
+ * Tool: regulations_get_docket (`GET /v4/dockets/{docketId}`). Single-docket
515
+ * detail — the ONLY view carrying `rin`. `docketId` is charclass-validated at the
516
+ * Zod layer (S1); it is the only caller value reaching a path segment.
517
+ */
518
+ export async function getDocket(args) {
519
+ const label = "regulations:/v4/dockets/{id}";
520
+ const path = `/v4/dockets/${args.docketId}`;
521
+ const params = new URLSearchParams();
522
+ // M1 — the IDENTICAL typed catch ladder as searchDockets (fema.ts:262-275). A
523
+ // nonexistent id is EXPECTED to 404 → not_found; the missing-`data` driftError
524
+ // guard below is the mandatory fallback if a bad id instead yields a 200 error-
525
+ // envelope (S2).
526
+ let body;
527
+ try {
528
+ body = await getDatagov(REGULATIONS_HOST, path, label, params);
529
+ }
530
+ catch (e) {
531
+ if (e instanceof ToolErrorCarrier)
532
+ throw e;
533
+ if (e instanceof SyntaxError)
534
+ throw driftError(label, "Regulations.gov returned a non-JSON body at HTTP 200 — schema drift.");
535
+ throw e;
536
+ }
537
+ const b = (body ?? {});
538
+ // S2 — MANDATORY fabrication guard: a missing/non-object `data` → schema_drift,
539
+ // NEVER a fabricated {docketId, ...nulls} (mirror getBill's bill guard).
540
+ if (!b.data || typeof b.data !== "object") {
541
+ throw driftError(label, "regulations shape drift — /v4/dockets/{docketId} response.data missing/not-an-object.");
542
+ }
543
+ const notes = [];
544
+ pushKeyNote(notes);
545
+ notes.push("rin (Regulatory Identifier Number) is the join key to the Federal Register (fed_register_search_documents) and the Unified Agenda; null when this docket has no assigned RIN (e.g. many Nonrulemaking dockets).");
546
+ return withMeta({ docket: mapDocketDetail(b.data) }, {
547
+ source: REG_DOC_SOURCE(keyModeLabel()),
548
+ keylessMode: false, // M2 — genuinely keyed
549
+ returned: 1,
550
+ totalAvailable: null, // S4 — single-record detail convention (rely on complete:true)
551
+ filtersApplied: [],
552
+ filtersDropped: [],
553
+ fieldsUnavailable: [],
554
+ notes,
555
+ });
556
+ }
557
+ // ═══════════════════ Congress.gov ═════════════════════════════════
558
+ const CONGRESS_SOURCE = (mode) => `${CONGRESS_HOST} via Congress.gov API (${mode})`;
559
+ /** Map one Congress.gov `bills[]` item into a flat, honesty-coerced row. */
560
+ function mapBill(item) {
561
+ const it = (item ?? {});
562
+ const latest = (it.latestAction ?? null);
563
+ return {
564
+ congress: num(it.congress),
565
+ type: str(it.type),
566
+ number: str(it.number),
567
+ title: str(it.title),
568
+ originChamber: str(it.originChamber),
569
+ latestAction: latest
570
+ ? { actionDate: str(latest.actionDate), text: str(latest.text) }
571
+ : null,
572
+ updateDate: str(it.updateDate),
573
+ // `url` is the API's canonical resource locator; under header auth it is
574
+ // key-FREE (the key is never in any URL). Passed through as data.
575
+ url: str(it.url),
576
+ };
577
+ }
578
+ /**
579
+ * Tool: congress_search_bills. Lists bills via `/v3/bill`, `/v3/bill/{congress}`,
580
+ * or `/v3/bill/{congress}/{billType}` (congress/billType are Zod-constrained PATH
581
+ * segments — §4). Numeric offset/limit pagination; `totalAvailable =
582
+ * pagination.count` (EXACT). `nextOffset` is re-derived NUMERICALLY (never the
583
+ * upstream `pagination.next` URL — §2 rule 4).
584
+ *
585
+ * HONESTY on `query`: the Congress.gov `/v3/bill` endpoint has NO keyword-search
586
+ * parameter, so a supplied `query` is NOT sent and is disclosed in
587
+ * `filtersDropped` (a filter we cannot honor is surfaced, never silently ignored).
588
+ */
589
+ export async function searchBills(args) {
590
+ const label = "congress:/v3/bill";
591
+ const limit = args.limit ?? 20;
592
+ const offset = args.offset ?? 0;
593
+ if (args.billType && args.congress === undefined) {
594
+ throw new ToolErrorCarrier({
595
+ kind: "invalid_input",
596
+ message: "congress_search_bills: `billType` requires `congress` (the path is /v3/bill/{congress}/{billType}). Provide `congress`, or omit `billType`.",
597
+ retryable: false,
598
+ upstreamEndpoint: label,
599
+ });
600
+ }
601
+ let path = "/v3/bill";
602
+ if (args.congress !== undefined) {
603
+ path += `/${args.congress}`;
604
+ if (args.billType)
605
+ path += `/${args.billType}`;
606
+ }
607
+ const params = new URLSearchParams();
608
+ params.set("format", "json");
609
+ params.set("limit", String(limit));
610
+ params.set("offset", String(offset));
611
+ const filtersApplied = [];
612
+ const filtersDropped = [];
613
+ if (args.congress !== undefined)
614
+ filtersApplied.push("congress");
615
+ if (args.billType)
616
+ filtersApplied.push("billType");
617
+ if (args.fromDateTime) {
618
+ params.set("fromDateTime", args.fromDateTime);
619
+ filtersApplied.push("fromDateTime");
620
+ }
621
+ if (args.toDateTime) {
622
+ params.set("toDateTime", args.toDateTime);
623
+ filtersApplied.push("toDateTime");
624
+ }
625
+ if (args.query)
626
+ filtersDropped.push("query");
627
+ const body = await getDatagov(CONGRESS_HOST, path, label, params);
628
+ const b = (body ?? {});
629
+ // M2 — `bills` MUST be an array (missing/string/null → drift, never []).
630
+ if (!Array.isArray(b.bills)) {
631
+ throw driftError(label, "congress shape drift — /v3/bill response.bills must be an array.");
632
+ }
633
+ // M3 — CONTAINER-guarded total: null/absent `pagination` or non-number
634
+ // `pagination.count` → driftError (not a TypeError masked as upstream_unavailable).
635
+ if (!b.pagination || typeof b.pagination.count !== "number") {
636
+ throw driftError(label, "congress shape drift — /v3/bill pagination.count missing/non-number.");
637
+ }
638
+ const bills = b.bills.map(mapBill);
639
+ const returned = bills.length;
640
+ const totalAvailable = num(b.pagination.count); // EXACT
641
+ const hasMore = totalAvailable !== null ? offset + returned < totalAvailable : false;
642
+ const nextOffset = hasMore ? offset + returned : null;
643
+ const notes = [];
644
+ pushKeyNote(notes);
645
+ if (filtersDropped.includes("query")) {
646
+ notes.push("The `query` filter was NOT applied: Congress.gov /v3/bill has no keyword-search parameter. Results are UNFILTERED on keyword — narrow with `congress`, `billType`, `fromDateTime`/`toDateTime` instead.");
647
+ }
648
+ return withMeta({ bills }, {
649
+ source: CONGRESS_SOURCE(keyModeLabel()),
650
+ keylessMode: false,
651
+ returned,
652
+ totalAvailable,
653
+ filtersApplied,
654
+ filtersDropped,
655
+ fieldsUnavailable: [],
656
+ pagination: { offset, limit, hasMore, nextOffset },
657
+ notes,
658
+ });
659
+ }
660
+ /**
661
+ * Tool: congress_get_bill. Fetches one bill via
662
+ * `/v3/bill/{congress}/{billType}/{billNumber}` (all three path segments are
663
+ * Zod-constrained to int/enum — §4, no injection). Single-record honest `_meta`
664
+ * (complete:true). A 404 (nonexistent bill) → not_found (never fabricated).
665
+ */
666
+ export async function getBill(args) {
667
+ const label = "congress:/v3/bill";
668
+ const path = `/v3/bill/${args.congress}/${args.billType}/${args.billNumber}`;
669
+ const params = new URLSearchParams();
670
+ params.set("format", "json");
671
+ const body = await getDatagov(CONGRESS_HOST, path, label, params);
672
+ const b = (body ?? {});
673
+ if (!b.bill || typeof b.bill !== "object") {
674
+ throw driftError(label, "congress shape drift — /v3/bill/{congress}/{type}/{number} response.bill missing/not-an-object.");
675
+ }
676
+ const notes = [];
677
+ pushKeyNote(notes);
678
+ return withMeta({ bill: b.bill }, {
679
+ source: CONGRESS_SOURCE(keyModeLabel()),
680
+ keylessMode: false,
681
+ returned: 1,
682
+ totalAvailable: null,
683
+ filtersApplied: [],
684
+ filtersDropped: [],
685
+ fieldsUnavailable: [],
686
+ notes,
687
+ });
688
+ }
689
+ //# sourceMappingURL=datagov.js.map