@cliwant/mcp-sam-gov 1.5.0 → 1.7.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 (89) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +248 -231
  3. package/README.ko.md +248 -231
  4. package/README.md +733 -714
  5. package/dist/errors.d.ts +10 -0
  6. package/dist/errors.d.ts.map +1 -1
  7. package/dist/errors.js.map +1 -1
  8. package/dist/feedback.d.ts +64 -0
  9. package/dist/feedback.d.ts.map +1 -0
  10. package/dist/feedback.js +131 -0
  11. package/dist/feedback.js.map +1 -0
  12. package/dist/server.d.ts.map +1 -1
  13. package/dist/server.js +48 -2
  14. package/dist/server.js.map +1 -1
  15. package/dist/update-check.d.ts +38 -0
  16. package/dist/update-check.d.ts.map +1 -0
  17. package/dist/update-check.js +85 -0
  18. package/dist/update-check.js.map +1 -0
  19. package/package.json +111 -111
  20. package/src/attachments.ts +652 -652
  21. package/src/bea.ts +372 -372
  22. package/src/bls.ts +1943 -1943
  23. package/src/cache.ts +73 -73
  24. package/src/cbp-border.ts +177 -177
  25. package/src/census-economic.ts +431 -431
  26. package/src/census.ts +735 -735
  27. package/src/ckan.ts +495 -495
  28. package/src/clinicaltrials.ts +923 -923
  29. package/src/cms-facility.ts +379 -379
  30. package/src/cms-hospital.ts +344 -344
  31. package/src/cms-supplier.ts +527 -527
  32. package/src/cms-utilization.ts +389 -389
  33. package/src/cms.ts +634 -634
  34. package/src/coerce.ts +47 -47
  35. package/src/courtlistener.ts +465 -465
  36. package/src/cpsc.ts +333 -333
  37. package/src/datagov-catalog.ts +312 -312
  38. package/src/datagov.ts +907 -907
  39. package/src/datagovKey.ts +68 -68
  40. package/src/datasource.ts +721 -721
  41. package/src/disclosure.ts +61 -61
  42. package/src/dol.ts +515 -515
  43. package/src/ecfr.ts +248 -248
  44. package/src/echo.ts +496 -496
  45. package/src/edgar.ts +3046 -3046
  46. package/src/epa-envirofacts.ts +358 -358
  47. package/src/errors.ts +324 -314
  48. package/src/fac.ts +529 -529
  49. package/src/far.ts +1009 -1009
  50. package/src/fdic.ts +2052 -2052
  51. package/src/federal-register.ts +725 -725
  52. package/src/feedback.ts +160 -0
  53. package/src/fema.ts +680 -680
  54. package/src/fpds.ts +620 -620
  55. package/src/fred.ts +464 -464
  56. package/src/gao.ts +744 -744
  57. package/src/gov-domains.ts +237 -237
  58. package/src/govinfo.ts +497 -497
  59. package/src/grants.ts +290 -290
  60. package/src/gsa-csv.ts +992 -992
  61. package/src/gsa-perdiem.ts +361 -361
  62. package/src/integrity.ts +928 -928
  63. package/src/keys.ts +268 -268
  64. package/src/lda.ts +385 -385
  65. package/src/meta.ts +292 -292
  66. package/src/nhtsa.ts +352 -352
  67. package/src/nih.ts +375 -375
  68. package/src/nist-controls.ts +219 -219
  69. package/src/nonprofit.ts +460 -460
  70. package/src/nppes.ts +834 -834
  71. package/src/nsf.ts +706 -706
  72. package/src/nvd.ts +1124 -1124
  73. package/src/nws-weather.ts +167 -167
  74. package/src/ofac.ts +1166 -1166
  75. package/src/openfda-device.ts +356 -356
  76. package/src/openfda-drugsfda.ts +313 -313
  77. package/src/openfda.ts +518 -518
  78. package/src/pricing.ts +1075 -1075
  79. package/src/sam-gov/client.ts +774 -774
  80. package/src/sam-gov/index.ts +32 -32
  81. package/src/sam-gov/types.ts +152 -152
  82. package/src/sba.ts +357 -357
  83. package/src/server.ts +6692 -6639
  84. package/src/snapshot.ts +223 -223
  85. package/src/socrata.ts +532 -532
  86. package/src/treasury.ts +582 -582
  87. package/src/update-check.ts +88 -0
  88. package/src/usaspending.ts +2852 -2852
  89. package/src/usitc.ts +420 -420
