@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
package/src/grants.ts CHANGED
@@ -13,6 +13,8 @@
13
13
  */
14
14
 
15
15
  import { fetchWithRetry } from "./errors.js";
16
+ import { driftError } from "./datasource.js";
17
+ import { withMeta } from "./meta.js";
16
18
 
17
19
  const GRANTS = "https://api.grants.gov/v1/api";
18
20
 
@@ -61,12 +63,20 @@ export async function searchGrants(args: {
61
63
  number?: string;
62
64
  title?: string;
63
65
  agencyCode?: string;
66
+ // GRANT-2 (live-verified 2026-07-06): search2 returns the real agency NAME
67
+ // in `agency` (e.g. "Food and Nutrition Service"); `agencyName` is
68
+ // empty/absent on the search rows. Type both; the mapping prefers `agency`.
69
+ agency?: string;
64
70
  agencyName?: string;
65
71
  openDate?: string;
66
72
  closeDate?: string;
67
73
  oppStatus?: string;
68
74
  docType?: string;
69
- cfdaList?: string;
75
+ // E-2 (spec §1.6, §5): the Grants.gov search2 response returns
76
+ // cfdaList as an ARRAY of CFDA numbers (e.g. ["19.441"]) — NOT a
77
+ // pipe-delimited string as the type previously declared. Correct the
78
+ // type to match the runtime shape.
79
+ cfdaList?: string[];
70
80
  }[];
71
81
  };
72
82
  };
