@cliwant/mcp-sam-gov 0.3.0 → 1.1.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 (243) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +49 -16
  3. package/README.ko.md +49 -16
  4. package/README.md +255 -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-economic.d.ts +93 -0
  18. package/dist/census-economic.d.ts.map +1 -0
  19. package/dist/census-economic.js +355 -0
  20. package/dist/census-economic.js.map +1 -0
  21. package/dist/census.d.ts +182 -0
  22. package/dist/census.d.ts.map +1 -0
  23. package/dist/census.js +576 -0
  24. package/dist/census.js.map +1 -0
  25. package/dist/ckan.d.ts +141 -0
  26. package/dist/ckan.d.ts.map +1 -0
  27. package/dist/ckan.js +393 -0
  28. package/dist/ckan.js.map +1 -0
  29. package/dist/clinicaltrials.d.ts +180 -0
  30. package/dist/clinicaltrials.d.ts.map +1 -0
  31. package/dist/clinicaltrials.js +730 -0
  32. package/dist/clinicaltrials.js.map +1 -0
  33. package/dist/cms.d.ts +140 -0
  34. package/dist/cms.d.ts.map +1 -0
  35. package/dist/cms.js +482 -0
  36. package/dist/cms.js.map +1 -0
  37. package/dist/coerce.d.ts +32 -0
  38. package/dist/coerce.d.ts.map +1 -0
  39. package/dist/coerce.js +50 -0
  40. package/dist/coerce.js.map +1 -0
  41. package/dist/datagov-catalog.d.ts +84 -0
  42. package/dist/datagov-catalog.d.ts.map +1 -0
  43. package/dist/datagov-catalog.js +233 -0
  44. package/dist/datagov-catalog.js.map +1 -0
  45. package/dist/datagov.d.ts +146 -0
  46. package/dist/datagov.d.ts.map +1 -0
  47. package/dist/datagov.js +689 -0
  48. package/dist/datagov.js.map +1 -0
  49. package/dist/datagovKey.d.ts +36 -0
  50. package/dist/datagovKey.d.ts.map +1 -0
  51. package/dist/datagovKey.js +60 -0
  52. package/dist/datagovKey.js.map +1 -0
  53. package/dist/datasource.d.ts +313 -0
  54. package/dist/datasource.d.ts.map +1 -0
  55. package/dist/datasource.js +551 -0
  56. package/dist/datasource.js.map +1 -0
  57. package/dist/disclosure.d.ts +55 -0
  58. package/dist/disclosure.d.ts.map +1 -0
  59. package/dist/disclosure.js +57 -0
  60. package/dist/disclosure.js.map +1 -0
  61. package/dist/ecfr.d.ts +4 -2
  62. package/dist/ecfr.d.ts.map +1 -1
  63. package/dist/ecfr.js +74 -1
  64. package/dist/ecfr.js.map +1 -1
  65. package/dist/echo.d.ts +143 -0
  66. package/dist/echo.d.ts.map +1 -0
  67. package/dist/echo.js +424 -0
  68. package/dist/echo.js.map +1 -0
  69. package/dist/edgar.d.ts +377 -0
  70. package/dist/edgar.d.ts.map +1 -0
  71. package/dist/edgar.js +2418 -0
  72. package/dist/edgar.js.map +1 -0
  73. package/dist/errors.d.ts +26 -0
  74. package/dist/errors.d.ts.map +1 -1
  75. package/dist/errors.js +69 -1
  76. package/dist/errors.js.map +1 -1
  77. package/dist/fac.d.ts +180 -0
  78. package/dist/fac.d.ts.map +1 -0
  79. package/dist/fac.js +416 -0
  80. package/dist/fac.js.map +1 -0
  81. package/dist/far.d.ts +170 -0
  82. package/dist/far.d.ts.map +1 -0
  83. package/dist/far.js +804 -0
  84. package/dist/far.js.map +1 -0
  85. package/dist/fdic.d.ts +599 -0
  86. package/dist/fdic.d.ts.map +1 -0
  87. package/dist/fdic.js +1624 -0
  88. package/dist/fdic.js.map +1 -0
  89. package/dist/federal-register.d.ts +139 -2
  90. package/dist/federal-register.d.ts.map +1 -1
  91. package/dist/federal-register.js +419 -3
  92. package/dist/federal-register.js.map +1 -1
  93. package/dist/fema.d.ts +181 -0
  94. package/dist/fema.d.ts.map +1 -0
  95. package/dist/fema.js +436 -0
  96. package/dist/fema.js.map +1 -0
  97. package/dist/fpds.d.ts +108 -0
  98. package/dist/fpds.d.ts.map +1 -0
  99. package/dist/fpds.js +519 -0
  100. package/dist/fpds.js.map +1 -0
  101. package/dist/fred.d.ts +108 -0
  102. package/dist/fred.d.ts.map +1 -0
  103. package/dist/fred.js +373 -0
  104. package/dist/fred.js.map +1 -0
  105. package/dist/gao.d.ts +64 -0
  106. package/dist/gao.d.ts.map +1 -0
  107. package/dist/gao.js +640 -0
  108. package/dist/gao.js.map +1 -0
  109. package/dist/govinfo.d.ts +111 -0
  110. package/dist/govinfo.d.ts.map +1 -0
  111. package/dist/govinfo.js +422 -0
  112. package/dist/govinfo.js.map +1 -0
  113. package/dist/grants.d.ts +27 -4
  114. package/dist/grants.d.ts.map +1 -1
  115. package/dist/grants.js +111 -6
  116. package/dist/grants.js.map +1 -1
  117. package/dist/gsa-csv.d.ts +249 -0
  118. package/dist/gsa-csv.d.ts.map +1 -0
  119. package/dist/gsa-csv.js +784 -0
  120. package/dist/gsa-csv.js.map +1 -0
  121. package/dist/integrity.d.ts +212 -0
  122. package/dist/integrity.d.ts.map +1 -0
  123. package/dist/integrity.js +707 -0
  124. package/dist/integrity.js.map +1 -0
  125. package/dist/keys.d.ts +83 -0
  126. package/dist/keys.d.ts.map +1 -0
  127. package/dist/keys.js +173 -0
  128. package/dist/keys.js.map +1 -0
  129. package/dist/meta.d.ts +165 -0
  130. package/dist/meta.d.ts.map +1 -0
  131. package/dist/meta.js +162 -0
  132. package/dist/meta.js.map +1 -0
  133. package/dist/nih.d.ts +117 -0
  134. package/dist/nih.d.ts.map +1 -0
  135. package/dist/nih.js +291 -0
  136. package/dist/nih.js.map +1 -0
  137. package/dist/nppes.d.ts +157 -0
  138. package/dist/nppes.d.ts.map +1 -0
  139. package/dist/nppes.js +648 -0
  140. package/dist/nppes.js.map +1 -0
  141. package/dist/nsf.d.ts +176 -0
  142. package/dist/nsf.d.ts.map +1 -0
  143. package/dist/nsf.js +554 -0
  144. package/dist/nsf.js.map +1 -0
  145. package/dist/nvd.d.ts +176 -0
  146. package/dist/nvd.d.ts.map +1 -0
  147. package/dist/nvd.js +912 -0
  148. package/dist/nvd.js.map +1 -0
  149. package/dist/ofac.d.ts +205 -0
  150. package/dist/ofac.d.ts.map +1 -0
  151. package/dist/ofac.js +919 -0
  152. package/dist/ofac.js.map +1 -0
  153. package/dist/pricing.d.ts +110 -0
  154. package/dist/pricing.d.ts.map +1 -0
  155. package/dist/pricing.js +843 -0
  156. package/dist/pricing.js.map +1 -0
  157. package/dist/sam-gov/client.d.ts +60 -2
  158. package/dist/sam-gov/client.d.ts.map +1 -1
  159. package/dist/sam-gov/client.js +320 -54
  160. package/dist/sam-gov/client.js.map +1 -1
  161. package/dist/sam-gov/index.d.ts +1 -1
  162. package/dist/sam-gov/index.d.ts.map +1 -1
  163. package/dist/sam-gov/index.js +1 -1
  164. package/dist/sam-gov/index.js.map +1 -1
  165. package/dist/sam-gov/types.d.ts +24 -0
  166. package/dist/sam-gov/types.d.ts.map +1 -1
  167. package/dist/sba.d.ts +72 -0
  168. package/dist/sba.d.ts.map +1 -0
  169. package/dist/sba.js +281 -0
  170. package/dist/sba.js.map +1 -0
  171. package/dist/server.d.ts +14 -2
  172. package/dist/server.d.ts.map +1 -1
  173. package/dist/server.js +4032 -297
  174. package/dist/server.js.map +1 -1
  175. package/dist/snapshot.d.ts +115 -0
  176. package/dist/snapshot.d.ts.map +1 -0
  177. package/dist/snapshot.js +175 -0
  178. package/dist/snapshot.js.map +1 -0
  179. package/dist/socrata.d.ts +157 -0
  180. package/dist/socrata.d.ts.map +1 -0
  181. package/dist/socrata.js +448 -0
  182. package/dist/socrata.js.map +1 -0
  183. package/dist/treasury.d.ts +143 -0
  184. package/dist/treasury.d.ts.map +1 -0
  185. package/dist/treasury.js +436 -0
  186. package/dist/treasury.js.map +1 -0
  187. package/dist/usaspending.d.ts +260 -65
  188. package/dist/usaspending.d.ts.map +1 -1
  189. package/dist/usaspending.js +1629 -201
  190. package/dist/usaspending.js.map +1 -1
  191. package/dist/usitc.d.ts +142 -0
  192. package/dist/usitc.d.ts.map +1 -0
  193. package/dist/usitc.js +339 -0
  194. package/dist/usitc.js.map +1 -0
  195. package/package.json +24 -2
  196. package/src/attachments.ts +652 -0
  197. package/src/bls.ts +1943 -0
  198. package/src/cache.ts +5 -0
  199. package/src/census-economic.ts +425 -0
  200. package/src/census.ts +735 -0
  201. package/src/ckan.ts +495 -0
  202. package/src/clinicaltrials.ts +923 -0
  203. package/src/cms.ts +634 -0
  204. package/src/coerce.ts +47 -0
  205. package/src/datagov-catalog.ts +296 -0
  206. package/src/datagov.ts +907 -0
  207. package/src/datagovKey.ts +68 -0
  208. package/src/datasource.ts +721 -0
  209. package/src/disclosure.ts +61 -0
  210. package/src/ecfr.ts +98 -1
  211. package/src/echo.ts +496 -0
  212. package/src/edgar.ts +3014 -0
  213. package/src/errors.ts +84 -1
  214. package/src/fac.ts +529 -0
  215. package/src/far.ts +1007 -0
  216. package/src/fdic.ts +2052 -0
  217. package/src/federal-register.ts +512 -3
  218. package/src/fema.ts +541 -0
  219. package/src/fpds.ts +620 -0
  220. package/src/fred.ts +464 -0
  221. package/src/gao.ts +744 -0
  222. package/src/govinfo.ts +497 -0
  223. package/src/grants.ts +139 -7
  224. package/src/gsa-csv.ts +992 -0
  225. package/src/integrity.ts +928 -0
  226. package/src/keys.ts +216 -0
  227. package/src/meta.ts +292 -0
  228. package/src/nih.ts +375 -0
  229. package/src/nppes.ts +834 -0
  230. package/src/nsf.ts +706 -0
  231. package/src/nvd.ts +1124 -0
  232. package/src/ofac.ts +1166 -0
  233. package/src/pricing.ts +1075 -0
  234. package/src/sam-gov/client.ts +345 -63
  235. package/src/sam-gov/index.ts +5 -1
  236. package/src/sam-gov/types.ts +22 -0
  237. package/src/sba.ts +357 -0
  238. package/src/server.ts +4717 -328
  239. package/src/snapshot.ts +223 -0
  240. package/src/socrata.ts +532 -0
  241. package/src/treasury.ts +575 -0
  242. package/src/usaspending.ts +1946 -214
  243. package/src/usitc.ts +420 -0