package/src/ecfr.ts CHANGED
@@ -1,248 +1,248 @@
1
- /**
2
- * eCFR (Electronic Code of Federal Regulations) wrappers (keyless).
3
- *
4
- * eCFR is the up-to-date version of the CFR — Title 48 = FAR (Federal
5
- * Acquisition Regulation), Title 2 = Federal financial assistance, etc.
6
- * For a federal contractor, eCFR is the primary source for regulation
7
- * text the agent should quote when answering compliance questions.
8
- *
9
- * Endpoints:
10
- * - /versioner/v1/titles.json — list 50 CFR titles + last-amended dates
11
- * - /search/v1/results — full-text search across the entire CFR
12
- *
13
- * Both keyless. Documented at https://www.ecfr.gov/developers/.
14
- */
15
-
16
- import { fetchWithRetry } from "./errors.js";
17
- import { driftError } from "./datasource.js";
18
- import { memoize } from "./cache.js";
19
- import { withMeta } from "./meta.js";
20
-
21
- const ECFR = "https://www.ecfr.gov/api";
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
-
29
- async function fetchJson<T>(url: string): Promise<T> {
30
- const r = await fetchWithRetry(
31
- url,
32
- {
33
- headers: { Accept: "application/json" },
34
- signal: AbortSignal.timeout(15_000),
35
- },
36
- `ecfr:${url.split("/api/")[1] ?? url}`,
37
- );
38
- return (await r.json()) as T;
39
- }
40
-
41
- export async function listTitles() {
42
- // 50 CFR titles change very infrequently. Cache aggressively (5 min).
43
- return memoize("ecfr:titles", async () => {
44
- type Resp = {
45
- titles?: {
46
- number?: number;
47
- name?: string;
48
- latest_amended_on?: string;
49
- latest_issue_date?: string;
50
- up_to_date_as_of?: string;
51
- reserved?: boolean;
52
- }[];
53
- };
54
- const json = await fetchJson<Resp>(`${ECFR}/versioner/v1/titles.json`);
55
- const titles = (json.titles ?? []).map((t) => ({
56
- number: t.number ?? 0,
57
- name: t.name ?? "",
58
- latestAmendedOn: t.latest_amended_on,
59
- latestIssueDate: t.latest_issue_date,
60
- upToDateAsOf: t.up_to_date_as_of,
61
- reserved: !!t.reserved,
62
- }));
63
- // Carry the honesty envelope every other tool has (dogfooding 2026-07-15: this
64
- // list tool previously returned a bare {titles} with no _meta). The titles
65
- // endpoint returns the COMPLETE canonical CFR title set in one response — no
66
- // pagination, no filter — so the count IS the total and complete derives true.
67
- return withMeta(
68
- { titles },
69
- {
70
- source: "ecfr.gov/api (versioner/v1/titles)",
71
- keylessMode: true,
72
- returned: titles.length,
73
- totalAvailable: titles.length,
74
- truncated: false,
75
- filtersApplied: [],
76
- filtersDropped: [],
77
- notes: [
78
- "Complete canonical list of all CFR titles — the endpoint returns every title in a single response (no pagination or filtering).",
79
- ],
80
- },
81
- );
82
- });
83
- }
84
-
85
- export async function search(args: {
86
- query: string;
87
- titleNumber?: number;
88
- chapter?: number;
89
- perPage?: number;
90
- }) {
91
- const url = new URL(`${ECFR}/search/v1/results`);
92
- url.searchParams.set("query", args.query);
93
- url.searchParams.set("per_page", String(args.perPage ?? 5));
94
- if (args.titleNumber) {
95
- // eCFR search filter: hierarchy[title]=N (NOT just title=N — that's
96
- // an "unpermitted parameter" error from the eCFR API).
97
- url.searchParams.set("hierarchy[title]", String(args.titleNumber));
98
- }
99
- // Optional chapter filter (additive; existing ecfr_search callers pass none).
100
- // Within Title 48: chapter 1 = FAR, chapter 2 = DFARS, chapter 5 = GSAM, etc.
101
- // This is what lets far_search scope to FAR/DFARS and keep GSAM/agency
102
- // supplements out server-side. Same hierarchy[…] contract as the title filter.
103
- if (args.chapter !== undefined) {
104
- url.searchParams.set("hierarchy[chapter]", String(args.chapter));
105
- }
106
-
107
- type Resp = {
108
- results?: {
109
- starts_on?: string;
110
- ends_on?: string | null;
111
- type?: string;
112
- hierarchy?: {
113
- title?: string;
114
- chapter?: string;
115
- subchapter?: string;
116
- part?: string;
117
- subpart?: string;
118
- section?: string;
119
- };
120
- hierarchy_headings?: Record<string, string | null>;
121
- headings?: Record<string, string | null>;
122
- full_text_excerpt?: string;
123
- score?: number;
124
- }[];
125
- meta?: {
126
- current_page?: number;
127
- total_pages?: number;
128
- total_count?: number;
129
- max_score?: number;
130
- description?: string;
131
- };
132
- };
133
- const json = await fetchJson<Resp>(url.toString());
134
- // F6 (P2 empty-vs-outage): a 200 whose `results` is PRESENT-but-non-array — or
135
- // a body carrying NEITHER a `results` array NOR a `meta` object — is drift / an
136
- // unexpected shape, NOT a genuine no-match. Throw (schema_drift) rather than
137
- // letting `(json.results ?? []).map` coalesce it into a fake AUTHORITATIVE empty.
138
- // A GENUINE empty (results:[] with meta.total_count:0) flows through honestly
139
- // below. (eCFR normally signals errors via HTTP status caught by fetchWithRetry;
140
- // this closes the previously-unhandled + untested 200-body drift path.)
141
- if (json.results !== undefined && !Array.isArray(json.results)) {
142
- throw driftError(
143
- "ecfr.gov",
144
- "eCFR search returned HTTP 200 but `results` is not an array — treating it as schema drift, NOT an empty result set.",
145
- );
146
- }
147
- if (json.results === undefined && json.meta === undefined) {
148
- throw driftError(
149
- "ecfr.gov",
150
- "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.",
151
- );
152
- }
153
- const data = {
154
- results: (json.results ?? []).map((r) => ({
155
- type: r.type ?? "",
156
- title: r.hierarchy?.title ?? "",
157
- chapter: r.hierarchy?.chapter,
158
- part: r.hierarchy?.part,
159
- subpart: r.hierarchy?.subpart,
160
- section: r.hierarchy?.section,
161
- headingPath: Object.values(r.hierarchy_headings ?? {})
162
- .filter(Boolean)
163
- .join(" › "),
164
- excerpt: stripHtml(r.full_text_excerpt ?? ""),
165
- score: r.score ?? 0,
166
- // Stable ecfr.gov URL pattern from the hierarchy
167
- ecfrUrl: r.hierarchy
168
- ? buildEcfrUrl(r.hierarchy)
169
- : "",
170
- effectiveOn: r.starts_on ?? "",
171
- // Additive: the version's end date. null = the CURRENT (in-force) version;
172
- // a non-null date = a HISTORICAL version. eCFR search returns ~5 versions
173
- // per section; existing ecfr_search callers simply ignore this extra field,
174
- // while far_search uses it to collapse historical dups to the current one.
175
- endsOn: r.ends_on ?? null,
176
- })),
177
- };
178
-
179
- // Truthful `_meta` (spec §1.2 A6, §2.3). eCFR returns a hit count in
180
- // `meta.total_count`, BUT that count is capped by Elasticsearch's
181
- // `index.max_result_window` at 10,000 (a real ceiling — unlike a genuine
182
- // small count, a value at/above the cap is a LOWER BOUND, not an exact
183
- // total). Below the cap `totalAvailable` is exact (the AI can tell a top-N
184
- // slice from the full match set); at/above the cap we flag
185
- // `totalIsLowerBound:true` + a note so a broad query that truly matches
186
- // >10,000 sections is NOT reported as if exactly 10,000 (D1). A6: echo the
187
- // applied title scope so the AI can VERIFY it searched the intended corpus —
188
- // Title 48 (FAR) vs every CFR title — rather than silently trusting a filter
189
- // that could return cross-title results if the eCFR param contract ever changes.
190
- const returned = data.results.length;
191
- const totalAvailable =
192
- typeof json.meta?.total_count === "number" ? json.meta.total_count : null;
193
- // eCFR's search total_count saturates at the Elasticsearch max_result_window
194
- // (live-verified 10,000). Use `>= cap` (not `=== cap`) so a total AT OR ABOVE
195
- // the ceiling is treated as a lower bound — strictly safe if the window were
196
- // ever configured higher. Mirrors federal-register.ts's FR_COUNT_CAP and
197
- // edgar.ts's FTS_WINDOW.
198
- const totalIsLowerBound =
199
- totalAvailable !== null && totalAvailable >= ECFR_TOTAL_COUNT_CAP;
200
- const scopeNote =
201
- args.titleNumber !== undefined
202
- ? `searched CFR Title ${args.titleNumber}${
203
- args.titleNumber === 48 ? " (FAR — Federal Acquisition Regulation)" : ""
204
- } only`
205
- : "searched all CFR titles (no title filter applied)";
206
- const notes = [scopeNote];
207
- if (totalIsLowerBound) {
208
- notes.push(
209
- `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.`,
210
- );
211
- }
212
- return withMeta(data, {
213
- source: "ecfr.gov/api (search/v1)",
214
- keylessMode: true,
215
- returned,
216
- totalAvailable,
217
- // Explicit boolean (not conditional): a below-cap count is DEFINITIVELY
218
- // exact (totalIsLowerBound:false), an at/above-cap count is a lower bound
219
- // (true). The AI can trust `false` as "this is the real total".
220
- totalIsLowerBound,
221
- truncated:
222
- totalAvailable !== null ? returned < totalAvailable : undefined,
223
- filtersApplied: args.titleNumber !== undefined ? ["titleNumber"] : [],
224
- filtersDropped: [],
225
- fieldsUnavailable: [],
226
- notes,
227
- });
228
- }
229
-
230
- function stripHtml(s: string): string {
231
- return s
232
- .replace(/<[^>]+>/g, "")
233
- .replace(/\s+/g, " ")
234
- .trim();
235
- }
236
-
237
- function buildEcfrUrl(h: {
238
- title?: string;
239
- chapter?: string;
240
- part?: string;
241
- section?: string;
242
- }): string {
243
- const base = `https://www.ecfr.gov/current/title-${h.title}`;
244
- if (h.section) return `${base}/section-${h.section}`;
245
- if (h.part) return `${base}/part-${h.part}`;
246
- if (h.chapter) return `${base}/chapter-${h.chapter}`;
247
- return base;
248
- }
1
+ /**
2
+ * eCFR (Electronic Code of Federal Regulations) wrappers (keyless).
3
+ *
4
+ * eCFR is the up-to-date version of the CFR — Title 48 = FAR (Federal
5
+ * Acquisition Regulation), Title 2 = Federal financial assistance, etc.
6
+ * For a federal contractor, eCFR is the primary source for regulation
7
+ * text the agent should quote when answering compliance questions.
8
+ *
9
+ * Endpoints:
10
+ * - /versioner/v1/titles.json — list 50 CFR titles + last-amended dates
11
+ * - /search/v1/results — full-text search across the entire CFR
12
+ *
13
+ * Both keyless. Documented at https://www.ecfr.gov/developers/.
14
+ */
15
+
16
+ import { fetchWithRetry } from "./errors.js";
17
+ import { driftError } from "./datasource.js";
18
+ import { memoize } from "./cache.js";
19
+ import { withMeta } from "./meta.js";
20
+
21
+ const ECFR = "https://www.ecfr.gov/api";
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
+
29
+ async function fetchJson<T>(url: string): Promise<T> {
30
+ const r = await fetchWithRetry(
31
+ url,
32
+ {
33
+ headers: { Accept: "application/json" },
34
+ signal: AbortSignal.timeout(15_000),
35
+ },
36
+ `ecfr:${url.split("/api/")[1] ?? url}`,
37
+ );
38
+ return (await r.json()) as T;
39
+ }
40
+
41
+ export async function listTitles() {
42
+ // 50 CFR titles change very infrequently. Cache aggressively (5 min).
43
+ return memoize("ecfr:titles", async () => {
44
+ type Resp = {
45
+ titles?: {
46
+ number?: number;
47
+ name?: string;
48
+ latest_amended_on?: string;
49
+ latest_issue_date?: string;
50
+ up_to_date_as_of?: string;
51
+ reserved?: boolean;
52
+ }[];
53
+ };
54
+ const json = await fetchJson<Resp>(`${ECFR}/versioner/v1/titles.json`);
55
+ const titles = (json.titles ?? []).map((t) => ({
56
+ number: t.number ?? 0,
57
+ name: t.name ?? "",
58
+ latestAmendedOn: t.latest_amended_on,
59
+ latestIssueDate: t.latest_issue_date,
60
+ upToDateAsOf: t.up_to_date_as_of,
61
+ reserved: !!t.reserved,
62
+ }));
63
+ // Carry the honesty envelope every other tool has (dogfooding 2026-07-15: this
64
+ // list tool previously returned a bare {titles} with no _meta). The titles
65
+ // endpoint returns the COMPLETE canonical CFR title set in one response — no
66
+ // pagination, no filter — so the count IS the total and complete derives true.
67
+ return withMeta(
68
+ { titles },
69
+ {
70
+ source: "ecfr.gov/api (versioner/v1/titles)",
71
+ keylessMode: true,
72
+ returned: titles.length,
73
+ totalAvailable: titles.length,
74
+ truncated: false,
75
+ filtersApplied: [],
76
+ filtersDropped: [],
77
+ notes: [
78
+ "Complete canonical list of all CFR titles — the endpoint returns every title in a single response (no pagination or filtering).",
79
+ ],
80
+ },
81
+ );
82
+ });
83
+ }
84
+
85
+ export async function search(args: {
86
+ query: string;
87
+ titleNumber?: number;
88
+ chapter?: number;
89
+ perPage?: number;
90
+ }) {
91
+ const url = new URL(`${ECFR}/search/v1/results`);
92
+ url.searchParams.set("query", args.query);
93
+ url.searchParams.set("per_page", String(args.perPage ?? 5));
94
+ if (args.titleNumber) {
95
+ // eCFR search filter: hierarchy[title]=N (NOT just title=N — that's
96
+ // an "unpermitted parameter" error from the eCFR API).
97
+ url.searchParams.set("hierarchy[title]", String(args.titleNumber));
98
+ }
99
+ // Optional chapter filter (additive; existing ecfr_search callers pass none).
100
+ // Within Title 48: chapter 1 = FAR, chapter 2 = DFARS, chapter 5 = GSAM, etc.
101
+ // This is what lets far_search scope to FAR/DFARS and keep GSAM/agency
102
+ // supplements out server-side. Same hierarchy[…] contract as the title filter.
103
+ if (args.chapter !== undefined) {
104
+ url.searchParams.set("hierarchy[chapter]", String(args.chapter));
105
+ }
106
+
107
+ type Resp = {
108
+ results?: {
109
+ starts_on?: string;
110
+ ends_on?: string | null;
111
+ type?: string;
112
+ hierarchy?: {
113
+ title?: string;
114
+ chapter?: string;
115
+ subchapter?: string;
116
+ part?: string;
117
+ subpart?: string;
118
+ section?: string;
119
+ };
120
+ hierarchy_headings?: Record<string, string | null>;
121
+ headings?: Record<string, string | null>;
122
+ full_text_excerpt?: string;
123
+ score?: number;
124
+ }[];
125
+ meta?: {
126
+ current_page?: number;
127
+ total_pages?: number;
128
+ total_count?: number;
129
+ max_score?: number;
130
+ description?: string;
131
+ };
132
+ };
133
+ const json = await fetchJson<Resp>(url.toString());
134
+ // F6 (P2 empty-vs-outage): a 200 whose `results` is PRESENT-but-non-array — or
135
+ // a body carrying NEITHER a `results` array NOR a `meta` object — is drift / an
136
+ // unexpected shape, NOT a genuine no-match. Throw (schema_drift) rather than
137
+ // letting `(json.results ?? []).map` coalesce it into a fake AUTHORITATIVE empty.
138
+ // A GENUINE empty (results:[] with meta.total_count:0) flows through honestly
139
+ // below. (eCFR normally signals errors via HTTP status caught by fetchWithRetry;
140
+ // this closes the previously-unhandled + untested 200-body drift path.)
141
+ if (json.results !== undefined && !Array.isArray(json.results)) {
142
+ throw driftError(
143
+ "ecfr.gov",
144
+ "eCFR search returned HTTP 200 but `results` is not an array — treating it as schema drift, NOT an empty result set.",
145
+ );
146
+ }
147
+ if (json.results === undefined && json.meta === undefined) {
148
+ throw driftError(
149
+ "ecfr.gov",
150
+ "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.",
151
+ );
152
+ }
153
+ const data = {
154
+ results: (json.results ?? []).map((r) => ({
155
+ type: r.type ?? "",
156
+ title: r.hierarchy?.title ?? "",
157
+ chapter: r.hierarchy?.chapter,
158
+ part: r.hierarchy?.part,
159
+ subpart: r.hierarchy?.subpart,
160
+ section: r.hierarchy?.section,
161
+ headingPath: Object.values(r.hierarchy_headings ?? {})
162
+ .filter(Boolean)
163
+ .join(" › "),
164
+ excerpt: stripHtml(r.full_text_excerpt ?? ""),
165
+ score: r.score ?? 0,
166
+ // Stable ecfr.gov URL pattern from the hierarchy
167
+ ecfrUrl: r.hierarchy
168
+ ? buildEcfrUrl(r.hierarchy)
169
+ : "",
170
+ effectiveOn: r.starts_on ?? "",
171
+ // Additive: the version's end date. null = the CURRENT (in-force) version;
172
+ // a non-null date = a HISTORICAL version. eCFR search returns ~5 versions
173
+ // per section; existing ecfr_search callers simply ignore this extra field,
174
+ // while far_search uses it to collapse historical dups to the current one.
175
+ endsOn: r.ends_on ?? null,
176
+ })),
177
+ };
178
+
179
+ // Truthful `_meta` (spec §1.2 A6, §2.3). eCFR returns a hit count in
180
+ // `meta.total_count`, BUT that count is capped by Elasticsearch's
181
+ // `index.max_result_window` at 10,000 (a real ceiling — unlike a genuine
182
+ // small count, a value at/above the cap is a LOWER BOUND, not an exact
183
+ // total). Below the cap `totalAvailable` is exact (the AI can tell a top-N
184
+ // slice from the full match set); at/above the cap we flag
185
+ // `totalIsLowerBound:true` + a note so a broad query that truly matches
186
+ // >10,000 sections is NOT reported as if exactly 10,000 (D1). A6: echo the
187
+ // applied title scope so the AI can VERIFY it searched the intended corpus —
188
+ // Title 48 (FAR) vs every CFR title — rather than silently trusting a filter
189
+ // that could return cross-title results if the eCFR param contract ever changes.
190
+ const returned = data.results.length;
191
+ const totalAvailable =
192
+ typeof json.meta?.total_count === "number" ? json.meta.total_count : null;
193
+ // eCFR's search total_count saturates at the Elasticsearch max_result_window
194
+ // (live-verified 10,000). Use `>= cap` (not `=== cap`) so a total AT OR ABOVE
195
+ // the ceiling is treated as a lower bound — strictly safe if the window were
196
+ // ever configured higher. Mirrors federal-register.ts's FR_COUNT_CAP and
197
+ // edgar.ts's FTS_WINDOW.
198
+ const totalIsLowerBound =
199
+ totalAvailable !== null && totalAvailable >= ECFR_TOTAL_COUNT_CAP;
200
+ const scopeNote =
201
+ args.titleNumber !== undefined
202
+ ? `searched CFR Title ${args.titleNumber}${
203
+ args.titleNumber === 48 ? " (FAR — Federal Acquisition Regulation)" : ""
204
+ } only`
205
+ : "searched all CFR titles (no title filter applied)";
206
+ const notes = [scopeNote];
207
+ if (totalIsLowerBound) {
208
+ notes.push(
209
+ `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.`,
210
+ );
211
+ }
212
+ return withMeta(data, {
213
+ source: "ecfr.gov/api (search/v1)",
214
+ keylessMode: true,
215
+ returned,
216
+ totalAvailable,
217
+ // Explicit boolean (not conditional): a below-cap count is DEFINITIVELY
218
+ // exact (totalIsLowerBound:false), an at/above-cap count is a lower bound
219
+ // (true). The AI can trust `false` as "this is the real total".
220
+ totalIsLowerBound,
221
+ truncated:
222
+ totalAvailable !== null ? returned < totalAvailable : undefined,
223
+ filtersApplied: args.titleNumber !== undefined ? ["titleNumber"] : [],
224
+ filtersDropped: [],
225
+ fieldsUnavailable: [],
226
+ notes,
227
+ });
228
+ }
229
+
230
+ function stripHtml(s: string): string {
231
+ return s
232
+ .replace(/<[^>]+>/g, "")
233
+ .replace(/\s+/g, " ")
234
+ .trim();
235
+ }
236
+
237
+ function buildEcfrUrl(h: {
238
+ title?: string;
239
+ chapter?: string;
240
+ part?: string;
241
+ section?: string;
242
+ }): string {
243
+ const base = `https://www.ecfr.gov/current/title-${h.title}`;
244
+ if (h.section) return `${base}/section-${h.section}`;
245
+ if (h.part) return `${base}/part-${h.part}`;
246
+ if (h.chapter) return `${base}/chapter-${h.chapter}`;
247
+ return base;
248
+ }