@cliwant/mcp-sam-gov 0.2.1 → 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 +279 -67
  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 +42 -0
  14. package/dist/cache.d.ts.map +1 -0
  15. package/dist/cache.js +64 -0
  16. package/dist/cache.js.map +1 -0
  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 +92 -17
  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 +102 -0
  70. package/dist/errors.d.ts.map +1 -0
  71. package/dist/errors.js +247 -0
  72. package/dist/errors.js.map +1 -0
  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 +432 -15
  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 +114 -11
  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 +3897 -295
  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 +1664 -228
  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 +73 -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 +231 -127
  198. package/src/echo.ts +496 -0
  199. package/src/edgar.ts +3014 -0
  200. package/src/errors.ts +303 -0
  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 +706 -191
  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 +290 -155
  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 +4559 -327
  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 +2680 -925
  228. package/src/usitc.ts +420 -0
package/dist/grants.d.ts CHANGED
@@ -19,7 +19,7 @@ export declare function searchGrants(args: {
19
19
  oppNum?: string;
20
20
  oppStatuses?: GrantStatus[];
21
21
  rows?: number;
22
- }): Promise<{
22
+ }): Promise<import("./meta.js").MetaBundle<{
23
23
  totalRecords: number;
24
24
  grants: {
25
25
  id: string;
@@ -31,18 +31,40 @@ export declare function searchGrants(args: {
31
31
  closeDate: string | undefined;
32
32
  status: string | undefined;
33
33
  docType: string | undefined;
34
- cfdaList: string | undefined;
34
+ cfdaList: string[];
35
35
  }[];
36
- }>;
36
+ }>>;
37
37
  export declare function getGrant(args: {
38
38
  opportunityId: string;
39
39
  }): Promise<{
40
+ found: false;
41
+ opportunityId: string;
42
+ id?: undefined;
43
+ opportunityNumber?: undefined;
44
+ title?: undefined;
45
+ agency?: undefined;
46
+ description?: undefined;
47
+ postingDate?: undefined;
48
+ responseDate?: undefined;
49
+ archiveDate?: undefined;
50
+ awardCeiling?: undefined;
51
+ awardFloor?: undefined;
52
+ estimatedFunding?: undefined;
53
+ expectedNumberOfAwards?: undefined;
54
+ applicantTypes?: undefined;
55
+ fundingInstruments?: undefined;
56
+ fundingCategories?: undefined;
57
+ cfdaPrograms?: undefined;
58
+ } | {
59
+ found: true;
40
60
  id: number;
41
61
  opportunityNumber: string;
42
62
  title: string;
43
63
  agency: {
44
64
  code: string | undefined;
45
- name: string | undefined;
65
+ name: string | null;
66
+ department: string | null;
67
+ contactName: string | null;
46
68
  };
47
69
  description: string;
48
70
  postingDate: string | undefined;
@@ -59,5 +81,6 @@ export declare function getGrant(args: {
59
81
  number: string;
60
82
  title: string;
61
83
  }[];
84
+ opportunityId?: undefined;
62
85
  }>;
63
86
  //# sourceMappingURL=grants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"grants.d.ts","sourceRoot":"","sources":["../src/grants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAoBH,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC;AAE1E,wBAAsB,YAAY,CAAC,IAAI,EAAE;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,WAAW,EAAE,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;;;;;;;;;;;;;;GA+CA;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;oBAoDpC,MAAM,EAAE;wBAGR,MAAM,EAAE;uBAGR,MAAM,EAAE;;;;;GAMjC"}
1
+ {"version":3,"file":"grants.d.ts","sourceRoot":"","sources":["../src/grants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAyBH,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC;AAE1E,wBAAsB,YAAY,CAAC,IAAI,EAAE;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,WAAW,EAAE,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;;;;;;;;;;;;;;IAwIA;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBA6FpC,MAAM,EAAE;wBAGR,MAAM,EAAE;uBAGR,MAAM,EAAE;;;;;;GAMjC"}
package/dist/grants.js CHANGED
@@ -11,17 +11,17 @@
11
11
  *
12
12
  * Documented at https://grants.gov/web/grants/s2s/grantor/schemas/grants-search-2-soap.html
13
13
  */
14
+ import { fetchWithRetry } from "./errors.js";
15
+ import { driftError } from "./datasource.js";
16
+ import { withMeta } from "./meta.js";
14
17
  const GRANTS = "https://api.grants.gov/v1/api";
15
18
  async function postJson(endpoint, body) {
16
- const r = await fetch(`${GRANTS}/${endpoint}`, {
19
+ const r = await fetchWithRetry(`${GRANTS}/${endpoint}`, {
17
20
  method: "POST",
18
21
  headers: { "Content-Type": "application/json" },
19
22
  body: JSON.stringify(body),
20
23
  signal: AbortSignal.timeout(15_000),
21
- });
22
- if (!r.ok) {
23
- throw new Error(`Grants.gov ${endpoint} returned ${r.status}`);
24
- }
24
+ }, `grants.gov:${endpoint}`);
25
25
  return (await r.json());
26
26
  }
27
27
  export async function searchGrants(args) {
@@ -37,21 +37,98 @@ export async function searchGrants(args) {
37
37
  if (json.errorcode && json.errorcode !== 0) {
38
38
  throw new Error(`Grants.gov error: ${json.msg ?? "unknown"}`);
39
39
  }
40
- return {
41
- totalRecords: json.data?.hitCount ?? 0,
40
+ // F4 (P2 empty-vs-outage): a 200 that is NEITHER a recognized error envelope
41
+ // (handled above) NOR a `{data:{hitCount:number,…}}` shape is drift — a
42
+ // malformed / outage / interstitial body — and must NOT coalesce to
43
+ // `totalRecords:0` (an AUTHORITATIVE "no grants"). A GENUINE empty carries a
44
+ // numeric `data.hitCount:0` and stays an honest empty below. (Mirrors the
45
+ // govinfo/nih driftError precedent — a drift is a throw, never a fake-empty.)
46
+ const hitCount = json.data?.hitCount;
47
+ if (typeof hitCount !== "number") {
48
+ throw driftError("grants.gov", "Grants.gov search2 returned HTTP 200 but the body carries neither an error code nor a numeric data.hitCount — treating it as schema drift / an outage interstitial, NOT an empty result set.");
49
+ }
50
+ const totalRecords = hitCount;
51
+ const data = {
52
+ totalRecords,
42
53
  grants: (json.data?.oppHits ?? []).map((g) => ({
43
54
  id: g.id ?? "",
44
55
  opportunityNumber: g.number ?? "",
45
56
  title: g.title ?? "",
46
57
  agencyCode: g.agencyCode ?? "",
47
- agencyName: g.agencyName ?? "",
58
+ // GRANT-2: prefer `agency` (the real agency name) over the empty `agencyName`.
59
+ agencyName: g.agency ?? g.agencyName ?? "",
48
60
  openDate: g.openDate,
49
61
  closeDate: g.closeDate,
50
62
  status: g.oppStatus,
51
63
  docType: g.docType,
52
- cfdaList: g.cfdaList,
64
+ // E-2: always an array — normalize absent/legacy-string to []/[str]
65
+ // so consumers can rely on `cfdaList` being string[] unconditionally.
66
+ cfdaList: Array.isArray(g.cfdaList)
67
+ ? g.cfdaList
68
+ : g.cfdaList
69
+ ? [g.cfdaList]
70
+ : [],
53
71
  })),
54
72
  };
73
+ // Truthful `_meta` (spec §1.2 A4, §2.3). Grants.gov reports a real match
74
+ // total (`hitCount`), so `totalAvailable` is trustworthy. A4: Grants.gov
75
+ // silently IGNORES unknown agency codes / CFDA numbers (it returns the
76
+ // unfiltered set rather than erroring), and it does not echo back which
77
+ // filters it honored — so the AI cannot verify a filter took. We list the
78
+ // filters we SENT in `filtersApplied` and warn that unknown values are
79
+ // dropped silently, so the AI treats a suspiciously large result set with
80
+ // caution instead of asserting it is filtered.
81
+ const returned = data.grants.length;
82
+ const sent = [];
83
+ if (args.keyword)
84
+ sent.push("keyword");
85
+ if (args.cfda)
86
+ sent.push("cfda");
87
+ if (args.agency)
88
+ sent.push("agency");
89
+ if (args.oppNum)
90
+ sent.push("oppNum");
91
+ if (args.oppStatuses?.length)
92
+ sent.push("oppStatuses");
93
+ const notes = [];
94
+ if (args.agency || args.cfda) {
95
+ notes.push("Grants.gov silently ignores an unknown agency code or CFDA number (it returns the UNFILTERED result set instead of an error) and does not confirm which filters were honored — if the result count looks too broad, verify the agency/CFDA value.");
96
+ }
97
+ // VQ-1 (C82 dogfooding): Grants.gov OR-tokenizes multi-word keywords (matches ANY
98
+ // word) and does NOT support phrase quoting (a quoted "..." keyword returns 0). Live:
99
+ // "cybersecurity information technology" → 926 broad hits (State-Dept program
100
+ // statements top) vs 280 focused for "cybersecurity". The note is RESULT-AWARE
101
+ // (adversarial review F1/F2): "broad set ≠ no grants" only holds when there ARE
102
+ // results; a 0-result multi-word query needs the opposite advice. Quotes are stripped
103
+ // for the word count so a quote-wrapped phrase is still detected as multi-word.
104
+ const rawKw = args.keyword ?? "";
105
+ const looksQuoted = /^\s*["'][\s\S]*["']\s*$/.test(rawKw);
106
+ const kwWords = rawKw
107
+ .trim()
108
+ .replace(/^["']+|["']+$/g, "")
109
+ .trim()
110
+ // disclosure-split-ok: grants.gov OR-splits ONLY whitespace (and '+'), NOT punctuation — live-verified 2026-07; the shared DISCLOSURE_SPLIT_RE punctuation class must NOT be applied here (it would over-disclose). See ADR-0022.
111
+ .split(/\s+/)
112
+ .filter(Boolean);
113
+ if (kwWords.length > 1) {
114
+ if (totalRecords > 0) {
115
+ notes.push(`Grants.gov OR-matches multi-word keywords: it returns opportunities containing ANY of these ${kwWords.length} words, so this keyword may BROADEN results if any word is a common term ("information"/"technology"/"program" match many unrelated grants) — a broad, poorly-ranked set here does NOT mean "no relevant grants". For focused results pass ONE specific term (e.g. "cybersecurity"); Grants.gov does NOT support phrase quoting (a quoted "..." keyword returns 0). Narrow with cfda / agency / oppStatuses instead.`);
116
+ }
117
+ else {
118
+ notes.push(`This ${kwWords.length}-word keyword returned 0 results. Grants.gov OR-matches keywords and does NOT support phrase quoting${looksQuoted ? ' — your keyword appears quote-wrapped, so REMOVE the quotes (a quoted "..." keyword always returns 0)' : ' (a quoted "..." keyword returns 0)'}. A 0 here means even the OR of these words has no match — try each word separately, or narrow with cfda/agency/oppStatuses, to isolate.`);
119
+ }
120
+ }
121
+ return withMeta(data, {
122
+ source: "grants.gov/api (search2)",
123
+ keylessMode: true,
124
+ returned,
125
+ totalAvailable: totalRecords,
126
+ truncated: returned < totalRecords,
127
+ filtersApplied: sent,
128
+ filtersDropped: [],
129
+ fieldsUnavailable: [],
130
+ notes,
131
+ });
55
132
  }
56
133
  export async function getGrant(args) {
57
134
  const json = await postJson("fetchOpportunity", {
@@ -61,12 +138,38 @@ export async function getGrant(args) {
61
138
  throw new Error(`Grants.gov error: ${json.msg ?? "unknown"}`);
62
139
  }
63
140
  const d = json.data ?? {};
141
+ // NOT-FOUND: Grants.gov's fetchOpportunity returns errorcode:0 ("Webservice
142
+ // Succeeds") + a HOLLOW `data` object even for a NONEXISTENT opportunityId
143
+ // (LIVE-VERIFIED 2026-07-06: id 999999999 → errorcode 0 but data carries NO
144
+ // id / opportunityNumber / opportunityTitle / synopsis — only skeleton fields).
145
+ // Mapping that shell would fabricate a grant (id:0, title:"") for an opportunity
146
+ // that does NOT exist — an absence-as-present lie. `id` is the reliable signal:
147
+ // a real grant ALWAYS carries a numeric `id`, the hollow shell never does.
148
+ if (d.id === undefined || d.id === null) {
149
+ return { found: false, opportunityId: args.opportunityId };
150
+ }
64
151
  const s = d.synopsis ?? {};
152
+ const rawContact = s.agencyContactName ?? s.agencyName ?? null;
65
153
  return {
66
- id: d.id ?? 0,
154
+ found: true,
155
+ id: d.id, // guaranteed present past the not-found guard above
67
156
  opportunityNumber: d.opportunityNumber ?? "",
68
157
  title: d.opportunityTitle ?? "",
69
- agency: { code: s.agencyCode ?? d.owningAgencyCode, name: s.agencyName },
158
+ // GRANT-2: `name` is the REAL agency (subtier preferred, else the department),
159
+ // NOT synopsis.agencyName (which is the contact person). Fall back through both
160
+ // the synopsis and top-level `data` locations. `department` is the top-tier
161
+ // agency; `contactName` preserves the person the mislabeled old `name` held
162
+ // (newlines collapsed so it renders on one line).
163
+ agency: {
164
+ code: s.agencyCode ?? d.owningAgencyCode,
165
+ name: s.agencyDetails?.agencyName ??
166
+ d.agencyDetails?.agencyName ??
167
+ s.topAgencyDetails?.agencyName ??
168
+ d.topAgencyDetails?.agencyName ??
169
+ null,
170
+ department: s.topAgencyDetails?.agencyName ?? d.topAgencyDetails?.agencyName ?? null,
171
+ contactName: rawContact ? rawContact.replace(/\s*\n\s*/g, " — ") : null,
172
+ },
70
173
  description: s.synopsisDesc ?? d.synopsisDesc ?? "",
71
174
  postingDate: s.postingDate,
72
175
  responseDate: s.responseDate,
@@ -1 +1 @@
1
- {"version":3,"file":"grants.js","sourceRoot":"","sources":["../src/grants.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"grants.js","sourceRoot":"","sources":["../src/grants.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,249 @@
1
+ /**
2
+ * GSA daily-CSV keyless backbone (opt-in) — page-completing enrichment for the
3
+ * fields the keyless SAM HAL list endpoint nulls.
4
+ *
5
+ * Why this exists
6
+ * ----------------
7
+ * `sam_search_opportunities` (keyless HAL) returns a page of notices but nulls
8
+ * each row's naics / setAside / place-of-performance / responseDeadline / type
9
+ * (the list payload omits those VALUES even though the server-side filter
10
+ * honored them). Today the only fill path is `sam_get_opportunity` PER notice —
11
+ * N detail calls to complete an N-row page.
12
+ *
13
+ * The GSA "Contract Opportunities" daily bulk CSV carries all of those fields
14
+ * keyed by NoticeId (the 32-hex id that equals the HAL `_id`). So ONE cached
15
+ * index lookup completes a whole page, and it's a durability hedge if the
16
+ * undocumented HAL list drifts.
17
+ *
18
+ * Source — LIVE-VERIFIED 2026-07-03
19
+ * ---------------------------------
20
+ * https://falextracts.s3.amazonaws.com/Contract Opportunities/datagov/ContractOpportunitiesFullCSV.csv
21
+ * HEAD 200, 225,722,960 bytes (225.7 MB), `last-modified` daily,
22
+ * `accept-ranges: bytes`, 47 columns. Key + HAL-nulled fields (0-indexed):
23
+ * NoticeId@0 (32-hex == HAL _id), Title@1, Type@10, SetASideCode@14,
24
+ * SetASide@15, ResponseDeadLine@16, NaicsCode@17, PopCity@20, PopState@21,
25
+ * PopZip@22, PopCountry@23, Active@24, Description@46.
26
+ *
27
+ * Gold-standard verification (2026-07-03): for a live notice
28
+ * (a4be592da0304872a252980925b9458f) the CSV index's naics/PoP EQUAL what
29
+ * `sam_get_opportunity` (detail) returns — naics 238990, PoP WA / Hoodsport /
30
+ * 98548 all match; setAsideCode 'SBA' equals detail's mapped setAside 'SBA'.
31
+ *
32
+ * The non-negotiable design constraints
33
+ * -------------------------------------
34
+ * 1. OFF BY DEFAULT. No forced 225 MB download. Enabled only via env
35
+ * (`SAM_GOV_CSV_CACHE` = a cache dir, or `SAM_GOV_ENABLE_CSV=1` to use a
36
+ * default dir under the OS temp). Every EXISTING keyless HAL tool is
37
+ * UNCHANGED.
38
+ * 2. BOUNDED RAM. Never hold the 225 MB in memory. Stream the download to a
39
+ * cache FILE on disk. Build a COMPACT index of only the ~11 needed columns
40
+ * (NOT Description@46), keyed by NoticeId (~24 MB for this snapshot's
41
+ * ~78k rows, verified). Persist the compact index to the cache dir and
42
+ * load THAT into a Map; refresh when > 24 h old or the CSV `last-modified`
43
+ * changed.
44
+ * 3. STREAMING RFC-4180 parser, NO new npm dependency. Fields are quoted and
45
+ * some (Title, addresses) embed commas; a state machine handles the
46
+ * in-quote toggle, `""`→`"` escape, and newlines inside quotes. Since we
47
+ * skip Description@46 (the worst offender) we only parse through column 24.
48
+ * 4. TRUTHFUL `_meta` (the product): honest source, freshness (CSV
49
+ * last-modified + index build time), degraded on download/parse failure
50
+ * (never a silent empty), and per-noticeId a notice ABSENT from the
51
+ * current snapshot → `found:false` + nulls + an explicit "not in current
52
+ * CSV snapshot" disclosure, NEVER faked. DISABLED → a structured note
53
+ * explaining how to enable it (not an error, not fake data). A cold-cache
54
+ * first call that triggers the download is slow → disclosed as "warming".
55
+ */
56
+ /** The GSA daily bulk CSV (keyless). Space in the path is URL-encoded. */
57
+ export declare const GSA_CSV_URL = "https://falextracts.s3.amazonaws.com/Contract%20Opportunities/datagov/ContractOpportunitiesFullCSV.csv";
58
+ export declare const GSA_CSV_SOURCE = "gsa.gov daily bulk CSV (keyless)";
59
+ /** Max noticeIds accepted per batch (completes a `sam_search_opportunities` page). */
60
+ export declare const MAX_NOTICE_IDS = 100;
61
+ /**
62
+ * Resolve the opt-in config from the environment.
63
+ *
64
+ * Enablement (in priority order):
65
+ * - `SAM_GOV_CSV_FIXTURE` = path to a LOCAL CSV file → enabled, no network
66
+ * (used by tests and for pinning a pre-downloaded file). The cache dir
67
+ * defaults next to the fixture unless `SAM_GOV_CSV_CACHE` is also set.
68
+ * - `SAM_GOV_CSV_CACHE` = a cache DIRECTORY path → enabled; downloads live.
69
+ * - `SAM_GOV_ENABLE_CSV` truthy (`1`/`true`/`yes`) → enabled with a default
70
+ * cache dir under the OS temp (`<tmp>/mcp-sam-gov-csv`).
71
+ * - otherwise DISABLED (default) — the backbone never touches the network.
72
+ */
73
+ export type CsvConfig = {
74
+ enabled: boolean;
75
+ cacheDir: string;
76
+ /** A local CSV to index instead of downloading (tests / pinned file). */
77
+ fixturePath: string | null;
78
+ };
79
+ export declare function resolveCsvConfig(env?: NodeJS.ProcessEnv): CsvConfig;
80
+ /**
81
+ * Parse one LOGICAL CSV record's text into fields 0..maxCol (inclusive).
82
+ *
83
+ * A proper state machine (NOT `split(",")`): tracks an in-quote toggle, turns
84
+ * `""` into a single `"`, and treats commas/CR/quotes literally while inside a
85
+ * quoted field. It also reports whether parsing ended INSIDE an open quote —
86
+ * the record-assembler uses that to know a physical newline fell inside a
87
+ * quoted field and the logical record continues on the next line.
88
+ *
89
+ * CRITICAL — we must scan the WHOLE text to get quote parity right, even though
90
+ * we only KEEP fields up to `maxCol`. Once we're past `maxCol` we stop STORING
91
+ * fields (so the giant Description@46 is never materialized — bounded memory),
92
+ * but we KEEP walking the characters to maintain the in-quote toggle. Early-
93
+ * returning at `maxCol` would misjudge a newline that falls inside a LATER
94
+ * quoted field (e.g. an embedded newline in Description@46): the record would
95
+ * be wrongly treated as complete and the field's tail would corrupt the next
96
+ * record. (This was a real bug — the fix is: cap storage, never cap scanning.)
97
+ */
98
+ export declare function parseRecordFields(text: string, maxCol?: number): {
99
+ fields: string[];
100
+ inQuotes: boolean;
101
+ };
102
+ /** The compact per-notice record we keep in the index (NOT Description). */
103
+ export type NoticeFields = {
104
+ title: string;
105
+ type: string;
106
+ setAsideCode: string;
107
+ setAside: string;
108
+ responseDeadline: string;
109
+ naicsCode: string;
110
+ popCity: string;
111
+ popState: string;
112
+ popZip: string;
113
+ popCountry: string;
114
+ active: string;
115
+ };
116
+ /** TEST-ONLY: drop the process-lifetime memo so a test can re-point the env. */
117
+ export declare function _resetIndexForTests(): void;
118
+ /** A read-only view of a loaded index for enrichment consumers. */
119
+ export type ReadyIndex = {
120
+ get(noticeId: string): NoticeFields | undefined;
121
+ csvLastModified: string | null;
122
+ indexBuiltAt: string;
123
+ rowCount: number;
124
+ };
125
+ /**
126
+ * NON-BLOCKING: return a ready CSV index if one is loaded + fresh, else null.
127
+ *
128
+ * Guarantees for the search hot-path:
129
+ * - Disabled config → null (no work, no network) — caller skips enrichment.
130
+ * - An in-memory index within its TTL → returned synchronously (no HEAD, no
131
+ * download).
132
+ * - No in-memory index (cold) → returns null IMMEDIATELY and, unless a
133
+ * refresh is already in flight, kicks off a background `ensureIndex` (its
134
+ * result is memoized into `loaded` for a subsequent call). The promise is
135
+ * deliberately NOT awaited here and its rejection is swallowed so a failed
136
+ * warm never surfaces on the search path.
137
+ * - An in-memory index PAST its TTL → still returned (stale-but-usable) while
138
+ * a background refresh is kicked off; the caller discloses the age via
139
+ * freshness so a slightly-stale snapshot is honest, never a stall.
140
+ *
141
+ * This never throws — any misconfiguration or I/O error degrades to null.
142
+ */
143
+ export declare function tryGetReadyIndex(cfg?: CsvConfig): ReadyIndex | null;
144
+ /** One opportunity row as emitted by the keyless sam_search handler. */
145
+ export type SearchOppRow = {
146
+ noticeId: string;
147
+ title?: string | null;
148
+ agency?: string | null;
149
+ solicitationNumber?: string | null;
150
+ responseDeadline?: string | null;
151
+ naics?: string | null;
152
+ setAside?: string | null;
153
+ uiLink?: string | null;
154
+ type?: string | null;
155
+ placeOfPerformance?: SamCsvPlaceOfPerformance | null;
156
+ [k: string]: unknown;
157
+ };
158
+ /** Place-of-performance shape composed from the CSV Pop* columns. */
159
+ export type SamCsvPlaceOfPerformance = {
160
+ city: string | null;
161
+ state: string | null;
162
+ zip: string | null;
163
+ country: string | null;
164
+ };
165
+ export type EnrichmentOutcome = {
166
+ /** The page with null fields filled from the CSV where the notice was found. */
167
+ opportunities: SearchOppRow[];
168
+ /** Notices in the page that were present in the CSV snapshot. */
169
+ foundCount: number;
170
+ /** Notices in the page absent from the CSV snapshot (left un-enriched). */
171
+ missingCount: number;
172
+ /** Union of field names filled on ≥1 row (subset of naics/setAside/… ). */
173
+ fieldsFilled: Set<string>;
174
+ /** Snapshot freshness, mirrored into data + _meta by the caller. */
175
+ freshness: {
176
+ csvLastModified: string | null;
177
+ indexBuiltAt: string;
178
+ indexAgeHours: number | null;
179
+ rowCount: number;
180
+ };
181
+ };
182
+ /**
183
+ * Fill the keyless search page's null naics/setAside/placeOfPerformance (and
184
+ * responseDeadline/type when currently null) from the ready CSV index.
185
+ *
186
+ * Rules (honesty):
187
+ * - Only a null field is filled — a value already present (e.g. keyed mode,
188
+ * or a HAL row that carried the value) is NEVER overwritten.
189
+ * - A field is filled only when the CSV cell is a real non-empty value (an
190
+ * empty CSV cell stays null — absence ≠ empty).
191
+ * - `type`/`placeOfPerformance` keys are ADDED only when a value is actually
192
+ * filled from the CSV, so a not-in-snapshot row keeps the exact original
193
+ * shape (no spurious null keys).
194
+ * - A noticeId absent from the snapshot is left byte-identical + counted.
195
+ */
196
+ export declare function enrichSearchOpportunities(opportunities: SearchOppRow[], index: ReadyIndex): EnrichmentOutcome;
197
+ export type LookupResult = {
198
+ noticeId: string;
199
+ found: boolean;
200
+ naicsCode: string | null;
201
+ setAside: string | null;
202
+ setAsideCode: string | null;
203
+ popState: string | null;
204
+ popCity: string | null;
205
+ popZip: string | null;
206
+ popCountry: string | null;
207
+ responseDeadline: string | null;
208
+ type: string | null;
209
+ active: boolean | null;
210
+ title: string | null;
211
+ };
212
+ /**
213
+ * BATCH keyless enrichment: for each 32-hex noticeId return the HAL-nulled
214
+ * fields (naics / setAside / place-of-performance / responseDeadline / type /
215
+ * active / title) from the cached GSA CSV index — completing a whole
216
+ * `sam_search_opportunities` page in ONE call.
217
+ *
218
+ * Honesty contract (the product):
219
+ * - DISABLED (default) → `data.enabled:false`, every row `found:false`+nulls,
220
+ * and `_meta.notes` explains how to enable the backbone. NOT an error, NOT
221
+ * fake data.
222
+ * - a noticeId ABSENT from the current snapshot → `found:false` + nulls + a
223
+ * disclosure that it is "not in the current CSV snapshot", NEVER faked.
224
+ * - download/parse failure → `degraded` (a structured error is thrown and
225
+ * surfaced by the server as a retryable envelope) — never a silent empty.
226
+ * - `_meta.freshness` carries the CSV `last-modified` + the index build time
227
+ * so the AI knows how stale the snapshot is; a cold-cache first call that
228
+ * triggered the download is disclosed as "warming".
229
+ */
230
+ export declare function lookupNoticeFields(args: {
231
+ noticeIds: string[];
232
+ }, env?: NodeJS.ProcessEnv): Promise<import("./meta.js").MetaBundle<{
233
+ results: LookupResult[];
234
+ enabled: boolean;
235
+ freshness: null;
236
+ }> | import("./meta.js").MetaBundle<{
237
+ results: LookupResult[];
238
+ enabled: boolean;
239
+ freshness: {
240
+ csvLastModified: string | null;
241
+ indexBuiltAt: string;
242
+ indexAgeHours: number | null;
243
+ rowCount: number;
244
+ warming: boolean;
245
+ };
246
+ foundCount: number;
247
+ missingCount: number;
248
+ }>>;
249
+ //# sourceMappingURL=gsa-csv.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gsa-csv.d.ts","sourceRoot":"","sources":["../src/gsa-csv.ts"],"names":[],"mappings":"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"}