@@ -74,21 +84,102 @@ export async function searchGrants(args: {
74
84
  if (json.errorcode && json.errorcode !== 0) {
75
85
  throw new Error(`Grants.gov error: ${json.msg ?? "unknown"}`);
76
86
  }
77
- return {
78
- totalRecords: json.data?.hitCount ?? 0,
87
+ // F4 (P2 empty-vs-outage): a 200 that is NEITHER a recognized error envelope
88
+ // (handled above) NOR a `{data:{hitCount:number,…}}` shape is drift — a
89
+ // malformed / outage / interstitial body — and must NOT coalesce to
90
+ // `totalRecords:0` (an AUTHORITATIVE "no grants"). A GENUINE empty carries a
91
+ // numeric `data.hitCount:0` and stays an honest empty below. (Mirrors the
92
+ // govinfo/nih driftError precedent — a drift is a throw, never a fake-empty.)
93
+ const hitCount = json.data?.hitCount;
94
+ if (typeof hitCount !== "number") {
95
+ throw driftError(
96
+ "grants.gov",
97
+ "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.",
98
+ );
99
+ }
100
+ const totalRecords = hitCount;
101
+ const data = {
102
+ totalRecords,
79
103
  grants: (json.data?.oppHits ?? []).map((g) => ({
80
104
  id: g.id ?? "",
81
105
  opportunityNumber: g.number ?? "",
82
106
  title: g.title ?? "",
83
107
  agencyCode: g.agencyCode ?? "",
84
- agencyName: g.agencyName ?? "",
108
+ // GRANT-2: prefer `agency` (the real agency name) over the empty `agencyName`.
109
+ agencyName: g.agency ?? g.agencyName ?? "",
85
110
  openDate: g.openDate,
86
111
  closeDate: g.closeDate,
87
112
  status: g.oppStatus,
88
113
  docType: g.docType,
89
- cfdaList: g.cfdaList,
114
+ // E-2: always an array — normalize absent/legacy-string to []/[str]
115
+ // so consumers can rely on `cfdaList` being string[] unconditionally.
116
+ cfdaList: Array.isArray(g.cfdaList)
117
+ ? g.cfdaList
118
+ : g.cfdaList
119
+ ? [g.cfdaList as unknown as string]
120
+ : [],
90
121
  })),
91
122
  };
123
+
124
+ // Truthful `_meta` (spec §1.2 A4, §2.3). Grants.gov reports a real match
125
+ // total (`hitCount`), so `totalAvailable` is trustworthy. A4: Grants.gov
126
+ // silently IGNORES unknown agency codes / CFDA numbers (it returns the
127
+ // unfiltered set rather than erroring), and it does not echo back which
128
+ // filters it honored — so the AI cannot verify a filter took. We list the
129
+ // filters we SENT in `filtersApplied` and warn that unknown values are
130
+ // dropped silently, so the AI treats a suspiciously large result set with
131
+ // caution instead of asserting it is filtered.
132
+ const returned = data.grants.length;
133
+ const sent: string[] = [];
134
+ if (args.keyword) sent.push("keyword");
135
+ if (args.cfda) sent.push("cfda");
136
+ if (args.agency) sent.push("agency");
137
+ if (args.oppNum) sent.push("oppNum");
138
+ if (args.oppStatuses?.length) sent.push("oppStatuses");
139
+ const notes: string[] = [];
140
+ if (args.agency || args.cfda) {
141
+ notes.push(
142
+ "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.",
143
+ );
144
+ }
145
+ // VQ-1 (C82 dogfooding): Grants.gov OR-tokenizes multi-word keywords (matches ANY
146
+ // word) and does NOT support phrase quoting (a quoted "..." keyword returns 0). Live:
147
+ // "cybersecurity information technology" → 926 broad hits (State-Dept program
148
+ // statements top) vs 280 focused for "cybersecurity". The note is RESULT-AWARE
149
+ // (adversarial review F1/F2): "broad set ≠ no grants" only holds when there ARE
150
+ // results; a 0-result multi-word query needs the opposite advice. Quotes are stripped
151
+ // for the word count so a quote-wrapped phrase is still detected as multi-word.
152
+ const rawKw = args.keyword ?? "";
153
+ const looksQuoted = /^\s*["'][\s\S]*["']\s*$/.test(rawKw);
154
+ const kwWords = rawKw
155
+ .trim()
156
+ .replace(/^["']+|["']+$/g, "")
157
+ .trim()
158
+ // 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.
159
+ .split(/\s+/)
160
+ .filter(Boolean);
161
+ if (kwWords.length > 1) {
162
+ if (totalRecords > 0) {
163
+ notes.push(
164
+ `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.`,
165
+ );
166
+ } else {
167
+ notes.push(
168
+ `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.`,
169
+ );
170
+ }
171
+ }
172
+ return withMeta(data, {
173
+ source: "grants.gov/api (search2)",
174
+ keylessMode: true,
175
+ returned,
176
+ totalAvailable: totalRecords,
177
+ truncated: returned < totalRecords,
178
+ filtersApplied: sent,
179
+ filtersDropped: [],
180
+ fieldsUnavailable: [],
181
+ notes,
182
+ });
92
183
  }
93
184
 
94
185
  export async function getGrant(args: { opportunityId: string }) {
@@ -101,6 +192,11 @@ export async function getGrant(args: { opportunityId: string }) {
101
192
  opportunityTitle?: string;
102
193
  owningAgencyCode?: string;
103
194
  synopsisDesc?: string;
195
+ // GRANT-2: fetchOpportunity carries agencyDetails/topAgencyDetails at the TOP
196
+ // level too (sibling to synopsis) — a sparse-synopsis (e.g. forecasted) record
197
+ // may have them here but not under synopsis, so they are in the fallback chain.
198
+ agencyDetails?: { agencyName?: string; agencyCode?: string };
199
+ topAgencyDetails?: { agencyName?: string; agencyCode?: string };
104
200
  synopsis?: {
105
201
  synopsisDesc?: string;
106
202
  applicantTypes?: { description?: string }[];
@@ -115,6 +211,14 @@ export async function getGrant(args: { opportunityId: string }) {
115
211
  expectedNumberOfAwards?: number;
116
212
  agencyName?: string;
117
213
  agencyCode?: string;
214
+ // GRANT-2 (live-verified 2026-07-06): synopsis.agencyName is the CONTACT
215
+ // PERSON (== agencyContactName, e.g. "Andrew Day\nGrants/Agreements
216
+ // Officer"), NOT the agency. The real agency name lives in agencyDetails
217
+ // (subtier, e.g. "Food and Nutrition Service") and topAgencyDetails
218
+ // (department, e.g. "Department of Agriculture").
219
+ agencyContactName?: string;
220
+ agencyDetails?: { agencyName?: string; agencyCode?: string };
221
+ topAgencyDetails?: { agencyName?: string; agencyCode?: string };
118
222
  };
119
223
  opportunityHistoryDetails?: { actionType?: string; actionDate?: string }[];
120
224
  cfdas?: { cfdaNumber?: string; programTitle?: string }[];
@@ -127,12 +231,40 @@ export async function getGrant(args: { opportunityId: string }) {
127
231
  throw new Error(`Grants.gov error: ${json.msg ?? "unknown"}`);
128
232
  }
129
233
  const d = json.data ?? {};
234
+ // NOT-FOUND: Grants.gov's fetchOpportunity returns errorcode:0 ("Webservice
235
+ // Succeeds") + a HOLLOW `data` object even for a NONEXISTENT opportunityId
236
+ // (LIVE-VERIFIED 2026-07-06: id 999999999 → errorcode 0 but data carries NO
237
+ // id / opportunityNumber / opportunityTitle / synopsis — only skeleton fields).
238
+ // Mapping that shell would fabricate a grant (id:0, title:"") for an opportunity
239
+ // that does NOT exist — an absence-as-present lie. `id` is the reliable signal:
240
+ // a real grant ALWAYS carries a numeric `id`, the hollow shell never does.
241
+ if (d.id === undefined || d.id === null) {
242
+ return { found: false as const, opportunityId: args.opportunityId };
243
+ }
130
244
  const s = d.synopsis ?? {};
245
+ const rawContact = s.agencyContactName ?? s.agencyName ?? null;
131
246
  return {
132
- id: d.id ?? 0,
247
+ found: true as const,
248
+ id: d.id, // guaranteed present past the not-found guard above
249
+
133
250
  opportunityNumber: d.opportunityNumber ?? "",
134
251
  title: d.opportunityTitle ?? "",
135
- agency: { code: s.agencyCode ?? d.owningAgencyCode, name: s.agencyName },
252
+ // GRANT-2: `name` is the REAL agency (subtier preferred, else the department),
253
+ // NOT synopsis.agencyName (which is the contact person). Fall back through both
254
+ // the synopsis and top-level `data` locations. `department` is the top-tier
255
+ // agency; `contactName` preserves the person the mislabeled old `name` held
256
+ // (newlines collapsed so it renders on one line).
257
+ agency: {
258
+ code: s.agencyCode ?? d.owningAgencyCode,
259
+ name:
260
+ s.agencyDetails?.agencyName ??
261
+ d.agencyDetails?.agencyName ??
262
+ s.topAgencyDetails?.agencyName ??
263
+ d.topAgencyDetails?.agencyName ??
264
+ null,
265
+ department: s.topAgencyDetails?.agencyName ?? d.topAgencyDetails?.agencyName ?? null,
266
+ contactName: rawContact ? rawContact.replace(/\s*\n\s*/g, " — ") : null,
267
+ },
136
268
  description: s.synopsisDesc ?? d.synopsisDesc ?? "",
137
269
  postingDate: s.postingDate,
138
270
  responseDate: s.responseDate,