@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/far.ts CHANGED
@@ -1,1009 +1,1009 @@
1
- /**
2
- * FAR / DFARS clause lookup (keyless) — the authoritative clause text + its
3
- * prescription, from the eCFR **versioner full** endpoint.
4
- *
5
- * Why this exists (and why NOT `ecfr_search`)
6
- * -------------------------------------------
7
- * The shipped full-text `ecfr_search` mis-ranks EXACT clause numbers: a bare
8
- * `52.212-4` query returns GSAM **552**.212-4 above the real FAR 52.212-4
9
- * (doc-09 §1.2). A proposal writer needs the AUTHORITATIVE clause text AND the
10
- * rule that says WHEN it applies ("As prescribed in …") — the exact pair. The
11
- * clean path is the versioner full endpoint, which `src/ecfr.ts` does not call:
12
- *
13
- * GET /api/versioner/v1/full/{date}/title-48.xml?section={clause}
14
- *
15
- * keyless, HTTP 200 (~40 KB XML) for a real clause, clean HTTP 404
16
- * `{"error":"No matching content found."}` for an absent one. Title 48 = FAR.
17
- * `{date}` defaults to Title 48 `up_to_date_as_of` (from listTitles, cached).
18
- *
19
- * TRUTHFULNESS invariants (a reviewer WILL try to break these):
20
- * - A DOWN/failing eCFR service must NEVER read as "clause not found": only a
21
- * genuine HTTP 404 maps to `not_found`; any other fetch error propagates
22
- * with fetchWithRetry's classification (retryable 5xx/network/etc.).
23
- * - A genuinely-absent clause is `not_found` (retryable:false), NEVER a fake
24
- * empty clause. Clause text is never silently dropped or fabricated.
25
- * - A prescription-section fetch failure is NON-FATAL: `prescription:null` +
26
- * disclosed in `_meta.notes`; it never crashes the clause result.
27
- * - `farOverhaulRisk` carries NO fabricated FAR-case numbers/dates — only the
28
- * fixed structural caveat + the real authoritative-list / deviation URLs.
29
- *
30
- * Self-contained: imports `getText` from ./datasource.js (the shared XML/text
31
- * fetch port, ADR-0013) + ToolErrorCarrier from ./errors.js, memoize from
32
- * ./cache.js, listTitles from ./ecfr.js, and withMeta from ./meta.js — the
33
- * versioner XML parse lives here (ecfr.ts's stripHtml is private and stays
34
- * private).
35
- */
36
-
37
- import { ToolErrorCarrier } from "./errors.js";
38
- import { getText } from "./datasource.js";
39
- import { memoize } from "./cache.js";
40
- import { listTitles, search as ecfrSearch } from "./ecfr.js";
41
- import { withMeta } from "./meta.js";
42
-
43
- const ECFR = "https://www.ecfr.gov/api";
44
-
45
- /** The regulation family a clause number belongs to (from its prefix). */
46
- type Regulation = "FAR" | "DFARS" | "GSAM" | "other";
47
-
48
- /**
49
- * Fetch a versioner-full XML document as text via the shared `getText` port
50
- * (ADR-0013). This thin wrapper owns the far-specific, path-bearing `ecfr:…`
51
- * label derivation (kept LOCAL — NOT hoisted into the port), then delegates:
52
- * `getText` retries via fetchWithRetry (retry defaults true) and returns the raw
53
- * body. The versioner endpoint serves `title-48.xml`. fetchWithRetry throws a
54
- * classified ToolErrorCarrier on any non-2xx (404 → not_found, 5xx →
55
- * upstream_unavailable, network → upstream_unavailable), which callers here
56
- * either map (404 on the CLAUSE) or let propagate.
57
- */
58
- async function fetchText(url: string): Promise<string> {
59
- return getText(url, {
60
- label: `ecfr:${url.split("/api/")[1] ?? url}`,
61
- headers: { Accept: "application/xml" },
62
- });
63
- }
64
-
65
- /**
66
- * Strip XML tags → clean, paragraph-preserving plain text. The versioner body
67
- * is block XML (`<P>`, `<HD1>`, `<EXTRACT>`, `<I>`…); we drop the tags but keep
68
- * paragraph boundaries as spaces so the clause reads as continuous prose, then
69
- * decode the handful of numeric/entity refs the feed uses (—, &, ", ', <, >).
70
- */
71
- function stripXml(s: string): string {
72
- return s
73
- // Block-level closers become a space so paragraphs don't run together.
74
- .replace(/<\/(P|HD1|HEAD|EXTRACT|CITA|EDNOTE|PSPACE|HED|DIV8|LI)>/gi, " ")
75
- // Drop every remaining tag.
76
- .replace(/<[^>]+>/g, " ")
77
- // Decode the entities the eCFR XML actually emits.
78
- .replace(/&#8212;|&mdash;/gi, "—")
79
- .replace(/&#8211;|&ndash;/gi, "–")
80
- .replace(/&#8217;|&rsquo;/gi, "’")
81
- .replace(/&#8220;|&ldquo;/gi, "“")
82
- .replace(/&#8221;|&rdquo;/gi, "”")
83
- .replace(/&quot;/gi, '"')
84
- .replace(/&apos;/gi, "'")
85
- .replace(/&amp;/gi, "&")
86
- .replace(/&lt;/gi, "<")
87
- .replace(/&gt;/gi, ">")
88
- // Collapse whitespace.
89
- .replace(/\s+/g, " ")
90
- .trim();
91
- }
92
-
93
- /**
94
- * Normalize a raw clauseNumber to its bare `NN.NNN-N` / `NNN.NNN-NNNN` core by
95
- * stripping ONLY a leading `FAR`/`DFARS` prefix (case-insensitive) and
96
- * surrounding whitespace. It DOES NOT strip embedded characters: doing so would
97
- * fabricate a plausible-but-wrong clause from garbage (e.g. `52.212-4extra5` →
98
- * `52.212-45`, a DIFFERENT real clause) that then passes CLAUSE_RE and fetches a
99
- * silently-wrong answer. By leaving embedded/trailing junk in place, a non-clause
100
- * input fails CLAUSE_RE below and farClauseLookup throws `invalid_input` — exactly
101
- * as the server Zod boundary (regex `^\s*(?:d?far[s]?\b[\s.:#-]*)?\d{1,3}\.\d{3,4}-\d{1,4}\s*$`)
102
- * already rejects it, so far.ts is safe called standalone. The legit shapes
103
- * (`52.212-4`, `FAR 52.212-4`, `DFARS 252.204-7012`, ` 52.212-4 `) still collapse
104
- * to the bare clause and succeed.
105
- */
106
- function normalizeClauseNumber(raw: string): string {
107
- return (raw ?? "")
108
- .trim()
109
- // Strip ONLY the leading regulation prefix (FAR 52.212-4 / DFARS 252.204-7012)
110
- // and its separator; NOTHING else is removed, so embedded garbage survives to
111
- // be rejected by CLAUSE_RE (never mangled into a valid-looking clause).
112
- .replace(/^\s*(?:d?far[s]?)\b[\s.:#-]*/i, "")
113
- .trim();
114
- }
115
-
116
- const CLAUSE_RE = /^\d{1,3}\.\d{3,4}-\d{1,4}$/;
117
-
118
- /** Regulation family from the clause-number prefix (deterministic, no fetch). */
119
- function regulationFor(clauseNumber: string): Regulation {
120
- if (/^252\./.test(clauseNumber)) return "DFARS";
121
- if (/^2\d\d\./.test(clauseNumber)) return "DFARS";
122
- if (/^552\./.test(clauseNumber)) return "GSAM";
123
- if (/^52\./.test(clauseNumber)) return "FAR";
124
- return "other";
125
- }
126
-
127
- /**
128
- * The RFO (Revolutionary FAR Overhaul) currency caveat — an ALWAYS-PRESENT
129
- * structural flag, NOT a per-clause claim. eCFR reflects the CODIFIED FAR only;
130
- * the RFO is replacing FAR parts via agency class deviations that may not appear
131
- * in eCFR, so a clause can be current in the CFR yet operationally superseded.
132
- *
133
- * This is the HONEST design (doc-09 §3 baked unverified FAR-case numbers/dates
134
- * as [가설] — those go stale/wrong). We ship the never-stale-wrong version: no
135
- * fabricated specifics, only the fixed caveat + the real authoritative-list and
136
- * deviation URLs (all VERIFIED HTTP 200, 2026-07-04). `appliesTo` scopes the
137
- * caveat to the clause's own regulation family.
138
- */
139
- function buildFarOverhaulRisk(regulation: Regulation) {
140
- return {
141
- note:
142
- "eCFR reflects the CODIFIED FAR only. The Revolutionary FAR Overhaul (RFO) is actively replacing FAR parts via agency class deviations that may NOT appear in eCFR — so this clause text can be technically current in the CFR yet operationally superseded. Verify the controlling deviation before relying on it.",
143
- authoritativeList: "https://www.acquisition.gov/far-overhaul",
144
- deviationSources: [
145
- "https://www.acquisition.gov/far-overhaul",
146
- "https://www.acquisition.gov/dfars",
147
- "https://www.acq.osd.mil/dpap/dars/",
148
- ],
149
- appliesTo: regulation,
150
- };
151
- }
152
-
153
- /** Title 48 currency metadata, cached (titles.json changes infrequently). */
154
- async function title48Currency(): Promise<{
155
- upToDateAsOf: string | null;
156
- latestAmendedOn: string | null;
157
- }> {
158
- return memoize("far:title48-currency", async () => {
159
- // listTitles now returns a MetaBundle (honesty envelope); the titles live on
160
- // `.data.titles`.
161
- const { titles } = (await listTitles()).data;
162
- const t48 = titles.find((t) => t.number === 48);
163
- return {
164
- upToDateAsOf: t48?.upToDateAsOf ?? null,
165
- latestAmendedOn: t48?.latestAmendedOn ?? null,
166
- };
167
- });
168
- }
169
-
170
- /** Extract the first `<HEAD>…</HEAD>` inner text (tags stripped). */
171
- function firstHead(xml: string): string | null {
172
- const m = xml.match(/<HEAD>([\s\S]*?)<\/HEAD>/i);
173
- if (!m || m[1] === undefined) return null;
174
- const h = stripXml(m[1]);
175
- return h.length > 0 ? h : null;
176
- }
177
-
178
- /**
179
- * Does this body look like a REAL eCFR Title-48 section, vs an empty body, a
180
- * CDN/WAF HTML interstitial, or a truncated proxy response? (Defect-2 guard.)
181
- * Real versioner section XML carries an uppercase `<HEAD>…</HEAD>` plus
182
- * substantive text. The `<HEAD>` test is CASE-SENSITIVE on purpose: an HTML
183
- * challenge page uses lowercase `<head>`, and empty/truncated bodies carry
184
- * neither — so a hollow 200 fails this check and is refused rather than parsed
185
- * into a fake `complete:true` clause.
186
- */
187
- function looksLikeSectionXml(xml: string): boolean {
188
- return /<HEAD>/.test(xml) && stripXml(xml).length >= 20;
189
- }
190
-
191
- /**
192
- * Fetch + parse ONE Title-48 section as a prescription reference (its heading +
193
- * stripped text). NON-FATAL by contract: returns null on ANY failure so a
194
- * prescription problem never sinks the clause result. Memoized by URL.
195
- */
196
- async function fetchPrescription(
197
- baseSection: string,
198
- asOfDate: string,
199
- ): Promise<{ section: string; heading: string | null; text: string } | null> {
200
- const url = `${ECFR}/versioner/v1/full/${asOfDate}/title-48.xml?section=${baseSection}`;
201
- try {
202
- const xml = await memoize(`far:section:${asOfDate}:${baseSection}`, async () => {
203
- const body = await fetchText(url);
204
- // A hollow/interstitial 200 is a fetch FAILURE, not an empty prescription
205
- // (Defect-2, non-fatal path). Throw so it is NOT cached and becomes null.
206
- if (!looksLikeSectionXml(body))
207
- throw new Error("non-section prescription body");
208
- return body;
209
- });
210
- const heading = firstHead(xml);
211
- const text = stripXml(xml);
212
- return { section: baseSection, heading, text };
213
- } catch {
214
- // Any failure (404/5xx/network/parse/hollow-body) → null; caller discloses it.
215
- return null;
216
- }
217
- }
218
-
219
- export async function farClauseLookup(args: {
220
- clauseNumber: string;
221
- includePrescription?: boolean;
222
- asOfDate?: string;
223
- }) {
224
- const clauseNumber = normalizeClauseNumber(args.clauseNumber ?? "");
225
- const includePrescription = args.includePrescription ?? true;
226
-
227
- // Defense-in-depth: the server Zod schema already rejects a non-matching
228
- // clauseNumber, but guard here too so far.ts is safe called directly.
229
- if (!CLAUSE_RE.test(clauseNumber)) {
230
- throw new ToolErrorCarrier({
231
- kind: "invalid_input",
232
- message: `Invalid FAR/DFARS clause number '${args.clauseNumber}'. Expected a clause like 52.212-4, 252.204-7012, or 52.204-25 (optionally prefixed 'FAR'/'DFARS').`,
233
- retryable: false,
234
- upstreamEndpoint: "ecfr:versioner/v1/full/title-48",
235
- });
236
- }
237
-
238
- const regulation = regulationFor(clauseNumber);
239
-
240
- // asOfDate defaults to Title 48's up_to_date_as_of (cached).
241
- const currency = await title48Currency();
242
- const asOfDate = args.asOfDate ?? currency.upToDateAsOf ?? "";
243
- // Guard (Defect 1): never query a blank/invalid date. If currency could NOT be
244
- // resolved (titles.json returns 200 but Title 48 — or its up_to_date_as_of —
245
- // is missing/renamed → upToDateAsOf:null, WITHOUT throwing) AND the caller gave
246
- // no asOfDate, asOfDate is "". The versioner 404s on a blank-date URL, and that
247
- // 404 would be mislabeled "clause not found" — a lie about a real, existing
248
- // clause. This is a currency-RESOLUTION failure, not an absent clause.
249
- if (!/^\d{4}-\d{2}-\d{2}$/.test(asOfDate)) {
250
- throw new ToolErrorCarrier({
251
- kind: "schema_drift",
252
- message: `Could not resolve Title 48's current codification date from the eCFR titles endpoint (up_to_date_as_of unavailable) and no asOfDate was supplied. Refusing to query a blank date — the versioner would return HTTP 404, which must NOT be reported as a missing clause. Retry shortly, or pass an explicit asOfDate (YYYY-MM-DD).`,
253
- retryable: true,
254
- upstreamEndpoint: "ecfr:versioner/v1/titles.json",
255
- });
256
- }
257
- const isCurrent =
258
- currency.upToDateAsOf !== null && asOfDate === currency.upToDateAsOf;
259
-
260
- // Guard (Defect 3): a VALID-format asOfDate that is AFTER the latest eCFR
261
- // codification has no versioner snapshot — the versioner 404s, and the generic
262
- // not-found path below would mislabel it "clause not found (the clause number
263
- // may be wrong, reserved, or removed)". That is a LIE about a real, current
264
- // clause: the problem is the DATE (past the latest edition), not the clause.
265
- // Common trigger: a caller infers asOfDate = today when today > up_to_date_as_of
266
- // (observed in dogfood). Fail with an honest, actionable message and
267
- // `invalid_input` (fix the date) rather than not_found (clause absent).
268
- if (
269
- currency.upToDateAsOf !== null &&
270
- /^\d{4}-\d{2}-\d{2}$/.test(currency.upToDateAsOf) &&
271
- asOfDate > currency.upToDateAsOf
272
- ) {
273
- throw new ToolErrorCarrier({
274
- kind: "invalid_input",
275
- message: `eCFR has no Title 48 codification as of ${asOfDate} — the latest available codification is ${currency.upToDateAsOf}. FAR clause ${clauseNumber} is NOT missing or removed; there is simply no eCFR snapshot for a date past the latest edition. Omit asOfDate to use the latest, or pass a date on or before ${currency.upToDateAsOf}.`,
276
- retryable: false,
277
- upstreamEndpoint: "ecfr:versioner/v1/full/title-48",
278
- });
279
- }
280
-
281
- // ── Fetch the CLAUSE XML (memoized by URL). ─────────────────────────────
282
- const clauseUrl = `${ECFR}/versioner/v1/full/${asOfDate}/title-48.xml?section=${clauseNumber}`;
283
- let xml: string;
284
- try {
285
- xml = await memoize(`far:section:${asOfDate}:${clauseNumber}`, async () => {
286
- const body = await fetchText(clauseUrl);
287
- // Guard (Defect 2): a 200 with an empty body, a CDN/WAF HTML interstitial,
288
- // or a truncated proxy response must NOT be parsed into a hollow
289
- // `complete:true` clause (heading/text empty, yet ok:true). Require real
290
- // Title-48 section XML. Throwing HERE (inside the memoize producer) keeps
291
- // the bad body OUT of the cache so a retry re-fetches cleanly.
292
- if (!looksLikeSectionXml(body)) {
293
- throw new ToolErrorCarrier({
294
- kind: "upstream_unavailable",
295
- message: `The eCFR versioner returned HTTP 200 for ${clauseNumber} (as of ${asOfDate}) but the body was not a parseable Title 48 section (empty, truncated, or a CDN/WAF interstitial). Refusing to emit a hollow clause. Retry shortly.`,
296
- retryable: true,
297
- upstreamStatus: 200,
298
- upstreamEndpoint: "ecfr:versioner/v1/full/title-48",
299
- });
300
- }
301
- return body;
302
- });
303
- } catch (e) {
304
- // A genuine 404 → not_found NAMING the clause (never null/empty). Any OTHER
305
- // error (5xx/network/timeout) PROPAGATES with its classification so a DOWN
306
- // service is never misread as "clause not found".
307
- if (e instanceof ToolErrorCarrier && e.toolError.kind === "not_found") {
308
- throw new ToolErrorCarrier({
309
- kind: "not_found",
310
- message: `FAR/DFARS clause ${clauseNumber} not found in Title 48 as of ${asOfDate}. (The eCFR versioner returned HTTP 404 "No matching content found" — the clause number may be wrong, reserved, or removed in this edition.)`,
311
- retryable: false,
312
- upstreamStatus: 404,
313
- upstreamEndpoint: "ecfr:versioner/v1/full/title-48",
314
- });
315
- }
316
- throw e;
317
- }
318
-
319
- // ── Parse the clause body. ──────────────────────────────────────────────
320
- const rawHead = firstHead(xml);
321
- // Strip a leading clause number if the HEAD duplicates it
322
- // ("52.212-4 Contract Terms…" → "Contract Terms…").
323
- const heading =
324
- rawHead != null
325
- ? rawHead.replace(new RegExp(`^${clauseNumber}\\s*[.:\\-—]?\\s*`), "").trim() ||
326
- rawHead
327
- : null;
328
-
329
- const revMatch = xml.match(/\(([A-Z]{3}\.?\s+\d{4})\)/);
330
- const revision = revMatch?.[1] ?? null;
331
-
332
- const preMatch = xml.match(
333
- /As prescribed in (\d{1,3}\.\d+(?:\([a-z0-9]+\))*)/i,
334
- );
335
- const prescribedIn = preMatch?.[1] ?? null;
336
-
337
- // Detect clause vs provision from the prescribing verb. DFARS uses BOTH
338
- // "insert the following …" and "use the following …" (Defect 3: the narrow
339
- // /insert/-only regex silently mislabeled DFARS provisions as clauses). When
340
- // NEITHER verb is present, default to "clause" but DISCLOSE it as inferred
341
- // (see the note pushed below) rather than assert it.
342
- const kindMatch = xml.match(/(?:insert|use) the following (clause|provision)/i);
343
- const kindDetected = kindMatch?.[1]?.toLowerCase() as
344
- | "clause"
345
- | "provision"
346
- | undefined;
347
- const kind: "clause" | "provision" = kindDetected ?? "clause";
348
-
349
- const text = stripXml(xml);
350
-
351
- // ── Optional prescription section (non-fatal). ──────────────────────────
352
- const notes: string[] = [];
353
- let prescription:
354
- | { section: string; heading: string | null; text: string }
355
- | null = null;
356
- let prescriptionDegraded = false;
357
-
358
- if (includePrescription && prescribedIn) {
359
- // Trim any trailing subparagraph to the base section: 12.301(b)(3) → 12.301.
360
- const baseSection = prescribedIn.replace(/\(.*$/, "");
361
- prescription = await fetchPrescription(baseSection, asOfDate);
362
- if (prescription === null) {
363
- prescriptionDegraded = true;
364
- notes.push(
365
- `The prescribing section ${baseSection} (from "As prescribed in ${prescribedIn}") could NOT be fetched — prescription is null. This is a partial result: the clause text above is complete, but the "when does this clause apply?" rule was not retrieved (fetch it directly at ${ECFR}/versioner/v1/full/${asOfDate}/title-48.xml?section=${baseSection}, or via ecfr on ${`https://www.ecfr.gov/current/title-48/section-${baseSection}`}).`,
366
- );
367
- }
368
- } else if (includePrescription && !prescribedIn) {
369
- notes.push(
370
- "No 'As prescribed in …' pointer was found in this clause's text, so no prescription section was fetched (prescription:null). Some provisions/clauses carry the prescription in the parent subpart rather than an inline opener.",
371
- );
372
- }
373
-
374
- // Disclose when `kind` was inferred rather than read from a verb (Defect 3):
375
- // an undetected verb defaults to "clause", which would silently mislabel a
376
- // provision — so the consumer is told the field is a default, not an assertion.
377
- if (kindDetected === undefined) {
378
- notes.push(
379
- 'The instrument kind (clause vs provision) could NOT be determined from the text — no "insert/use the following clause/provision" verb was found — so kind defaults to "clause". Verify against the section heading if the clause-vs-provision distinction matters.',
380
- );
381
- }
382
-
383
- const ecfrUrl = `https://www.ecfr.gov/current/title-48/section-${clauseNumber}`;
384
-
385
- const farOverhaulRisk = buildFarOverhaulRisk(regulation);
386
-
387
- // Currency disclosures.
388
- if (!isCurrent) {
389
- notes.push(
390
- currency.upToDateAsOf
391
- ? `asOfDate ${asOfDate} is NOT Title 48's current codification date (${currency.upToDateAsOf}); this is a point-in-time read of the FAR as of ${asOfDate}, which may differ from the clause in force today.`
392
- : `Title 48's current codification date could not be confirmed from titles.json, so isCurrent is false; treat ${asOfDate} as the requested point-in-time edition.`,
393
- );
394
- }
395
- // The RFO caveat is ALWAYS surfaced (structural, never per-clause-fabricated).
396
- notes.push(
397
- `RFO caveat: eCFR carries the CODIFIED ${regulation} only. The Revolutionary FAR Overhaul is replacing FAR parts via agency class deviations that may not appear here — verify the controlling deviation (farOverhaulRisk.authoritativeList) before relying on this clause.`,
398
- );
399
-
400
- // Currency + provenance live in `data` (top-level), NOT in the meta partial:
401
- // the project's buildMeta (meta.ts) finalizes a FIXED-shape ResponseMeta and
402
- // drops unknown keys, so asOfDate/isCurrent/farOverhaulRisk passed via _meta
403
- // would be silently discarded. The design note anticipated this — carry them
404
- // where they actually survive. The honest completeness/degradation signals
405
- // (complete, fieldsUnavailable, notes) DO belong in _meta and are set there.
406
- const data = {
407
- clauseNumber,
408
- kind,
409
- regulation,
410
- heading,
411
- revision,
412
- text,
413
- prescribedIn,
414
- prescription,
415
- ecfrUrl,
416
- // Point-in-time provenance for THIS read (mirrors the design note's _meta
417
- // fields; placed in data so they are not dropped by buildMeta).
418
- asOfDate,
419
- titleUpToDateAsOf: currency.upToDateAsOf,
420
- titleLatestAmendedOn: currency.latestAmendedOn,
421
- isCurrent,
422
- // Always-present structural currency caveat (never fabricated specifics).
423
- farOverhaulRisk,
424
- };
425
-
426
- return withMeta(data, {
427
- source: "ecfr:versioner/full",
428
- keylessMode: true,
429
- // A single authoritative clause record.
430
- returned: 1,
431
- totalAvailable: 1,
432
- // A missing prescription is a genuine partial result → not complete.
433
- complete: prescriptionDegraded ? false : undefined,
434
- // fieldsUnavailable ONLY when we tried and failed to get the prescription.
435
- fieldsUnavailable: prescriptionDegraded ? ["prescription"] : [],
436
- filtersApplied: [],
437
- filtersDropped: [],
438
- notes,
439
- });
440
- }
441
-
442
- // ════════════════════════════════════════════════════════════════════════════
443
- // far_compliance_matrix — RFP cited-clause list → proposal-ready matrix.
444
- //
445
- // COMPOSES farClauseLookup: fan it out (bounded concurrency) over a deduped
446
- // clause list and assemble a Section-L/M-ready matrix — each clause's text +
447
- // prescription + whether it is a pass/fail eligibility GATE + the same currency
448
- // caveat farClauseLookup carries.
449
- //
450
- // TRUTHFULNESS — the load-bearing split (the C19 lesson): each clause has THREE
451
- // possible outcomes and "absent" is NEVER conflated with "couldn't fetch":
452
- // 1. resolved → a full row in `rows[]`.
453
- // 2. not_found (404) → `unresolved[]` (the clause genuinely isn't in Title 48).
454
- // 3. any other error → `errored[]` (a DOWN/failing eCFR — retryable — must NOT
455
- // read as "clause doesn't exist"; invalid_input too).
456
- // Every input clause lands in EXACTLY one bucket; summary.total proves it. Gate
457
- // tags come ONLY from the verified static GATE_MAP — never guessed.
458
- // ════════════════════════════════════════════════════════════════════════════
459
-
460
- /**
461
- * Eligibility-gate map (STATIC, verified live 2026-07-04 — headings confirmed).
462
- * A resolved row whose clauseNumber is a key here is a pass/fail award-eligibility
463
- * gate; the value is the disclosed label. Kept deliberately SMALL and defensible:
464
- * NEVER invent a gate meaning for a clause not in this map.
465
- */
466
- const GATE_MAP: Record<string, string> = {
467
- "52.204-24":
468
- "Section 889 — covered-telecom/video-surveillance prohibition (award-eligibility gate)",
469
- "52.204-25":
470
- "Section 889 — covered-telecom/video-surveillance prohibition (award-eligibility gate)",
471
- "52.204-26":
472
- "Section 889 — covered-telecom/video-surveillance prohibition (award-eligibility gate)",
473
- "52.219-14":
474
- "Limitations on Subcontracting — set-aside compliance gate",
475
- "252.204-7012":
476
- "Safeguarding Covered Defense Information + cyber incident reporting (CUI cyber gate)",
477
- "252.204-7020": "NIST SP 800-171 DoD Assessment (cyber gate)",
478
- "252.204-7021": "CMMC compliance (cyber gate)",
479
- };
480
-
481
- /** Hard ceiling on clauses processed per call (after dedupe). Mirrors the Zod cap. */
482
- const MATRIX_MAX_CLAUSES = 25;
483
- /** Bounded fan-out width — small pool so we never fire 25 eCFR fetches at once. */
484
- const MATRIX_CONCURRENCY = 5;
485
-
486
- /** One resolved matrix row: farClauseLookup's honest fields + a gate flag. */
487
- type MatrixRow = {
488
- clauseNumber: string;
489
- kind: "clause" | "provision";
490
- regulation: Regulation;
491
- heading: string | null;
492
- revision: string | null;
493
- prescribedIn: string | null;
494
- prescription:
495
- | { section: string; heading: string | null; text: string }
496
- | null;
497
- text: string;
498
- ecfrUrl: string;
499
- farOverhaulRisk: ReturnType<typeof buildFarOverhaulRisk>;
500
- /** The eligibility-gate label (from GATE_MAP), or null when not a mapped gate. */
501
- gate: string | null;
502
- };
503
-
504
- /** A clause that did not resolve, with a disclosed reason. */
505
- type UnresolvedClause = { clauseNumber: string; reason: string };
506
-
507
- /**
508
- * Run an async mapper over `items` with at most `width` in flight at once. A
509
- * lightweight promise pool (worker-draining a shared cursor): each worker pulls
510
- * the next index until the list is exhausted, so results are written back by
511
- * original index. Preserves input order and never fires more than `width`
512
- * concurrent fetches. Never rejects — the mapper itself must not throw (callers
513
- * here wrap each unit in try/catch).
514
- */
515
- async function mapPool<T, R>(
516
- items: readonly T[],
517
- width: number,
518
- mapper: (item: T, index: number) => Promise<R>,
519
- ): Promise<R[]> {
520
- const results = new Array<R>(items.length);
521
- let cursor = 0;
522
- const workerCount = Math.max(1, Math.min(width, items.length));
523
- const worker = async () => {
524
- for (;;) {
525
- const i = cursor++;
526
- if (i >= items.length) return;
527
- results[i] = await mapper(items[i] as T, i);
528
- }
529
- };
530
- await Promise.all(Array.from({ length: workerCount }, () => worker()));
531
- return results;
532
- }
533
-
534
- export async function farComplianceMatrix(args: {
535
- clauses: string[];
536
- asOfDate?: string;
537
- includePrescription?: boolean;
538
- flagGates?: boolean;
539
- }) {
540
- const includePrescription = args.includePrescription ?? true;
541
- const flagGates = args.flagGates !== false; // default true; only false disables
542
-
543
- // ── Normalize + dedupe case-insensitively, then cap AFTER dedupe. ─────────
544
- // normalizeClauseNumber already lowercases nothing (clause numbers are digits),
545
- // but it strips FAR/DFARS prefixes + stray chars so "52.212-4", "FAR 52.212-4",
546
- // and " 52.212-4 " collapse to one key. We keep the FIRST spelling's normalized
547
- // form and preserve input order.
548
- const seen = new Set<string>();
549
- const deduped: string[] = [];
550
- for (const raw of args.clauses ?? []) {
551
- const norm = normalizeClauseNumber(raw ?? "");
552
- // Keep even a non-matching normalized token: farClauseLookup will classify it
553
- // as invalid_input → errored (NOT silently dropped). Dedupe on the normalized
554
- // key so a malformed value that appears twice is only reported once.
555
- const key = norm.toLowerCase();
556
- if (seen.has(key)) continue;
557
- seen.add(key);
558
- deduped.push(norm);
559
- }
560
- const clauses = deduped.slice(0, MATRIX_MAX_CLAUSES);
561
- const total = clauses.length;
562
-
563
- // ── Resolve currency ONCE up front (avoid resolving it 25×). ──────────────
564
- // farClauseLookup would resolve this per call; we resolve it here and pass an
565
- // explicit asOfDate into each call. If currency can't be resolved AND no
566
- // asOfDate was supplied, refuse with a SINGLE schema_drift rather than letting
567
- // 25 identical ones bubble up (and a blank-date URL must never 404 into a fake
568
- // "not found"). This mirrors farClauseLookup's Defect-1 guard.
569
- const currency = await title48Currency();
570
- const asOfDate = args.asOfDate ?? currency.upToDateAsOf ?? "";
571
- if (!/^\d{4}-\d{2}-\d{2}$/.test(asOfDate)) {
572
- throw new ToolErrorCarrier({
573
- kind: "schema_drift",
574
- message: `Could not resolve Title 48's current codification date from the eCFR titles endpoint (up_to_date_as_of unavailable) and no asOfDate was supplied. Refusing to build a matrix against a blank date — the versioner would return HTTP 404, which must NOT be reported as missing clauses. Retry shortly, or pass an explicit asOfDate (YYYY-MM-DD).`,
575
- retryable: true,
576
- upstreamEndpoint: "ecfr:versioner/v1/titles.json",
577
- });
578
- }
579
-
580
- // ── Fan out farClauseLookup with bounded concurrency, catching EACH clause
581
- // individually so one failure never sinks the matrix. ─────────────────────
582
- type Outcome =
583
- | { status: "resolved"; row: MatrixRow }
584
- | { status: "unresolved"; entry: UnresolvedClause }
585
- | { status: "errored"; entry: UnresolvedClause };
586
-
587
- const outcomes = await mapPool<string, Outcome>(
588
- clauses,
589
- MATRIX_CONCURRENCY,
590
- async (clauseNumber): Promise<Outcome> => {
591
- try {
592
- const res = await farClauseLookup({
593
- clauseNumber,
594
- asOfDate,
595
- includePrescription,
596
- });
597
- const d = res.data;
598
- const gate = flagGates ? GATE_MAP[d.clauseNumber] ?? null : null;
599
- const row: MatrixRow = {
600
- clauseNumber: d.clauseNumber,
601
- kind: d.kind,
602
- regulation: d.regulation,
603
- heading: d.heading,
604
- revision: d.revision,
605
- prescribedIn: d.prescribedIn,
606
- prescription: d.prescription,
607
- text: d.text,
608
- ecfrUrl: d.ecfrUrl,
609
- farOverhaulRisk: d.farOverhaulRisk,
610
- gate,
611
- };
612
- return { status: "resolved", row };
613
- } catch (e) {
614
- const kind =
615
- e instanceof ToolErrorCarrier ? e.toolError.kind : "unknown";
616
- const reason =
617
- e instanceof ToolErrorCarrier
618
- ? e.toolError.message
619
- : e instanceof Error
620
- ? e.message
621
- : String(e);
622
- // A genuine 404 (absent clause) → unresolved. ANY OTHER kind (a fetch/
623
- // service problem: upstream_unavailable / schema_drift / rate_limited /
624
- // invalid_input / unknown) → errored. A DOWN eCFR must NEVER read as
625
- // "clause doesn't exist".
626
- if (kind === "not_found") {
627
- return {
628
- status: "unresolved",
629
- entry: { clauseNumber, reason },
630
- };
631
- }
632
- return { status: "errored", entry: { clauseNumber, reason } };
633
- }
634
- },
635
- );
636
-
637
- const rows: MatrixRow[] = [];
638
- const unresolved: UnresolvedClause[] = [];
639
- const errored: UnresolvedClause[] = [];
640
- for (const o of outcomes) {
641
- if (o.status === "resolved") rows.push(o.row);
642
- else if (o.status === "unresolved") unresolved.push(o.entry);
643
- else errored.push(o.entry);
644
- }
645
-
646
- // ── Summary (must be internally consistent). ──────────────────────────────
647
- const resolved = rows.length;
648
- const far = rows.filter((r) => r.regulation === "FAR").length;
649
- const dfars = rows.filter((r) => r.regulation === "DFARS").length;
650
- const gsam = rows.filter((r) => r.regulation === "GSAM").length;
651
- const other = rows.filter((r) => r.regulation === "other").length;
652
- const gates = rows.filter((r) => r.gate !== null).length;
653
- const summary = {
654
- total, // deduped input count === resolved + unresolved.length + errored.length
655
- resolved,
656
- unresolved: unresolved.length,
657
- errored: errored.length,
658
- far,
659
- dfars,
660
- gsam,
661
- other,
662
- gates,
663
- };
664
-
665
- // ── Disclosing notes — one per non-empty bucket + a single currency caveat. ─
666
- const notes: string[] = [];
667
- // Disclose the cap if it dropped clauses (the MCP Zod schema rejects >25, so
668
- // this only fires for a direct call — but a silent drop is never acceptable).
669
- if (deduped.length > total) {
670
- notes.push(
671
- `Input had ${deduped.length} distinct clauses; capped at ${MATRIX_MAX_CLAUSES} — the ${deduped.length - total} beyond the cap were NOT processed (they appear in NONE of rows/unresolved/errored). Split the list across calls to cover them all.`,
672
- );
673
- }
674
- if (unresolved.length > 0) {
675
- notes.push(
676
- `${unresolved.length} clause(s) not found in Title 48 as of ${asOfDate} (listed in unresolved). The clause number(s) may be wrong, reserved, or removed in this edition — this IS a real answer, not a service problem.`,
677
- );
678
- }
679
- if (errored.length > 0) {
680
- notes.push(
681
- `${errored.length} clause(s) could not be fetched due to a service issue (listed in errored) — retry. This is NOT a confirmation they don't exist; a DOWN/failing eCFR is distinct from a genuinely-absent clause.`,
682
- );
683
- }
684
- // Surface the RFO currency caveat ONCE if any resolved row is FAR/DFARS (reuse
685
- // farClauseLookup's wording — eCFR carries only the CODIFIED FAR/DFARS).
686
- if (rows.some((r) => r.regulation === "FAR" || r.regulation === "DFARS")) {
687
- notes.push(
688
- `RFO caveat: eCFR carries the CODIFIED FAR/DFARS only. The Revolutionary FAR Overhaul is replacing FAR parts via agency class deviations that may not appear here — verify the controlling deviation (each row's farOverhaulRisk.authoritativeList) before relying on a clause.`,
689
- );
690
- }
691
-
692
- const data = { asOfDate, rows, unresolved, errored, summary };
693
-
694
- return withMeta(data, {
695
- source: "ecfr:versioner/full (matrix over far_clause_lookup)",
696
- keylessMode: true,
697
- returned: rows.length,
698
- // A compliance matrix has NO upstream "match count" — it's a lookup over a
699
- // caller-supplied clause list, and the requested count is `summary.total`.
700
- // Use null (not `total`): with returned<total when clauses FAIL, buildMeta
701
- // would force `truncated:true` (meta.ts:104), falsely signalling a cap when
702
- // the missing clauses are actually disclosed in unresolved/errored. complete
703
- // is already explicit-false in that case; truncated must stay false.
704
- totalAvailable: null,
705
- // Explicit false whenever ANY clause didn't resolve; undefined lets buildMeta
706
- // derive true for the all-resolved case.
707
- complete:
708
- unresolved.length === 0 && errored.length === 0 ? undefined : false,
709
- // ONLY the errored/outage bucket counts as degradation — a genuine not_found
710
- // is a real answer, not a fetch failure.
711
- degraded: errored.length
712
- ? { attempted: total, succeeded: resolved, failed: errored.length }
713
- : undefined,
714
- filtersApplied: [],
715
- filtersDropped: [],
716
- notes,
717
- });
718
- }
719
-
720
- // ════════════════════════════════════════════════════════════════════════════
721
- // far_search — FAR/DFARS-scoped semantic search (the discovery front-door).
722
- //
723
- // COMPOSES ecfr.search. It fixes the two compliance-use-case flaws of the raw
724
- // full-text ecfr_search: (1) it mixes GSAM/agency-supplement sections into FAR
725
- // results (the 552-over-52 mis-rank), and (2) it returns eCFR's ~5×-per-section
726
- // HISTORICAL duplicates. far_search scopes by chapter (FAR=1 / DFARS=2) — which
727
- // keeps GSAM (chapter 5) and other supplements out at the source — and collapses
728
- // each section's historical versions to the CURRENT one (ends_on==null). It's
729
- // the "which clauses touch topic X" front-door that then feeds far_clause_lookup
730
- // for authoritative text.
731
- //
732
- // TRUTHFULNESS invariants (a reviewer WILL attack these):
733
- // - scope:far returns ONLY FAR (chapter-1) rows — no GSAM/agency-supplement
734
- // leakage. The chapter filter bites server-side; we also never re-admit a
735
- // non-FAR section.
736
- // - dedupeVersions NEVER drops a DISTINCT section — it only collapses the SAME
737
- // section's historical dups; the raw→distinct collapse is disclosed, and
738
- // dedupeVersions:false returns every raw row (incl. historical).
739
- // - isCurrent per row === (endsOn==null), honest. A section with NO current
740
- // row in the window keeps its LATEST version, marked isCurrent:false + noted.
741
- // - A search-endpoint FAILURE PROPAGATES (ecfr.search throws) — a DOWN service
742
- // must NEVER read as "0 results" (the load-bearing project lesson).
743
- // - No fabricated totalAvailable — a deduped view has no clean upstream count.
744
- // ════════════════════════════════════════════════════════════════════════════
745
-
746
- /** Which regulation family a far_search scope targets. */
747
- type FarSearchScope = "far" | "dfars" | "both";
748
-
749
- /** eCFR Title-48 chapter for a single-regulation scope (1=FAR, 2=DFARS). */
750
- const SCOPE_CHAPTER: Record<"far" | "dfars", number> = { far: 1, dfars: 2 };
751
-
752
- /** The mapped shape of one ecfr.search result row (fields far_search consumes). */
753
- type EcfrSearchRow = Awaited<ReturnType<typeof ecfrSearch>>["data"]["results"][number];
754
-
755
- /** One far_search result row. */
756
- type FarSearchRow = {
757
- regulation: Regulation;
758
- type: string;
759
- /** The FAR/DFARS part as a number (null if unparseable), for partsOnly. */
760
- part: number | null;
761
- section: string;
762
- headingPath: string;
763
- excerpt: string;
764
- score: number;
765
- ecfrUrl: string;
766
- effectiveOn: string;
767
- endsOn: string | null;
768
- /** endsOn==null ⇒ the CURRENT (in-force) version; false ⇒ a kept historical. */
769
- isCurrent: boolean;
770
- };
771
-
772
- /** Regulation family from the eCFR chapter we queried, falling back to the
773
- * section prefix (252.→DFARS, 52.→FAR) when a row's chapter is ambiguous. The
774
- * queried chapter is authoritative (the server-side filter guarantees it), so we
775
- * prefer it and only consult the prefix as a defense-in-depth cross-check. */
776
- function regulationForRow(queriedChapter: number, section: string): Regulation {
777
- if (queriedChapter === 1) return "FAR";
778
- if (queriedChapter === 2) return "DFARS";
779
- // Defensive fallback (should not hit for scope far/dfars): infer from prefix.
780
- return regulationFor(section);
781
- }
782
-
783
- /** Parse a hierarchy.part string to a number; null when absent/unparseable. */
784
- function partNumber(part: string | undefined): number | null {
785
- if (part === undefined || part === "") return null;
786
- const n = Number(part);
787
- return Number.isFinite(n) ? n : null;
788
- }
789
-
790
- /**
791
- * Map one raw ecfr.search row → a FarSearchRow, tagging regulation from the
792
- * chapter we queried with and deriving isCurrent from endsOn.
793
- */
794
- function mapFarRow(raw: EcfrSearchRow, queriedChapter: number): FarSearchRow {
795
- return {
796
- regulation: regulationForRow(queriedChapter, raw.section ?? ""),
797
- type: raw.type ?? "",
798
- part: partNumber(raw.part),
799
- section: raw.section ?? "",
800
- headingPath: raw.headingPath ?? "",
801
- excerpt: raw.excerpt ?? "",
802
- score: raw.score ?? 0,
803
- ecfrUrl: raw.ecfrUrl ?? "",
804
- effectiveOn: raw.effectiveOn ?? "",
805
- endsOn: raw.endsOn ?? null,
806
- isCurrent: (raw.endsOn ?? null) === null,
807
- };
808
- }
809
-
810
- /**
811
- * Collapse same-section historical versions to ONE row per distinct section:
812
- * keep the CURRENT version (endsOn==null) if present; otherwise keep the LATEST
813
- * (max effectiveOn) and mark it isCurrent:false. NEVER drops a distinct section —
814
- * only same-section dups. Input order of first appearance is preserved.
815
- */
816
- /**
817
- * The dedup/identity key for a row. Numbered sections key on `section`. But eCFR
818
- * returns chapter/part/subpart-level hits — e.g. "Appendix G to Chapter 2" — with
819
- * NO hierarchy.section; those must NOT all collide on "" (which would silently
820
- * collapse DISTINCT appendices into one and corrupt distinctSections). Fall back
821
- * to headingPath, then ecfrUrl, then a per-row anon sentinel — so every DISTINCT
822
- * entity gets a distinct key, while a single section's own historical versions
823
- * still group together (their headingPath/ecfrUrl is stable across versions).
824
- */
825
- function rowKey(row: FarSearchRow, index: number): string {
826
- return row.section || row.headingPath || row.ecfrUrl || `__anon_${index}`;
827
- }
828
-
829
- function dedupeBySection(rows: FarSearchRow[]): FarSearchRow[] {
830
- const order: string[] = [];
831
- const bySection = new Map<string, FarSearchRow>();
832
- rows.forEach((row, i) => {
833
- const key = rowKey(row, i);
834
- const existing = bySection.get(key);
835
- if (existing === undefined) {
836
- order.push(key);
837
- bySection.set(key, row);
838
- return;
839
- }
840
- // Prefer a current row; between two non-current rows keep the later one.
841
- if (existing.isCurrent) return; // already have the current version
842
- if (row.isCurrent) {
843
- bySection.set(key, row);
844
- return;
845
- }
846
- // Both historical → keep the one with the later effectiveOn (string compare
847
- // is correct for ISO YYYY-MM-DD dates).
848
- if (row.effectiveOn > existing.effectiveOn) bySection.set(key, row);
849
- });
850
- return order.map((k) => bySection.get(k) as FarSearchRow);
851
- }
852
-
853
- export async function farSearch(args: {
854
- query: string;
855
- scope?: FarSearchScope;
856
- dedupeVersions?: boolean;
857
- partsOnly?: number[];
858
- perPage?: number;
859
- }) {
860
- const scope: FarSearchScope = args.scope ?? "far";
861
- const dedupeVersions = args.dedupeVersions ?? true;
862
- const perPage = args.perPage ?? 5;
863
- const partsOnly =
864
- args.partsOnly && args.partsOnly.length > 0 ? args.partsOnly : null;
865
-
866
- // Fetch a LARGER raw window than perPage because dedup + partsOnly collapse
867
- // rows. Cap at 50 (eCFR search allows more, but 50 is plenty for a top-N view).
868
- const rawWindow = Math.min(perPage * 5, 50);
869
-
870
- // ── Fetch the raw rows. A search-endpoint failure PROPAGATES (ecfr.search
871
- // throws via fetchWithRetry) — never caught→empty. `both` = two calls merged.
872
- const chapters: number[] =
873
- scope === "both" ? [1, 2] : [SCOPE_CHAPTER[scope]];
874
- let hitWindowCap = false;
875
- let offScopeDropped = 0;
876
- const mapped: FarSearchRow[] = [];
877
- for (const chapter of chapters) {
878
- const res = await ecfrSearch({
879
- query: args.query,
880
- titleNumber: 48,
881
- chapter,
882
- perPage: rawWindow,
883
- });
884
- const rows = res.data.results;
885
- // If a chapter's raw page filled the window, MORE distinct rows may exist
886
- // beyond it → disclose truncation.
887
- if (rows.length >= rawWindow) hitWindowCap = true;
888
- for (const raw of rows) {
889
- // DEFENSE-IN-DEPTH (the load-bearing "no leakage" invariant): the chapter
890
- // filter is server-side, but never TRUST it blindly — if a row's OWN
891
- // hierarchy.chapter doesn't match the chapter we queried (a GSAM/agency
892
- // section that slipped through), DROP it rather than mislabel it FAR/DFARS.
893
- // scope:far returns ONLY FAR (chapter 1) rows, full stop. A row with no
894
- // chapter at all is kept (the server filter is the primary guarantee; we
895
- // only reject a row that positively contradicts the queried scope).
896
- const rawChapter =
897
- raw.chapter !== undefined && raw.chapter !== ""
898
- ? Number(raw.chapter)
899
- : null;
900
- if (rawChapter !== null && rawChapter !== chapter) {
901
- offScopeDropped++;
902
- continue;
903
- }
904
- mapped.push(mapFarRow(raw, chapter));
905
- }
906
- }
907
-
908
- // ── partsOnly (client-side): restrict to rows whose part is in the list. ──
909
- const partFiltered = partsOnly
910
- ? mapped.filter((r) => r.part !== null && partsOnly.includes(r.part))
911
- : mapped;
912
-
913
- // ── dedupeVersions (default true): collapse same-section historical dups. ──
914
- const deduped = dedupeVersions
915
- ? dedupeBySection(partFiltered)
916
- : partFiltered;
917
-
918
- // ── Return the top perPage DISTINCT rows. ────────────────────────────────
919
- const rows = deduped.slice(0, perPage);
920
- // Count distinct on the SAME key dedupe uses (section, falling back to
921
- // headingPath/ecfrUrl for section-less appendix/part-level hits) — counting on
922
- // `section` alone would report every section-less appendix as one.
923
- const distinctSections = new Set(rows.map((r, i) => rowKey(r, i))).size;
924
-
925
- // A raw window that filled up, OR a post-slice cut, both mean more may exist.
926
- const truncated = hitWindowCap || deduped.length > rows.length;
927
-
928
- // ── Currency (Title 48) — placed in `data` because buildMeta drops it. ────
929
- const currency = await title48Currency();
930
-
931
- // ── Disclosing notes. ─────────────────────────────────────────────────────
932
- // A human-readable label for the scope (used in several notes below).
933
- const scopeRegLabel =
934
- scope === "far" ? "FAR" : scope === "dfars" ? "DFARS" : "FAR/DFARS";
935
- const notes: string[] = [];
936
- // The raw→distinct collapse note reports the IN-SCOPE rows (post off-scope
937
- // drop), so it reflects historical-version collapse only, not the scope guard.
938
- const inScopeRaw = mapped.length;
939
- if (dedupeVersions && inScopeRaw > deduped.length) {
940
- notes.push(
941
- `${inScopeRaw} raw result(s) → ${deduped.length} distinct current section(s) (historical versions collapsed; set dedupeVersions:false to see all).`,
942
- );
943
- }
944
- // Disclose the defense-in-depth scope guard if it dropped any off-scope row (a
945
- // GSAM/agency-supplement section the server-side chapter filter let slip).
946
- if (offScopeDropped > 0) {
947
- notes.push(
948
- `${offScopeDropped} result(s) outside the requested scope (${scopeRegLabel}) were dropped by a defense-in-depth chapter check — far_search returns ONLY ${scopeRegLabel} (Title 48 chapter ${chapters.join("/")}) sections, never GSAM/agency-supplement leakage.`,
949
- );
950
- }
951
- // Disclose any kept-historical row (a distinct section with NO current version
952
- // in the window) so isCurrent:false is never a silent surprise.
953
- const keptHistorical = rows.filter((r) => !r.isCurrent).map((r) => r.section);
954
- if (dedupeVersions && keptHistorical.length > 0) {
955
- notes.push(
956
- `${keptHistorical.length} section(s) had NO current (in-force) version within the fetched window, so their LATEST historical version was kept and marked isCurrent:false: ${keptHistorical.join(", ")}. Confirm the current text with far_clause_lookup.`,
957
- );
958
- }
959
- if (truncated) {
960
- notes.push(
961
- `More distinct sections may exist beyond this view (the raw search window or the perPage limit was reached). Narrow the query or raise perPage to see more.`,
962
- );
963
- }
964
- // The RFO currency caveat is ALWAYS surfaced (structural, never per-row-fabricated).
965
- notes.push(
966
- `RFO caveat: eCFR carries the CODIFIED ${scopeRegLabel} only. The Revolutionary FAR Overhaul is replacing FAR parts via agency class deviations that may not appear here — verify the controlling deviation (farOverhaulRisk.authoritativeList) before relying on a result.`,
967
- );
968
-
969
- // farOverhaulRisk applies to the whole scope. Pass a representative regulation
970
- // (FAR for far/both, DFARS for dfars) — the caveat text/URLs are identical; the
971
- // appliesTo tag reflects the scope's primary family.
972
- const farOverhaulRisk = buildFarOverhaulRisk(
973
- scope === "dfars" ? "DFARS" : "FAR",
974
- );
975
-
976
- // Currency + farOverhaulRisk live in `data` (top-level), NOT the meta partial:
977
- // buildMeta finalizes a FIXED-shape ResponseMeta and drops unknown keys, so
978
- // these would be silently discarded if passed via _meta (mirrors far_clause_lookup).
979
- const data = {
980
- query: args.query,
981
- scope,
982
- rows,
983
- returned: rows.length,
984
- distinctSections,
985
- titleUpToDateAsOf: currency.upToDateAsOf,
986
- farOverhaulRisk,
987
- };
988
-
989
- const filtersApplied = ["scope"];
990
- if (partsOnly) filtersApplied.push("partsOnly");
991
- if (dedupeVersions) filtersApplied.push("dedupeVersions");
992
-
993
- return withMeta(data, {
994
- source: "ecfr:search/v1 (FAR-scoped)",
995
- keylessMode: true,
996
- returned: rows.length,
997
- // A deduped/scoped view has NO clean upstream match count (eCFR's total_count
998
- // counts RAW historical versions across the whole title-chapter, not distinct
999
- // current sections). Do NOT fabricate one — null is the honest answer.
1000
- totalAvailable: null,
1001
- // With totalAvailable null, buildMeta cannot derive truncation, so we pass it
1002
- // explicitly when the window/limit was hit (more distinct rows may exist).
1003
- truncated,
1004
- filtersApplied,
1005
- filtersDropped: [],
1006
- fieldsUnavailable: [],
1007
- notes,
1008
- });
1009
- }
1
+ /**
2
+ * FAR / DFARS clause lookup (keyless) — the authoritative clause text + its
3
+ * prescription, from the eCFR **versioner full** endpoint.
4
+ *
5
+ * Why this exists (and why NOT `ecfr_search`)
6
+ * -------------------------------------------
7
+ * The shipped full-text `ecfr_search` mis-ranks EXACT clause numbers: a bare
8
+ * `52.212-4` query returns GSAM **552**.212-4 above the real FAR 52.212-4
9
+ * (doc-09 §1.2). A proposal writer needs the AUTHORITATIVE clause text AND the
10
+ * rule that says WHEN it applies ("As prescribed in …") — the exact pair. The
11
+ * clean path is the versioner full endpoint, which `src/ecfr.ts` does not call:
12
+ *
13
+ * GET /api/versioner/v1/full/{date}/title-48.xml?section={clause}
14
+ *
15
+ * keyless, HTTP 200 (~40 KB XML) for a real clause, clean HTTP 404
16
+ * `{"error":"No matching content found."}` for an absent one. Title 48 = FAR.
17
+ * `{date}` defaults to Title 48 `up_to_date_as_of` (from listTitles, cached).
18
+ *
19
+ * TRUTHFULNESS invariants (a reviewer WILL try to break these):
20
+ * - A DOWN/failing eCFR service must NEVER read as "clause not found": only a
21
+ * genuine HTTP 404 maps to `not_found`; any other fetch error propagates
22
+ * with fetchWithRetry's classification (retryable 5xx/network/etc.).
23
+ * - A genuinely-absent clause is `not_found` (retryable:false), NEVER a fake
24
+ * empty clause. Clause text is never silently dropped or fabricated.
25
+ * - A prescription-section fetch failure is NON-FATAL: `prescription:null` +
26
+ * disclosed in `_meta.notes`; it never crashes the clause result.
27
+ * - `farOverhaulRisk` carries NO fabricated FAR-case numbers/dates — only the
28
+ * fixed structural caveat + the real authoritative-list / deviation URLs.
29
+ *
30
+ * Self-contained: imports `getText` from ./datasource.js (the shared XML/text
31
+ * fetch port, ADR-0013) + ToolErrorCarrier from ./errors.js, memoize from
32
+ * ./cache.js, listTitles from ./ecfr.js, and withMeta from ./meta.js — the
33
+ * versioner XML parse lives here (ecfr.ts's stripHtml is private and stays
34
+ * private).
35
+ */
36
+
37
+ import { ToolErrorCarrier } from "./errors.js";
38
+ import { getText } from "./datasource.js";
39
+ import { memoize } from "./cache.js";
40
+ import { listTitles, search as ecfrSearch } from "./ecfr.js";
41
+ import { withMeta } from "./meta.js";
42
+
43
+ const ECFR = "https://www.ecfr.gov/api";
44
+
45
+ /** The regulation family a clause number belongs to (from its prefix). */
46
+ type Regulation = "FAR" | "DFARS" | "GSAM" | "other";
47
+
48
+ /**
49
+ * Fetch a versioner-full XML document as text via the shared `getText` port
50
+ * (ADR-0013). This thin wrapper owns the far-specific, path-bearing `ecfr:…`
51
+ * label derivation (kept LOCAL — NOT hoisted into the port), then delegates:
52
+ * `getText` retries via fetchWithRetry (retry defaults true) and returns the raw
53
+ * body. The versioner endpoint serves `title-48.xml`. fetchWithRetry throws a
54
+ * classified ToolErrorCarrier on any non-2xx (404 → not_found, 5xx →
55
+ * upstream_unavailable, network → upstream_unavailable), which callers here
56
+ * either map (404 on the CLAUSE) or let propagate.
57
+ */
58
+ async function fetchText(url: string): Promise<string> {
59
+ return getText(url, {
60
+ label: `ecfr:${url.split("/api/")[1] ?? url}`,
61
+ headers: { Accept: "application/xml" },
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Strip XML tags → clean, paragraph-preserving plain text. The versioner body
67
+ * is block XML (`<P>`, `<HD1>`, `<EXTRACT>`, `<I>`…); we drop the tags but keep
68
+ * paragraph boundaries as spaces so the clause reads as continuous prose, then
69
+ * decode the handful of numeric/entity refs the feed uses (—, &, ", ', <, >).
70
+ */
71
+ function stripXml(s: string): string {
72
+ return s
73
+ // Block-level closers become a space so paragraphs don't run together.
74
+ .replace(/<\/(P|HD1|HEAD|EXTRACT|CITA|EDNOTE|PSPACE|HED|DIV8|LI)>/gi, " ")
75
+ // Drop every remaining tag.
76
+ .replace(/<[^>]+>/g, " ")
77
+ // Decode the entities the eCFR XML actually emits.
78
+ .replace(/&#8212;|&mdash;/gi, "—")
79
+ .replace(/&#8211;|&ndash;/gi, "–")
80
+ .replace(/&#8217;|&rsquo;/gi, "’")
81
+ .replace(/&#8220;|&ldquo;/gi, "“")
82
+ .replace(/&#8221;|&rdquo;/gi, "”")
83
+ .replace(/&quot;/gi, '"')
84
+ .replace(/&apos;/gi, "'")
85
+ .replace(/&amp;/gi, "&")
86
+ .replace(/&lt;/gi, "<")
87
+ .replace(/&gt;/gi, ">")
88
+ // Collapse whitespace.
89
+ .replace(/\s+/g, " ")
90
+ .trim();
91
+ }
92
+
93
+ /**
94
+ * Normalize a raw clauseNumber to its bare `NN.NNN-N` / `NNN.NNN-NNNN` core by
95
+ * stripping ONLY a leading `FAR`/`DFARS` prefix (case-insensitive) and
96
+ * surrounding whitespace. It DOES NOT strip embedded characters: doing so would
97
+ * fabricate a plausible-but-wrong clause from garbage (e.g. `52.212-4extra5` →
98
+ * `52.212-45`, a DIFFERENT real clause) that then passes CLAUSE_RE and fetches a
99
+ * silently-wrong answer. By leaving embedded/trailing junk in place, a non-clause
100
+ * input fails CLAUSE_RE below and farClauseLookup throws `invalid_input` — exactly
101
+ * as the server Zod boundary (regex `^\s*(?:d?far[s]?\b[\s.:#-]*)?\d{1,3}\.\d{3,4}-\d{1,4}\s*$`)
102
+ * already rejects it, so far.ts is safe called standalone. The legit shapes
103
+ * (`52.212-4`, `FAR 52.212-4`, `DFARS 252.204-7012`, ` 52.212-4 `) still collapse
104
+ * to the bare clause and succeed.
105
+ */
106
+ function normalizeClauseNumber(raw: string): string {
107
+ return (raw ?? "")
108
+ .trim()
109
+ // Strip ONLY the leading regulation prefix (FAR 52.212-4 / DFARS 252.204-7012)
110
+ // and its separator; NOTHING else is removed, so embedded garbage survives to
111
+ // be rejected by CLAUSE_RE (never mangled into a valid-looking clause).
112
+ .replace(/^\s*(?:d?far[s]?)\b[\s.:#-]*/i, "")
113
+ .trim();
114
+ }
115
+
116
+ const CLAUSE_RE = /^\d{1,3}\.\d{3,4}-\d{1,4}$/;
117
+
118
+ /** Regulation family from the clause-number prefix (deterministic, no fetch). */
119
+ function regulationFor(clauseNumber: string): Regulation {
120
+ if (/^252\./.test(clauseNumber)) return "DFARS";
121
+ if (/^2\d\d\./.test(clauseNumber)) return "DFARS";
122
+ if (/^552\./.test(clauseNumber)) return "GSAM";
123
+ if (/^52\./.test(clauseNumber)) return "FAR";
124
+ return "other";
125
+ }
126
+
127
+ /**
128
+ * The RFO (Revolutionary FAR Overhaul) currency caveat — an ALWAYS-PRESENT
129
+ * structural flag, NOT a per-clause claim. eCFR reflects the CODIFIED FAR only;
130
+ * the RFO is replacing FAR parts via agency class deviations that may not appear
131
+ * in eCFR, so a clause can be current in the CFR yet operationally superseded.
132
+ *
133
+ * This is the HONEST design (doc-09 §3 baked unverified FAR-case numbers/dates
134
+ * as [가설] — those go stale/wrong). We ship the never-stale-wrong version: no
135
+ * fabricated specifics, only the fixed caveat + the real authoritative-list and
136
+ * deviation URLs (all VERIFIED HTTP 200, 2026-07-04). `appliesTo` scopes the
137
+ * caveat to the clause's own regulation family.
138
+ */
139
+ function buildFarOverhaulRisk(regulation: Regulation) {
140
+ return {
141
+ note:
142
+ "eCFR reflects the CODIFIED FAR only. The Revolutionary FAR Overhaul (RFO) is actively replacing FAR parts via agency class deviations that may NOT appear in eCFR — so this clause text can be technically current in the CFR yet operationally superseded. Verify the controlling deviation before relying on it.",
143
+ authoritativeList: "https://www.acquisition.gov/far-overhaul",
144
+ deviationSources: [
145
+ "https://www.acquisition.gov/far-overhaul",
146
+ "https://www.acquisition.gov/dfars",
147
+ "https://www.acq.osd.mil/dpap/dars/",
148
+ ],
149
+ appliesTo: regulation,
150
+ };
151
+ }
152
+
153
+ /** Title 48 currency metadata, cached (titles.json changes infrequently). */
154
+ async function title48Currency(): Promise<{
155
+ upToDateAsOf: string | null;
156
+ latestAmendedOn: string | null;
157
+ }> {
158
+ return memoize("far:title48-currency", async () => {
159
+ // listTitles now returns a MetaBundle (honesty envelope); the titles live on
160
+ // `.data.titles`.
161
+ const { titles } = (await listTitles()).data;
162
+ const t48 = titles.find((t) => t.number === 48);
163
+ return {
164
+ upToDateAsOf: t48?.upToDateAsOf ?? null,
165
+ latestAmendedOn: t48?.latestAmendedOn ?? null,
166
+ };
167
+ });
168
+ }
169
+
170
+ /** Extract the first `<HEAD>…</HEAD>` inner text (tags stripped). */
171
+ function firstHead(xml: string): string | null {
172
+ const m = xml.match(/<HEAD>([\s\S]*?)<\/HEAD>/i);
173
+ if (!m || m[1] === undefined) return null;
174
+ const h = stripXml(m[1]);
175
+ return h.length > 0 ? h : null;
176
+ }
177
+
178
+ /**
179
+ * Does this body look like a REAL eCFR Title-48 section, vs an empty body, a
180
+ * CDN/WAF HTML interstitial, or a truncated proxy response? (Defect-2 guard.)
181
+ * Real versioner section XML carries an uppercase `<HEAD>…</HEAD>` plus
182
+ * substantive text. The `<HEAD>` test is CASE-SENSITIVE on purpose: an HTML
183
+ * challenge page uses lowercase `<head>`, and empty/truncated bodies carry
184
+ * neither — so a hollow 200 fails this check and is refused rather than parsed
185
+ * into a fake `complete:true` clause.
186
+ */
187
+ function looksLikeSectionXml(xml: string): boolean {
188
+ return /<HEAD>/.test(xml) && stripXml(xml).length >= 20;
189
+ }
190
+
191
+ /**
192
+ * Fetch + parse ONE Title-48 section as a prescription reference (its heading +
193
+ * stripped text). NON-FATAL by contract: returns null on ANY failure so a
194
+ * prescription problem never sinks the clause result. Memoized by URL.
195
+ */
196
+ async function fetchPrescription(
197
+ baseSection: string,
198
+ asOfDate: string,
199
+ ): Promise<{ section: string; heading: string | null; text: string } | null> {
200
+ const url = `${ECFR}/versioner/v1/full/${asOfDate}/title-48.xml?section=${baseSection}`;
201
+ try {
202
+ const xml = await memoize(`far:section:${asOfDate}:${baseSection}`, async () => {
203
+ const body = await fetchText(url);
204
+ // A hollow/interstitial 200 is a fetch FAILURE, not an empty prescription
205
+ // (Defect-2, non-fatal path). Throw so it is NOT cached and becomes null.
206
+ if (!looksLikeSectionXml(body))
207
+ throw new Error("non-section prescription body");
208
+ return body;
209
+ });
210
+ const heading = firstHead(xml);
211
+ const text = stripXml(xml);
212
+ return { section: baseSection, heading, text };
213
+ } catch {
214
+ // Any failure (404/5xx/network/parse/hollow-body) → null; caller discloses it.
215
+ return null;
216
+ }
217
+ }
218
+
219
+ export async function farClauseLookup(args: {
220
+ clauseNumber: string;
221
+ includePrescription?: boolean;
222
+ asOfDate?: string;
223
+ }) {
224
+ const clauseNumber = normalizeClauseNumber(args.clauseNumber ?? "");
225
+ const includePrescription = args.includePrescription ?? true;
226
+
227
+ // Defense-in-depth: the server Zod schema already rejects a non-matching
228
+ // clauseNumber, but guard here too so far.ts is safe called directly.
229
+ if (!CLAUSE_RE.test(clauseNumber)) {
230
+ throw new ToolErrorCarrier({
231
+ kind: "invalid_input",
232
+ message: `Invalid FAR/DFARS clause number '${args.clauseNumber}'. Expected a clause like 52.212-4, 252.204-7012, or 52.204-25 (optionally prefixed 'FAR'/'DFARS').`,
233
+ retryable: false,
234
+ upstreamEndpoint: "ecfr:versioner/v1/full/title-48",
235
+ });
236
+ }
237
+
238
+ const regulation = regulationFor(clauseNumber);
239
+
240
+ // asOfDate defaults to Title 48's up_to_date_as_of (cached).
241
+ const currency = await title48Currency();
242
+ const asOfDate = args.asOfDate ?? currency.upToDateAsOf ?? "";
243
+ // Guard (Defect 1): never query a blank/invalid date. If currency could NOT be
244
+ // resolved (titles.json returns 200 but Title 48 — or its up_to_date_as_of —
245
+ // is missing/renamed → upToDateAsOf:null, WITHOUT throwing) AND the caller gave
246
+ // no asOfDate, asOfDate is "". The versioner 404s on a blank-date URL, and that
247
+ // 404 would be mislabeled "clause not found" — a lie about a real, existing
248
+ // clause. This is a currency-RESOLUTION failure, not an absent clause.
249
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(asOfDate)) {
250
+ throw new ToolErrorCarrier({
251
+ kind: "schema_drift",
252
+ message: `Could not resolve Title 48's current codification date from the eCFR titles endpoint (up_to_date_as_of unavailable) and no asOfDate was supplied. Refusing to query a blank date — the versioner would return HTTP 404, which must NOT be reported as a missing clause. Retry shortly, or pass an explicit asOfDate (YYYY-MM-DD).`,
253
+ retryable: true,
254
+ upstreamEndpoint: "ecfr:versioner/v1/titles.json",
255
+ });
256
+ }
257
+ const isCurrent =
258
+ currency.upToDateAsOf !== null && asOfDate === currency.upToDateAsOf;
259
+
260
+ // Guard (Defect 3): a VALID-format asOfDate that is AFTER the latest eCFR
261
+ // codification has no versioner snapshot — the versioner 404s, and the generic
262
+ // not-found path below would mislabel it "clause not found (the clause number
263
+ // may be wrong, reserved, or removed)". That is a LIE about a real, current
264
+ // clause: the problem is the DATE (past the latest edition), not the clause.
265
+ // Common trigger: a caller infers asOfDate = today when today > up_to_date_as_of
266
+ // (observed in dogfood). Fail with an honest, actionable message and
267
+ // `invalid_input` (fix the date) rather than not_found (clause absent).
268
+ if (
269
+ currency.upToDateAsOf !== null &&
270
+ /^\d{4}-\d{2}-\d{2}$/.test(currency.upToDateAsOf) &&
271
+ asOfDate > currency.upToDateAsOf
272
+ ) {
273
+ throw new ToolErrorCarrier({
274
+ kind: "invalid_input",
275
+ message: `eCFR has no Title 48 codification as of ${asOfDate} — the latest available codification is ${currency.upToDateAsOf}. FAR clause ${clauseNumber} is NOT missing or removed; there is simply no eCFR snapshot for a date past the latest edition. Omit asOfDate to use the latest, or pass a date on or before ${currency.upToDateAsOf}.`,
276
+ retryable: false,
277
+ upstreamEndpoint: "ecfr:versioner/v1/full/title-48",
278
+ });
279
+ }
280
+
281
+ // ── Fetch the CLAUSE XML (memoized by URL). ─────────────────────────────
282
+ const clauseUrl = `${ECFR}/versioner/v1/full/${asOfDate}/title-48.xml?section=${clauseNumber}`;
283
+ let xml: string;
284
+ try {
285
+ xml = await memoize(`far:section:${asOfDate}:${clauseNumber}`, async () => {
286
+ const body = await fetchText(clauseUrl);
287
+ // Guard (Defect 2): a 200 with an empty body, a CDN/WAF HTML interstitial,
288
+ // or a truncated proxy response must NOT be parsed into a hollow
289
+ // `complete:true` clause (heading/text empty, yet ok:true). Require real
290
+ // Title-48 section XML. Throwing HERE (inside the memoize producer) keeps
291
+ // the bad body OUT of the cache so a retry re-fetches cleanly.
292
+ if (!looksLikeSectionXml(body)) {
293
+ throw new ToolErrorCarrier({
294
+ kind: "upstream_unavailable",
295
+ message: `The eCFR versioner returned HTTP 200 for ${clauseNumber} (as of ${asOfDate}) but the body was not a parseable Title 48 section (empty, truncated, or a CDN/WAF interstitial). Refusing to emit a hollow clause. Retry shortly.`,
296
+ retryable: true,
297
+ upstreamStatus: 200,
298
+ upstreamEndpoint: "ecfr:versioner/v1/full/title-48",
299
+ });
300
+ }
301
+ return body;
302
+ });
303
+ } catch (e) {
304
+ // A genuine 404 → not_found NAMING the clause (never null/empty). Any OTHER
305
+ // error (5xx/network/timeout) PROPAGATES with its classification so a DOWN
306
+ // service is never misread as "clause not found".
307
+ if (e instanceof ToolErrorCarrier && e.toolError.kind === "not_found") {
308
+ throw new ToolErrorCarrier({
309
+ kind: "not_found",
310
+ message: `FAR/DFARS clause ${clauseNumber} not found in Title 48 as of ${asOfDate}. (The eCFR versioner returned HTTP 404 "No matching content found" — the clause number may be wrong, reserved, or removed in this edition.)`,
311
+ retryable: false,
312
+ upstreamStatus: 404,
313
+ upstreamEndpoint: "ecfr:versioner/v1/full/title-48",
314
+ });
315
+ }
316
+ throw e;
317
+ }
318
+
319
+ // ── Parse the clause body. ──────────────────────────────────────────────
320
+ const rawHead = firstHead(xml);
321
+ // Strip a leading clause number if the HEAD duplicates it
322
+ // ("52.212-4 Contract Terms…" → "Contract Terms…").
323
+ const heading =
324
+ rawHead != null
325
+ ? rawHead.replace(new RegExp(`^${clauseNumber}\\s*[.:\\-—]?\\s*`), "").trim() ||
326
+ rawHead
327
+ : null;
328
+
329
+ const revMatch = xml.match(/\(([A-Z]{3}\.?\s+\d{4})\)/);
330
+ const revision = revMatch?.[1] ?? null;
331
+
332
+ const preMatch = xml.match(
333
+ /As prescribed in (\d{1,3}\.\d+(?:\([a-z0-9]+\))*)/i,
334
+ );
335
+ const prescribedIn = preMatch?.[1] ?? null;
336
+
337
+ // Detect clause vs provision from the prescribing verb. DFARS uses BOTH
338
+ // "insert the following …" and "use the following …" (Defect 3: the narrow
339
+ // /insert/-only regex silently mislabeled DFARS provisions as clauses). When
340
+ // NEITHER verb is present, default to "clause" but DISCLOSE it as inferred
341
+ // (see the note pushed below) rather than assert it.
342
+ const kindMatch = xml.match(/(?:insert|use) the following (clause|provision)/i);
343
+ const kindDetected = kindMatch?.[1]?.toLowerCase() as
344
+ | "clause"
345
+ | "provision"
346
+ | undefined;
347
+ const kind: "clause" | "provision" = kindDetected ?? "clause";
348
+
349
+ const text = stripXml(xml);
350
+
351
+ // ── Optional prescription section (non-fatal). ──────────────────────────
352
+ const notes: string[] = [];
353
+ let prescription:
354
+ | { section: string; heading: string | null; text: string }
355
+ | null = null;
356
+ let prescriptionDegraded = false;
357
+
358
+ if (includePrescription && prescribedIn) {
359
+ // Trim any trailing subparagraph to the base section: 12.301(b)(3) → 12.301.
360
+ const baseSection = prescribedIn.replace(/\(.*$/, "");
361
+ prescription = await fetchPrescription(baseSection, asOfDate);
362
+ if (prescription === null) {
363
+ prescriptionDegraded = true;
364
+ notes.push(
365
+ `The prescribing section ${baseSection} (from "As prescribed in ${prescribedIn}") could NOT be fetched — prescription is null. This is a partial result: the clause text above is complete, but the "when does this clause apply?" rule was not retrieved (fetch it directly at ${ECFR}/versioner/v1/full/${asOfDate}/title-48.xml?section=${baseSection}, or via ecfr on ${`https://www.ecfr.gov/current/title-48/section-${baseSection}`}).`,
366
+ );
367
+ }
368
+ } else if (includePrescription && !prescribedIn) {
369
+ notes.push(
370
+ "No 'As prescribed in …' pointer was found in this clause's text, so no prescription section was fetched (prescription:null). Some provisions/clauses carry the prescription in the parent subpart rather than an inline opener.",
371
+ );
372
+ }
373
+
374
+ // Disclose when `kind` was inferred rather than read from a verb (Defect 3):
375
+ // an undetected verb defaults to "clause", which would silently mislabel a
376
+ // provision — so the consumer is told the field is a default, not an assertion.
377
+ if (kindDetected === undefined) {
378
+ notes.push(
379
+ 'The instrument kind (clause vs provision) could NOT be determined from the text — no "insert/use the following clause/provision" verb was found — so kind defaults to "clause". Verify against the section heading if the clause-vs-provision distinction matters.',
380
+ );
381
+ }
382
+
383
+ const ecfrUrl = `https://www.ecfr.gov/current/title-48/section-${clauseNumber}`;
384
+
385
+ const farOverhaulRisk = buildFarOverhaulRisk(regulation);
386
+
387
+ // Currency disclosures.
388
+ if (!isCurrent) {
389
+ notes.push(
390
+ currency.upToDateAsOf
391
+ ? `asOfDate ${asOfDate} is NOT Title 48's current codification date (${currency.upToDateAsOf}); this is a point-in-time read of the FAR as of ${asOfDate}, which may differ from the clause in force today.`
392
+ : `Title 48's current codification date could not be confirmed from titles.json, so isCurrent is false; treat ${asOfDate} as the requested point-in-time edition.`,
393
+ );
394
+ }
395
+ // The RFO caveat is ALWAYS surfaced (structural, never per-clause-fabricated).
396
+ notes.push(
397
+ `RFO caveat: eCFR carries the CODIFIED ${regulation} only. The Revolutionary FAR Overhaul is replacing FAR parts via agency class deviations that may not appear here — verify the controlling deviation (farOverhaulRisk.authoritativeList) before relying on this clause.`,
398
+ );
399
+
400
+ // Currency + provenance live in `data` (top-level), NOT in the meta partial:
401
+ // the project's buildMeta (meta.ts) finalizes a FIXED-shape ResponseMeta and
402
+ // drops unknown keys, so asOfDate/isCurrent/farOverhaulRisk passed via _meta
403
+ // would be silently discarded. The design note anticipated this — carry them
404
+ // where they actually survive. The honest completeness/degradation signals
405
+ // (complete, fieldsUnavailable, notes) DO belong in _meta and are set there.
406
+ const data = {
407
+ clauseNumber,
408
+ kind,
409
+ regulation,
410
+ heading,
411
+ revision,
412
+ text,
413
+ prescribedIn,
414
+ prescription,
415
+ ecfrUrl,
416
+ // Point-in-time provenance for THIS read (mirrors the design note's _meta
417
+ // fields; placed in data so they are not dropped by buildMeta).
418
+ asOfDate,
419
+ titleUpToDateAsOf: currency.upToDateAsOf,
420
+ titleLatestAmendedOn: currency.latestAmendedOn,
421
+ isCurrent,
422
+ // Always-present structural currency caveat (never fabricated specifics).
423
+ farOverhaulRisk,
424
+ };
425
+
426
+ return withMeta(data, {
427
+ source: "ecfr:versioner/full",
428
+ keylessMode: true,
429
+ // A single authoritative clause record.
430
+ returned: 1,
431
+ totalAvailable: 1,
432
+ // A missing prescription is a genuine partial result → not complete.
433
+ complete: prescriptionDegraded ? false : undefined,
434
+ // fieldsUnavailable ONLY when we tried and failed to get the prescription.
435
+ fieldsUnavailable: prescriptionDegraded ? ["prescription"] : [],
436
+ filtersApplied: [],
437
+ filtersDropped: [],
438
+ notes,
439
+ });
440
+ }
441
+
442
+ // ════════════════════════════════════════════════════════════════════════════
443
+ // far_compliance_matrix — RFP cited-clause list → proposal-ready matrix.
444
+ //
445
+ // COMPOSES farClauseLookup: fan it out (bounded concurrency) over a deduped
446
+ // clause list and assemble a Section-L/M-ready matrix — each clause's text +
447
+ // prescription + whether it is a pass/fail eligibility GATE + the same currency
448
+ // caveat farClauseLookup carries.
449
+ //
450
+ // TRUTHFULNESS — the load-bearing split (the C19 lesson): each clause has THREE
451
+ // possible outcomes and "absent" is NEVER conflated with "couldn't fetch":
452
+ // 1. resolved → a full row in `rows[]`.
453
+ // 2. not_found (404) → `unresolved[]` (the clause genuinely isn't in Title 48).
454
+ // 3. any other error → `errored[]` (a DOWN/failing eCFR — retryable — must NOT
455
+ // read as "clause doesn't exist"; invalid_input too).
456
+ // Every input clause lands in EXACTLY one bucket; summary.total proves it. Gate
457
+ // tags come ONLY from the verified static GATE_MAP — never guessed.
458
+ // ════════════════════════════════════════════════════════════════════════════
459
+
460
+ /**
461
+ * Eligibility-gate map (STATIC, verified live 2026-07-04 — headings confirmed).
462
+ * A resolved row whose clauseNumber is a key here is a pass/fail award-eligibility
463
+ * gate; the value is the disclosed label. Kept deliberately SMALL and defensible:
464
+ * NEVER invent a gate meaning for a clause not in this map.
465
+ */
466
+ const GATE_MAP: Record<string, string> = {
467
+ "52.204-24":
468
+ "Section 889 — covered-telecom/video-surveillance prohibition (award-eligibility gate)",
469
+ "52.204-25":
470
+ "Section 889 — covered-telecom/video-surveillance prohibition (award-eligibility gate)",
471
+ "52.204-26":
472
+ "Section 889 — covered-telecom/video-surveillance prohibition (award-eligibility gate)",
473
+ "52.219-14":
474
+ "Limitations on Subcontracting — set-aside compliance gate",
475
+ "252.204-7012":
476
+ "Safeguarding Covered Defense Information + cyber incident reporting (CUI cyber gate)",
477
+ "252.204-7020": "NIST SP 800-171 DoD Assessment (cyber gate)",
478
+ "252.204-7021": "CMMC compliance (cyber gate)",
479
+ };
480
+
481
+ /** Hard ceiling on clauses processed per call (after dedupe). Mirrors the Zod cap. */
482
+ const MATRIX_MAX_CLAUSES = 25;
483
+ /** Bounded fan-out width — small pool so we never fire 25 eCFR fetches at once. */
484
+ const MATRIX_CONCURRENCY = 5;
485
+
486
+ /** One resolved matrix row: farClauseLookup's honest fields + a gate flag. */
487
+ type MatrixRow = {
488
+ clauseNumber: string;
489
+ kind: "clause" | "provision";
490
+ regulation: Regulation;
491
+ heading: string | null;
492
+ revision: string | null;
493
+ prescribedIn: string | null;
494
+ prescription:
495
+ | { section: string; heading: string | null; text: string }
496
+ | null;
497
+ text: string;
498
+ ecfrUrl: string;
499
+ farOverhaulRisk: ReturnType<typeof buildFarOverhaulRisk>;
500
+ /** The eligibility-gate label (from GATE_MAP), or null when not a mapped gate. */
501
+ gate: string | null;
502
+ };
503
+
504
+ /** A clause that did not resolve, with a disclosed reason. */
505
+ type UnresolvedClause = { clauseNumber: string; reason: string };
506
+
507
+ /**
508
+ * Run an async mapper over `items` with at most `width` in flight at once. A
509
+ * lightweight promise pool (worker-draining a shared cursor): each worker pulls
510
+ * the next index until the list is exhausted, so results are written back by
511
+ * original index. Preserves input order and never fires more than `width`
512
+ * concurrent fetches. Never rejects — the mapper itself must not throw (callers
513
+ * here wrap each unit in try/catch).
514
+ */
515
+ async function mapPool<T, R>(
516
+ items: readonly T[],
517
+ width: number,
518
+ mapper: (item: T, index: number) => Promise<R>,
519
+ ): Promise<R[]> {
520
+ const results = new Array<R>(items.length);
521
+ let cursor = 0;
522
+ const workerCount = Math.max(1, Math.min(width, items.length));
523
+ const worker = async () => {
524
+ for (;;) {
525
+ const i = cursor++;
526
+ if (i >= items.length) return;
527
+ results[i] = await mapper(items[i] as T, i);
528
+ }
529
+ };
530
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
531
+ return results;
532
+ }
533
+
534
+ export async function farComplianceMatrix(args: {
535
+ clauses: string[];
536
+ asOfDate?: string;
537
+ includePrescription?: boolean;
538
+ flagGates?: boolean;
539
+ }) {
540
+ const includePrescription = args.includePrescription ?? true;
541
+ const flagGates = args.flagGates !== false; // default true; only false disables
542
+
543
+ // ── Normalize + dedupe case-insensitively, then cap AFTER dedupe. ─────────
544
+ // normalizeClauseNumber already lowercases nothing (clause numbers are digits),
545
+ // but it strips FAR/DFARS prefixes + stray chars so "52.212-4", "FAR 52.212-4",
546
+ // and " 52.212-4 " collapse to one key. We keep the FIRST spelling's normalized
547
+ // form and preserve input order.
548
+ const seen = new Set<string>();
549
+ const deduped: string[] = [];
550
+ for (const raw of args.clauses ?? []) {
551
+ const norm = normalizeClauseNumber(raw ?? "");
552
+ // Keep even a non-matching normalized token: farClauseLookup will classify it
553
+ // as invalid_input → errored (NOT silently dropped). Dedupe on the normalized
554
+ // key so a malformed value that appears twice is only reported once.
555
+ const key = norm.toLowerCase();
556
+ if (seen.has(key)) continue;
557
+ seen.add(key);
558
+ deduped.push(norm);
559
+ }
560
+ const clauses = deduped.slice(0, MATRIX_MAX_CLAUSES);
561
+ const total = clauses.length;
562
+
563
+ // ── Resolve currency ONCE up front (avoid resolving it 25×). ──────────────
564
+ // farClauseLookup would resolve this per call; we resolve it here and pass an
565
+ // explicit asOfDate into each call. If currency can't be resolved AND no
566
+ // asOfDate was supplied, refuse with a SINGLE schema_drift rather than letting
567
+ // 25 identical ones bubble up (and a blank-date URL must never 404 into a fake
568
+ // "not found"). This mirrors farClauseLookup's Defect-1 guard.
569
+ const currency = await title48Currency();
570
+ const asOfDate = args.asOfDate ?? currency.upToDateAsOf ?? "";
571
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(asOfDate)) {
572
+ throw new ToolErrorCarrier({
573
+ kind: "schema_drift",
574
+ message: `Could not resolve Title 48's current codification date from the eCFR titles endpoint (up_to_date_as_of unavailable) and no asOfDate was supplied. Refusing to build a matrix against a blank date — the versioner would return HTTP 404, which must NOT be reported as missing clauses. Retry shortly, or pass an explicit asOfDate (YYYY-MM-DD).`,
575
+ retryable: true,
576
+ upstreamEndpoint: "ecfr:versioner/v1/titles.json",
577
+ });
578
+ }
579
+
580
+ // ── Fan out farClauseLookup with bounded concurrency, catching EACH clause
581
+ // individually so one failure never sinks the matrix. ─────────────────────
582
+ type Outcome =
583
+ | { status: "resolved"; row: MatrixRow }
584
+ | { status: "unresolved"; entry: UnresolvedClause }
585
+ | { status: "errored"; entry: UnresolvedClause };
586
+
587
+ const outcomes = await mapPool<string, Outcome>(
588
+ clauses,
589
+ MATRIX_CONCURRENCY,
590
+ async (clauseNumber): Promise<Outcome> => {
591
+ try {
592
+ const res = await farClauseLookup({
593
+ clauseNumber,
594
+ asOfDate,
595
+ includePrescription,
596
+ });
597
+ const d = res.data;
598
+ const gate = flagGates ? GATE_MAP[d.clauseNumber] ?? null : null;
599
+ const row: MatrixRow = {
600
+ clauseNumber: d.clauseNumber,
601
+ kind: d.kind,
602
+ regulation: d.regulation,
603
+ heading: d.heading,
604
+ revision: d.revision,
605
+ prescribedIn: d.prescribedIn,
606
+ prescription: d.prescription,
607
+ text: d.text,
608
+ ecfrUrl: d.ecfrUrl,
609
+ farOverhaulRisk: d.farOverhaulRisk,
610
+ gate,
611
+ };
612
+ return { status: "resolved", row };
613
+ } catch (e) {
614
+ const kind =
615
+ e instanceof ToolErrorCarrier ? e.toolError.kind : "unknown";
616
+ const reason =
617
+ e instanceof ToolErrorCarrier
618
+ ? e.toolError.message
619
+ : e instanceof Error
620
+ ? e.message
621
+ : String(e);
622
+ // A genuine 404 (absent clause) → unresolved. ANY OTHER kind (a fetch/
623
+ // service problem: upstream_unavailable / schema_drift / rate_limited /
624
+ // invalid_input / unknown) → errored. A DOWN eCFR must NEVER read as
625
+ // "clause doesn't exist".
626
+ if (kind === "not_found") {
627
+ return {
628
+ status: "unresolved",
629
+ entry: { clauseNumber, reason },
630
+ };
631
+ }
632
+ return { status: "errored", entry: { clauseNumber, reason } };
633
+ }
634
+ },
635
+ );
636
+
637
+ const rows: MatrixRow[] = [];
638
+ const unresolved: UnresolvedClause[] = [];
639
+ const errored: UnresolvedClause[] = [];
640
+ for (const o of outcomes) {
641
+ if (o.status === "resolved") rows.push(o.row);
642
+ else if (o.status === "unresolved") unresolved.push(o.entry);
643
+ else errored.push(o.entry);
644
+ }
645
+
646
+ // ── Summary (must be internally consistent). ──────────────────────────────
647
+ const resolved = rows.length;
648
+ const far = rows.filter((r) => r.regulation === "FAR").length;
649
+ const dfars = rows.filter((r) => r.regulation === "DFARS").length;
650
+ const gsam = rows.filter((r) => r.regulation === "GSAM").length;
651
+ const other = rows.filter((r) => r.regulation === "other").length;
652
+ const gates = rows.filter((r) => r.gate !== null).length;
653
+ const summary = {
654
+ total, // deduped input count === resolved + unresolved.length + errored.length
655
+ resolved,
656
+ unresolved: unresolved.length,
657
+ errored: errored.length,
658
+ far,
659
+ dfars,
660
+ gsam,
661
+ other,
662
+ gates,
663
+ };
664
+
665
+ // ── Disclosing notes — one per non-empty bucket + a single currency caveat. ─
666
+ const notes: string[] = [];
667
+ // Disclose the cap if it dropped clauses (the MCP Zod schema rejects >25, so
668
+ // this only fires for a direct call — but a silent drop is never acceptable).
669
+ if (deduped.length > total) {
670
+ notes.push(
671
+ `Input had ${deduped.length} distinct clauses; capped at ${MATRIX_MAX_CLAUSES} — the ${deduped.length - total} beyond the cap were NOT processed (they appear in NONE of rows/unresolved/errored). Split the list across calls to cover them all.`,
672
+ );
673
+ }
674
+ if (unresolved.length > 0) {
675
+ notes.push(
676
+ `${unresolved.length} clause(s) not found in Title 48 as of ${asOfDate} (listed in unresolved). The clause number(s) may be wrong, reserved, or removed in this edition — this IS a real answer, not a service problem.`,
677
+ );
678
+ }
679
+ if (errored.length > 0) {
680
+ notes.push(
681
+ `${errored.length} clause(s) could not be fetched due to a service issue (listed in errored) — retry. This is NOT a confirmation they don't exist; a DOWN/failing eCFR is distinct from a genuinely-absent clause.`,
682
+ );
683
+ }
684
+ // Surface the RFO currency caveat ONCE if any resolved row is FAR/DFARS (reuse
685
+ // farClauseLookup's wording — eCFR carries only the CODIFIED FAR/DFARS).
686
+ if (rows.some((r) => r.regulation === "FAR" || r.regulation === "DFARS")) {
687
+ notes.push(
688
+ `RFO caveat: eCFR carries the CODIFIED FAR/DFARS only. The Revolutionary FAR Overhaul is replacing FAR parts via agency class deviations that may not appear here — verify the controlling deviation (each row's farOverhaulRisk.authoritativeList) before relying on a clause.`,
689
+ );
690
+ }
691
+
692
+ const data = { asOfDate, rows, unresolved, errored, summary };
693
+
694
+ return withMeta(data, {
695
+ source: "ecfr:versioner/full (matrix over far_clause_lookup)",
696
+ keylessMode: true,
697
+ returned: rows.length,
698
+ // A compliance matrix has NO upstream "match count" — it's a lookup over a
699
+ // caller-supplied clause list, and the requested count is `summary.total`.
700
+ // Use null (not `total`): with returned<total when clauses FAIL, buildMeta
701
+ // would force `truncated:true` (meta.ts:104), falsely signalling a cap when
702
+ // the missing clauses are actually disclosed in unresolved/errored. complete
703
+ // is already explicit-false in that case; truncated must stay false.
704
+ totalAvailable: null,
705
+ // Explicit false whenever ANY clause didn't resolve; undefined lets buildMeta
706
+ // derive true for the all-resolved case.
707
+ complete:
708
+ unresolved.length === 0 && errored.length === 0 ? undefined : false,
709
+ // ONLY the errored/outage bucket counts as degradation — a genuine not_found
710
+ // is a real answer, not a fetch failure.
711
+ degraded: errored.length
712
+ ? { attempted: total, succeeded: resolved, failed: errored.length }
713
+ : undefined,
714
+ filtersApplied: [],
715
+ filtersDropped: [],
716
+ notes,
717
+ });
718
+ }
719
+
720
+ // ════════════════════════════════════════════════════════════════════════════
721
+ // far_search — FAR/DFARS-scoped semantic search (the discovery front-door).
722
+ //
723
+ // COMPOSES ecfr.search. It fixes the two compliance-use-case flaws of the raw
724
+ // full-text ecfr_search: (1) it mixes GSAM/agency-supplement sections into FAR
725
+ // results (the 552-over-52 mis-rank), and (2) it returns eCFR's ~5×-per-section
726
+ // HISTORICAL duplicates. far_search scopes by chapter (FAR=1 / DFARS=2) — which
727
+ // keeps GSAM (chapter 5) and other supplements out at the source — and collapses
728
+ // each section's historical versions to the CURRENT one (ends_on==null). It's
729
+ // the "which clauses touch topic X" front-door that then feeds far_clause_lookup
730
+ // for authoritative text.
731
+ //
732
+ // TRUTHFULNESS invariants (a reviewer WILL attack these):
733
+ // - scope:far returns ONLY FAR (chapter-1) rows — no GSAM/agency-supplement
734
+ // leakage. The chapter filter bites server-side; we also never re-admit a
735
+ // non-FAR section.
736
+ // - dedupeVersions NEVER drops a DISTINCT section — it only collapses the SAME
737
+ // section's historical dups; the raw→distinct collapse is disclosed, and
738
+ // dedupeVersions:false returns every raw row (incl. historical).
739
+ // - isCurrent per row === (endsOn==null), honest. A section with NO current
740
+ // row in the window keeps its LATEST version, marked isCurrent:false + noted.
741
+ // - A search-endpoint FAILURE PROPAGATES (ecfr.search throws) — a DOWN service
742
+ // must NEVER read as "0 results" (the load-bearing project lesson).
743
+ // - No fabricated totalAvailable — a deduped view has no clean upstream count.
744
+ // ════════════════════════════════════════════════════════════════════════════
745
+
746
+ /** Which regulation family a far_search scope targets. */
747
+ type FarSearchScope = "far" | "dfars" | "both";
748
+
749
+ /** eCFR Title-48 chapter for a single-regulation scope (1=FAR, 2=DFARS). */
750
+ const SCOPE_CHAPTER: Record<"far" | "dfars", number> = { far: 1, dfars: 2 };
751
+
752
+ /** The mapped shape of one ecfr.search result row (fields far_search consumes). */
753
+ type EcfrSearchRow = Awaited<ReturnType<typeof ecfrSearch>>["data"]["results"][number];
754
+
755
+ /** One far_search result row. */
756
+ type FarSearchRow = {
757
+ regulation: Regulation;
758
+ type: string;
759
+ /** The FAR/DFARS part as a number (null if unparseable), for partsOnly. */
760
+ part: number | null;
761
+ section: string;
762
+ headingPath: string;
763
+ excerpt: string;
764
+ score: number;
765
+ ecfrUrl: string;
766
+ effectiveOn: string;
767
+ endsOn: string | null;
768
+ /** endsOn==null ⇒ the CURRENT (in-force) version; false ⇒ a kept historical. */
769
+ isCurrent: boolean;
770
+ };
771
+
772
+ /** Regulation family from the eCFR chapter we queried, falling back to the
773
+ * section prefix (252.→DFARS, 52.→FAR) when a row's chapter is ambiguous. The
774
+ * queried chapter is authoritative (the server-side filter guarantees it), so we
775
+ * prefer it and only consult the prefix as a defense-in-depth cross-check. */
776
+ function regulationForRow(queriedChapter: number, section: string): Regulation {
777
+ if (queriedChapter === 1) return "FAR";
778
+ if (queriedChapter === 2) return "DFARS";
779
+ // Defensive fallback (should not hit for scope far/dfars): infer from prefix.
780
+ return regulationFor(section);
781
+ }
782
+
783
+ /** Parse a hierarchy.part string to a number; null when absent/unparseable. */
784
+ function partNumber(part: string | undefined): number | null {
785
+ if (part === undefined || part === "") return null;
786
+ const n = Number(part);
787
+ return Number.isFinite(n) ? n : null;
788
+ }
789
+
790
+ /**
791
+ * Map one raw ecfr.search row → a FarSearchRow, tagging regulation from the
792
+ * chapter we queried with and deriving isCurrent from endsOn.
793
+ */
794
+ function mapFarRow(raw: EcfrSearchRow, queriedChapter: number): FarSearchRow {
795
+ return {
796
+ regulation: regulationForRow(queriedChapter, raw.section ?? ""),
797
+ type: raw.type ?? "",
798
+ part: partNumber(raw.part),
799
+ section: raw.section ?? "",
800
+ headingPath: raw.headingPath ?? "",
801
+ excerpt: raw.excerpt ?? "",
802
+ score: raw.score ?? 0,
803
+ ecfrUrl: raw.ecfrUrl ?? "",
804
+ effectiveOn: raw.effectiveOn ?? "",
805
+ endsOn: raw.endsOn ?? null,
806
+ isCurrent: (raw.endsOn ?? null) === null,
807
+ };
808
+ }
809
+
810
+ /**
811
+ * Collapse same-section historical versions to ONE row per distinct section:
812
+ * keep the CURRENT version (endsOn==null) if present; otherwise keep the LATEST
813
+ * (max effectiveOn) and mark it isCurrent:false. NEVER drops a distinct section —
814
+ * only same-section dups. Input order of first appearance is preserved.
815
+ */
816
+ /**
817
+ * The dedup/identity key for a row. Numbered sections key on `section`. But eCFR
818
+ * returns chapter/part/subpart-level hits — e.g. "Appendix G to Chapter 2" — with
819
+ * NO hierarchy.section; those must NOT all collide on "" (which would silently
820
+ * collapse DISTINCT appendices into one and corrupt distinctSections). Fall back
821
+ * to headingPath, then ecfrUrl, then a per-row anon sentinel — so every DISTINCT
822
+ * entity gets a distinct key, while a single section's own historical versions
823
+ * still group together (their headingPath/ecfrUrl is stable across versions).
824
+ */
825
+ function rowKey(row: FarSearchRow, index: number): string {
826
+ return row.section || row.headingPath || row.ecfrUrl || `__anon_${index}`;
827
+ }
828
+
829
+ function dedupeBySection(rows: FarSearchRow[]): FarSearchRow[] {
830
+ const order: string[] = [];
831
+ const bySection = new Map<string, FarSearchRow>();
832
+ rows.forEach((row, i) => {
833
+ const key = rowKey(row, i);
834
+ const existing = bySection.get(key);
835
+ if (existing === undefined) {
836
+ order.push(key);
837
+ bySection.set(key, row);
838
+ return;
839
+ }
840
+ // Prefer a current row; between two non-current rows keep the later one.
841
+ if (existing.isCurrent) return; // already have the current version
842
+ if (row.isCurrent) {
843
+ bySection.set(key, row);
844
+ return;
845
+ }
846
+ // Both historical → keep the one with the later effectiveOn (string compare
847
+ // is correct for ISO YYYY-MM-DD dates).
848
+ if (row.effectiveOn > existing.effectiveOn) bySection.set(key, row);
849
+ });
850
+ return order.map((k) => bySection.get(k) as FarSearchRow);
851
+ }
852
+
853
+ export async function farSearch(args: {
854
+ query: string;
855
+ scope?: FarSearchScope;
856
+ dedupeVersions?: boolean;
857
+ partsOnly?: number[];
858
+ perPage?: number;
859
+ }) {
860
+ const scope: FarSearchScope = args.scope ?? "far";
861
+ const dedupeVersions = args.dedupeVersions ?? true;
862
+ const perPage = args.perPage ?? 5;
863
+ const partsOnly =
864
+ args.partsOnly && args.partsOnly.length > 0 ? args.partsOnly : null;
865
+
866
+ // Fetch a LARGER raw window than perPage because dedup + partsOnly collapse
867
+ // rows. Cap at 50 (eCFR search allows more, but 50 is plenty for a top-N view).
868
+ const rawWindow = Math.min(perPage * 5, 50);
869
+
870
+ // ── Fetch the raw rows. A search-endpoint failure PROPAGATES (ecfr.search
871
+ // throws via fetchWithRetry) — never caught→empty. `both` = two calls merged.
872
+ const chapters: number[] =
873
+ scope === "both" ? [1, 2] : [SCOPE_CHAPTER[scope]];
874
+ let hitWindowCap = false;
875
+ let offScopeDropped = 0;
876
+ const mapped: FarSearchRow[] = [];
877
+ for (const chapter of chapters) {
878
+ const res = await ecfrSearch({
879
+ query: args.query,
880
+ titleNumber: 48,
881
+ chapter,
882
+ perPage: rawWindow,
883
+ });
884
+ const rows = res.data.results;
885
+ // If a chapter's raw page filled the window, MORE distinct rows may exist
886
+ // beyond it → disclose truncation.
887
+ if (rows.length >= rawWindow) hitWindowCap = true;
888
+ for (const raw of rows) {
889
+ // DEFENSE-IN-DEPTH (the load-bearing "no leakage" invariant): the chapter
890
+ // filter is server-side, but never TRUST it blindly — if a row's OWN
891
+ // hierarchy.chapter doesn't match the chapter we queried (a GSAM/agency
892
+ // section that slipped through), DROP it rather than mislabel it FAR/DFARS.
893
+ // scope:far returns ONLY FAR (chapter 1) rows, full stop. A row with no
894
+ // chapter at all is kept (the server filter is the primary guarantee; we
895
+ // only reject a row that positively contradicts the queried scope).
896
+ const rawChapter =
897
+ raw.chapter !== undefined && raw.chapter !== ""
898
+ ? Number(raw.chapter)
899
+ : null;
900
+ if (rawChapter !== null && rawChapter !== chapter) {
901
+ offScopeDropped++;
902
+ continue;
903
+ }
904
+ mapped.push(mapFarRow(raw, chapter));
905
+ }
906
+ }
907
+
908
+ // ── partsOnly (client-side): restrict to rows whose part is in the list. ──
909
+ const partFiltered = partsOnly
910
+ ? mapped.filter((r) => r.part !== null && partsOnly.includes(r.part))
911
+ : mapped;
912
+
913
+ // ── dedupeVersions (default true): collapse same-section historical dups. ──
914
+ const deduped = dedupeVersions
915
+ ? dedupeBySection(partFiltered)
916
+ : partFiltered;
917
+
918
+ // ── Return the top perPage DISTINCT rows. ────────────────────────────────
919
+ const rows = deduped.slice(0, perPage);
920
+ // Count distinct on the SAME key dedupe uses (section, falling back to
921
+ // headingPath/ecfrUrl for section-less appendix/part-level hits) — counting on
922
+ // `section` alone would report every section-less appendix as one.
923
+ const distinctSections = new Set(rows.map((r, i) => rowKey(r, i))).size;
924
+
925
+ // A raw window that filled up, OR a post-slice cut, both mean more may exist.
926
+ const truncated = hitWindowCap || deduped.length > rows.length;
927
+
928
+ // ── Currency (Title 48) — placed in `data` because buildMeta drops it. ────
929
+ const currency = await title48Currency();
930
+
931
+ // ── Disclosing notes. ─────────────────────────────────────────────────────
932
+ // A human-readable label for the scope (used in several notes below).
933
+ const scopeRegLabel =
934
+ scope === "far" ? "FAR" : scope === "dfars" ? "DFARS" : "FAR/DFARS";
935
+ const notes: string[] = [];
936
+ // The raw→distinct collapse note reports the IN-SCOPE rows (post off-scope
937
+ // drop), so it reflects historical-version collapse only, not the scope guard.
938
+ const inScopeRaw = mapped.length;
939
+ if (dedupeVersions && inScopeRaw > deduped.length) {
940
+ notes.push(
941
+ `${inScopeRaw} raw result(s) → ${deduped.length} distinct current section(s) (historical versions collapsed; set dedupeVersions:false to see all).`,
942
+ );
943
+ }
944
+ // Disclose the defense-in-depth scope guard if it dropped any off-scope row (a
945
+ // GSAM/agency-supplement section the server-side chapter filter let slip).
946
+ if (offScopeDropped > 0) {
947
+ notes.push(
948
+ `${offScopeDropped} result(s) outside the requested scope (${scopeRegLabel}) were dropped by a defense-in-depth chapter check — far_search returns ONLY ${scopeRegLabel} (Title 48 chapter ${chapters.join("/")}) sections, never GSAM/agency-supplement leakage.`,
949
+ );
950
+ }
951
+ // Disclose any kept-historical row (a distinct section with NO current version
952
+ // in the window) so isCurrent:false is never a silent surprise.
953
+ const keptHistorical = rows.filter((r) => !r.isCurrent).map((r) => r.section);
954
+ if (dedupeVersions && keptHistorical.length > 0) {
955
+ notes.push(
956
+ `${keptHistorical.length} section(s) had NO current (in-force) version within the fetched window, so their LATEST historical version was kept and marked isCurrent:false: ${keptHistorical.join(", ")}. Confirm the current text with far_clause_lookup.`,
957
+ );
958
+ }
959
+ if (truncated) {
960
+ notes.push(
961
+ `More distinct sections may exist beyond this view (the raw search window or the perPage limit was reached). Narrow the query or raise perPage to see more.`,
962
+ );
963
+ }
964
+ // The RFO currency caveat is ALWAYS surfaced (structural, never per-row-fabricated).
965
+ notes.push(
966
+ `RFO caveat: eCFR carries the CODIFIED ${scopeRegLabel} only. The Revolutionary FAR Overhaul is replacing FAR parts via agency class deviations that may not appear here — verify the controlling deviation (farOverhaulRisk.authoritativeList) before relying on a result.`,
967
+ );
968
+
969
+ // farOverhaulRisk applies to the whole scope. Pass a representative regulation
970
+ // (FAR for far/both, DFARS for dfars) — the caveat text/URLs are identical; the
971
+ // appliesTo tag reflects the scope's primary family.
972
+ const farOverhaulRisk = buildFarOverhaulRisk(
973
+ scope === "dfars" ? "DFARS" : "FAR",
974
+ );
975
+
976
+ // Currency + farOverhaulRisk live in `data` (top-level), NOT the meta partial:
977
+ // buildMeta finalizes a FIXED-shape ResponseMeta and drops unknown keys, so
978
+ // these would be silently discarded if passed via _meta (mirrors far_clause_lookup).
979
+ const data = {
980
+ query: args.query,
981
+ scope,
982
+ rows,
983
+ returned: rows.length,
984
+ distinctSections,
985
+ titleUpToDateAsOf: currency.upToDateAsOf,
986
+ farOverhaulRisk,
987
+ };
988
+
989
+ const filtersApplied = ["scope"];
990
+ if (partsOnly) filtersApplied.push("partsOnly");
991
+ if (dedupeVersions) filtersApplied.push("dedupeVersions");
992
+
993
+ return withMeta(data, {
994
+ source: "ecfr:search/v1 (FAR-scoped)",
995
+ keylessMode: true,
996
+ returned: rows.length,
997
+ // A deduped/scoped view has NO clean upstream match count (eCFR's total_count
998
+ // counts RAW historical versions across the whole title-chapter, not distinct
999
+ // current sections). Do NOT fabricate one — null is the honest answer.
1000
+ totalAvailable: null,
1001
+ // With totalAvailable null, buildMeta cannot derive truncation, so we pass it
1002
+ // explicitly when the window/limit was hit (more distinct rows may exist).
1003
+ truncated,
1004
+ filtersApplied,
1005
+ filtersDropped: [],
1006
+ fieldsUnavailable: [],
1007
+ notes,
1008
+ });
1009
+ }