@@ -0,0 +1,61 @@
1
+ /**
2
+ * disclosure.ts — the single audited disclosure-note TOKENIZER shared across the
3
+ * keyless DataSources (ADR-0022: an honesty-layer primitive, split into its own
4
+ * tiny single-concern module — the sibling of coerce.ts's map-layer primitives).
5
+ *
6
+ * WHY IT EXISTS: two independent sources (NSF Awards C108 = the OR-note,
7
+ * ClinicalTrials.gov C109 = the AND-note) each built a mandatory multi-token
8
+ * disclosure note from the SAME character-for-character delimiter class, and
9
+ * adversarial verification caught the SAME latent gap on BOTH — a whitespace-only
10
+ * `.split(/\s+/)` misses the API's PUNCTUATION delimiters, so a compound that only
11
+ * LOOKS like one token ("coral-reef" = coral OR reef; "Sanofi-Aventis" = Sanofi
12
+ * AND Aventis) leaks through and the mandatory note is silently SKIPPED. Hoisting
13
+ * ONE audited tokenizer removes the drift risk (a class regression now fails NSF
14
+ * AND ClinicalTrials suites at once instead of silently in one) and gives the lint
15
+ * guardrail (lint-invariants.mjs) + the parity fault test a single home to point
16
+ * at: "the ONLY sanctioned way to tokenize a disclosure value lives here."
17
+ *
18
+ * THE CLASS (`DISCLOSURE_SPLIT_RE`) is the PRECISE ES/Essie confirmed-splitter set,
19
+ * live-verified byte-identically on BOTH sources 2026-07-12:
20
+ * - SPLIT (→ multi-token, the note fires): whitespace + `- , / ; + & | @ # =`
21
+ * - DO NOT split (→ one token, no note): `. : _ '` (NSF also `\` `*`)
22
+ * It is deliberately NOT a `[^A-Za-z0-9]+` superset — that would over-disclose a
23
+ * split the APIs did NOT make on `.`/`_`/`'` (a fabricated union/conjunction). `-`
24
+ * is placed LAST (a literal, not a range); `/` is escaped for the regex delimiter.
25
+ *
26
+ * NOTE-AGNOSTIC BY DESIGN: this returns ONLY the token array. The `.length > 1`
27
+ * decision, the per-source note WORDING (NSF = OR-union, ClinicalTrials = AND
28
+ * co-occurrence), and any downstream suppression (ClinicalTrials' sponsor
29
+ * broadening-note) stay in each CALLER — they are genuinely source-specific and do
30
+ * NOT belong to the tokenizer.
31
+ *
32
+ * THE OPTIONAL `splitRe` PARAM is the sanctioned, honest escape hatch: a FUTURE
33
+ * source whose analyzer genuinely splits on a DIFFERENT class routes through THIS
34
+ * helper with its own `splitRe` (explicit, greppable, reviewable) rather than
35
+ * re-inlining a raw `.split(/\s+/)`. That is exactly what lets the lint ban the
36
+ * bare whitespace split outright (there is one sanctioned tokenizer, not many).
37
+ *
38
+ * No new dep, no I/O, pure function — the exact shape of a coerce.ts primitive.
39
+ */
40
+
41
+ /**
42
+ * The shared ES/Essie confirmed-splitter class (byte-identical to NSF's former
43
+ * `NSF_KEYWORD_SPLIT_RE` and ClinicalTrials' former `CT_TOKEN_SPLIT_RE`). Stateless
44
+ * (no `/g`), so a single module-level RegExp is safe to share across callers.
45
+ */
46
+ export const DISCLOSURE_SPLIT_RE = /[\s,;+&|@#=\/-]+/;
47
+
48
+ /**
49
+ * Tokenize a disclosure value for a multi-token honesty note: trim, split on the
50
+ * confirmed-splitter class (default `DISCLOSURE_SPLIT_RE`), and drop empty tokens.
51
+ * Returns ONLY the token array — the caller owns the `.length > 1` gate + note
52
+ * wording. `tokenizeForDisclosure("coral-reef")` → `["coral", "reef"]`;
53
+ * `tokenizeForDisclosure("web_service")` → `["web_service"]` (the class does NOT
54
+ * split `_`); `tokenizeForDisclosure("robotics")` → `["robotics"]`.
55
+ */
56
+ export function tokenizeForDisclosure(
57
+ value: string,
58
+ splitRe: RegExp = DISCLOSURE_SPLIT_RE,
59
+ ): string[] {
60
+ return value.trim().split(splitRe).filter((t) => t.length > 0);
61
+ }
package/src/ecfr.ts CHANGED
@@ -14,10 +14,18 @@
14
14
  */
15
15
 
16
16
  import { fetchWithRetry } from "./errors.js";
17
+ import { driftError } from "./datasource.js";
17
18
  import { memoize } from "./cache.js";
19
+ import { withMeta } from "./meta.js";
18
20
 
19
21
  const ECFR = "https://www.ecfr.gov/api";
20
22
 
23
+ // eCFR's /search/v1/results `meta.total_count` is capped by Elasticsearch's
24
+ // `index.max_result_window` (live-verified 10,000). A total AT OR ABOVE this
25
+ // sentinel is a LOWER BOUND, not an exact count — see the totalIsLowerBound
26
+ // wiring in `search` (D1). A genuine count below this stays exact.
27
+ const ECFR_TOTAL_COUNT_CAP = 10000;
28
+
21
29
  async function fetchJson<T>(url: string): Promise<T> {
22
30
  const r = await fetchWithRetry(
23
31
  url,
@@ -60,6 +68,7 @@ export async function listTitles() {
60
68
  export async function search(args: {
61
69
  query: string;
62
70
  titleNumber?: number;
71
+ chapter?: number;
63
72
  perPage?: number;
64
73
  }) {
65
74
  const url = new URL(`${ECFR}/search/v1/results`);
@@ -70,6 +79,13 @@ export async function search(args: {
70
79
  // an "unpermitted parameter" error from the eCFR API).
71
80
  url.searchParams.set("hierarchy[title]", String(args.titleNumber));
72
81
  }
82
+ // Optional chapter filter (additive; existing ecfr_search callers pass none).
83
+ // Within Title 48: chapter 1 = FAR, chapter 2 = DFARS, chapter 5 = GSAM, etc.
84
+ // This is what lets far_search scope to FAR/DFARS and keep GSAM/agency
85
+ // supplements out server-side. Same hierarchy[…] contract as the title filter.
86
+ if (args.chapter !== undefined) {
87
+ url.searchParams.set("hierarchy[chapter]", String(args.chapter));
88
+ }
73
89
 
74
90
  type Resp = {
75
91
  results?: {
@@ -89,9 +105,35 @@ export async function search(args: {
89
105
  full_text_excerpt?: string;
90
106
  score?: number;
91
107
  }[];
108
+ meta?: {
109
+ current_page?: number;
110
+ total_pages?: number;
111
+ total_count?: number;
112
+ max_score?: number;
113
+ description?: string;
114
+ };
92
115
  };
93
116
  const json = await fetchJson<Resp>(url.toString());
94
- return {
117
+ // F6 (P2 empty-vs-outage): a 200 whose `results` is PRESENT-but-non-array — or
118
+ // a body carrying NEITHER a `results` array NOR a `meta` object — is drift / an
119
+ // unexpected shape, NOT a genuine no-match. Throw (schema_drift) rather than
120
+ // letting `(json.results ?? []).map` coalesce it into a fake AUTHORITATIVE empty.
121
+ // A GENUINE empty (results:[] with meta.total_count:0) flows through honestly
122
+ // below. (eCFR normally signals errors via HTTP status caught by fetchWithRetry;
123
+ // this closes the previously-unhandled + untested 200-body drift path.)
124
+ if (json.results !== undefined && !Array.isArray(json.results)) {
125
+ throw driftError(
126
+ "ecfr.gov",
127
+ "eCFR search returned HTTP 200 but `results` is not an array — treating it as schema drift, NOT an empty result set.",
128
+ );
129
+ }
130
+ if (json.results === undefined && json.meta === undefined) {
131
+ throw driftError(
132
+ "ecfr.gov",
133
+ "eCFR search returned HTTP 200 with neither a `results` array nor a `meta` object — an unexpected shape; treating it as schema drift, NOT an empty result set.",
134
+ );
135
+ }
136
+ const data = {
95
137
  results: (json.results ?? []).map((r) => ({
96
138
  type: r.type ?? "",
97
139
  title: r.hierarchy?.title ?? "",
@@ -109,8 +151,63 @@ export async function search(args: {
109
151
  ? buildEcfrUrl(r.hierarchy)
110
152
  : "",
111
153
  effectiveOn: r.starts_on ?? "",
154
+ // Additive: the version's end date. null = the CURRENT (in-force) version;
155
+ // a non-null date = a HISTORICAL version. eCFR search returns ~5 versions
156
+ // per section; existing ecfr_search callers simply ignore this extra field,
157
+ // while far_search uses it to collapse historical dups to the current one.
158
+ endsOn: r.ends_on ?? null,
112
159
  })),
113
160
  };
161
+
162
+ // Truthful `_meta` (spec §1.2 A6, §2.3). eCFR returns a hit count in
163
+ // `meta.total_count`, BUT that count is capped by Elasticsearch's
164
+ // `index.max_result_window` at 10,000 (a real ceiling — unlike a genuine
165
+ // small count, a value at/above the cap is a LOWER BOUND, not an exact
166
+ // total). Below the cap `totalAvailable` is exact (the AI can tell a top-N
167
+ // slice from the full match set); at/above the cap we flag
168
+ // `totalIsLowerBound:true` + a note so a broad query that truly matches
169
+ // >10,000 sections is NOT reported as if exactly 10,000 (D1). A6: echo the
170
+ // applied title scope so the AI can VERIFY it searched the intended corpus —
171
+ // Title 48 (FAR) vs every CFR title — rather than silently trusting a filter
172
+ // that could return cross-title results if the eCFR param contract ever changes.
173
+ const returned = data.results.length;
174
+ const totalAvailable =
175
+ typeof json.meta?.total_count === "number" ? json.meta.total_count : null;
176
+ // eCFR's search total_count saturates at the Elasticsearch max_result_window
177
+ // (live-verified 10,000). Use `>= cap` (not `=== cap`) so a total AT OR ABOVE
178
+ // the ceiling is treated as a lower bound — strictly safe if the window were
179
+ // ever configured higher. Mirrors federal-register.ts's FR_COUNT_CAP and
180
+ // edgar.ts's FTS_WINDOW.
181
+ const totalIsLowerBound =
182
+ totalAvailable !== null && totalAvailable >= ECFR_TOTAL_COUNT_CAP;
183
+ const scopeNote =
184
+ args.titleNumber !== undefined
185
+ ? `searched CFR Title ${args.titleNumber}${
186
+ args.titleNumber === 48 ? " (FAR — Federal Acquisition Regulation)" : ""
187
+ } only`
188
+ : "searched all CFR titles (no title filter applied)";
189
+ const notes = [scopeNote];
190
+ if (totalIsLowerBound) {
191
+ notes.push(
192
+ `eCFR caps total_count at ${ECFR_TOTAL_COUNT_CAP} (Elasticsearch index.max_result_window); totalAvailable is a LOWER BOUND — the true match count may be higher and is UNKNOWN. See totalIsLowerBound. Narrow by title/chapter/date for an exact count.`,
193
+ );
194
+ }
195
+ return withMeta(data, {
196
+ source: "ecfr.gov/api (search/v1)",
197
+ keylessMode: true,
198
+ returned,
199
+ totalAvailable,
200
+ // Explicit boolean (not conditional): a below-cap count is DEFINITIVELY
201
+ // exact (totalIsLowerBound:false), an at/above-cap count is a lower bound
202
+ // (true). The AI can trust `false` as "this is the real total".
203
+ totalIsLowerBound,
204
+ truncated:
205
+ totalAvailable !== null ? returned < totalAvailable : undefined,
206
+ filtersApplied: args.titleNumber !== undefined ? ["titleNumber"] : [],
207
+ filtersDropped: [],
208
+ fieldsUnavailable: [],
209
+ notes,
210
+ });
114
211
  }
115
212
 
116
213
  function stripHtml(s: string): string {