@cyanheads/federal-regulations-mcp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +387 -0
- package/CLAUDE.md +387 -0
- package/Dockerfile +127 -0
- package/LICENSE +201 -0
- package/README.md +372 -0
- package/changelog/0.1.x/0.1.0.md +20 -0
- package/changelog/template.md +127 -0
- package/dist/config/server-config.d.ts +32 -0
- package/dist/config/server-config.d.ts.map +1 -0
- package/dist/config/server-config.js +64 -0
- package/dist/config/server-config.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server/resources/definitions/cfr-section.resource.d.ts +14 -0
- package/dist/mcp-server/resources/definitions/cfr-section.resource.d.ts.map +1 -0
- package/dist/mcp-server/resources/definitions/cfr-section.resource.js +77 -0
- package/dist/mcp-server/resources/definitions/cfr-section.resource.js.map +1 -0
- package/dist/mcp-server/resources/definitions/document.resource.d.ts +12 -0
- package/dist/mcp-server/resources/definitions/document.resource.d.ts.map +1 -0
- package/dist/mcp-server/resources/definitions/document.resource.js +38 -0
- package/dist/mcp-server/resources/definitions/document.resource.js.map +1 -0
- package/dist/mcp-server/resources/definitions/index.d.ts +12 -0
- package/dist/mcp-server/resources/definitions/index.d.ts.map +1 -0
- package/dist/mcp-server/resources/definitions/index.js +8 -0
- package/dist/mcp-server/resources/definitions/index.js.map +1 -0
- package/dist/mcp-server/tools/definitions/browse-cfr.tool.d.ts +73 -0
- package/dist/mcp-server/tools/definitions/browse-cfr.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/browse-cfr.tool.js +205 -0
- package/dist/mcp-server/tools/definitions/browse-cfr.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/find-comments.tool.d.ts +85 -0
- package/dist/mcp-server/tools/definitions/find-comments.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/find-comments.tool.js +271 -0
- package/dist/mcp-server/tools/definitions/find-comments.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/format-utils.d.ts +7 -0
- package/dist/mcp-server/tools/definitions/format-utils.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/format-utils.js +9 -0
- package/dist/mcp-server/tools/definitions/format-utils.js.map +1 -0
- package/dist/mcp-server/tools/definitions/get-cfr-section.tool.d.ts +50 -0
- package/dist/mcp-server/tools/definitions/get-cfr-section.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/get-cfr-section.tool.js +152 -0
- package/dist/mcp-server/tools/definitions/get-cfr-section.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/get-docket.tool.d.ts +67 -0
- package/dist/mcp-server/tools/definitions/get-docket.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/get-docket.tool.js +154 -0
- package/dist/mcp-server/tools/definitions/get-docket.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/get-document.tool.d.ts +51 -0
- package/dist/mcp-server/tools/definitions/get-document.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/get-document.tool.js +138 -0
- package/dist/mcp-server/tools/definitions/get-document.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/index.d.ts +356 -0
- package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/index.js +21 -0
- package/dist/mcp-server/tools/definitions/index.js.map +1 -0
- package/dist/mcp-server/tools/definitions/list-open-comments.tool.d.ts +47 -0
- package/dist/mcp-server/tools/definitions/list-open-comments.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/list-open-comments.tool.js +179 -0
- package/dist/mcp-server/tools/definitions/list-open-comments.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/search-rules.tool.d.ts +57 -0
- package/dist/mcp-server/tools/definitions/search-rules.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/search-rules.tool.js +185 -0
- package/dist/mcp-server/tools/definitions/search-rules.tool.js.map +1 -0
- package/dist/services/ecfr/ecfr-service.d.ts +54 -0
- package/dist/services/ecfr/ecfr-service.d.ts.map +1 -0
- package/dist/services/ecfr/ecfr-service.js +303 -0
- package/dist/services/ecfr/ecfr-service.js.map +1 -0
- package/dist/services/ecfr/types.d.ts +105 -0
- package/dist/services/ecfr/types.d.ts.map +1 -0
- package/dist/services/ecfr/types.js +7 -0
- package/dist/services/ecfr/types.js.map +1 -0
- package/dist/services/ecfr/xml.d.ts +15 -0
- package/dist/services/ecfr/xml.d.ts.map +1 -0
- package/dist/services/ecfr/xml.js +103 -0
- package/dist/services/ecfr/xml.js.map +1 -0
- package/dist/services/ecfr-mirror/ecfr-mirror.d.ts +43 -0
- package/dist/services/ecfr-mirror/ecfr-mirror.d.ts.map +1 -0
- package/dist/services/ecfr-mirror/ecfr-mirror.js +267 -0
- package/dist/services/ecfr-mirror/ecfr-mirror.js.map +1 -0
- package/dist/services/federal-register/federal-register-service.d.ts +30 -0
- package/dist/services/federal-register/federal-register-service.d.ts.map +1 -0
- package/dist/services/federal-register/federal-register-service.js +241 -0
- package/dist/services/federal-register/federal-register-service.js.map +1 -0
- package/dist/services/federal-register/types.d.ts +138 -0
- package/dist/services/federal-register/types.d.ts.map +1 -0
- package/dist/services/federal-register/types.js +8 -0
- package/dist/services/federal-register/types.js.map +1 -0
- package/dist/services/regulations-gov/regulations-gov-service.d.ts +55 -0
- package/dist/services/regulations-gov/regulations-gov-service.d.ts.map +1 -0
- package/dist/services/regulations-gov/regulations-gov-service.js +283 -0
- package/dist/services/regulations-gov/regulations-gov-service.js.map +1 -0
- package/dist/services/regulations-gov/types.d.ts +148 -0
- package/dist/services/regulations-gov/types.d.ts.map +1 -0
- package/dist/services/regulations-gov/types.js +7 -0
- package/dist/services/regulations-gov/types.js.map +1 -0
- package/dist/services/request-context.d.ts +13 -0
- package/dist/services/request-context.d.ts.map +1 -0
- package/dist/services/request-context.js +23 -0
- package/dist/services/request-context.js.map +1 -0
- package/package.json +94 -0
- package/scripts/_mirror-context.ts +101 -0
- package/scripts/ecfr-mirror-init.ts +36 -0
- package/scripts/ecfr-mirror-refresh.ts +29 -0
- package/scripts/ecfr-mirror-verify.ts +33 -0
- package/server.json +113 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-open-comments.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/list-open-comments.tool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview regulations_search_rules — the 80% entry point. Searches the
|
|
3
|
+
* Federal Register for proposed rules, final rules, notices, and presidential
|
|
4
|
+
* documents, filterable by agency, document type, date range, topic, and
|
|
5
|
+
* open-for-comment window. Keyless (Federal Register API).
|
|
6
|
+
* @module mcp-server/tools/definitions/search-rules.tool
|
|
7
|
+
*/
|
|
8
|
+
import { z } from '@cyanheads/mcp-ts-core';
|
|
9
|
+
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
10
|
+
export declare const searchRulesTool: import("@cyanheads/mcp-ts-core").ToolDefinition<z.ZodObject<{
|
|
11
|
+
query: z.ZodOptional<z.ZodString>;
|
|
12
|
+
type: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
13
|
+
PRORULE: "PRORULE";
|
|
14
|
+
RULE: "RULE";
|
|
15
|
+
NOTICE: "NOTICE";
|
|
16
|
+
PRESDOCU: "PRESDOCU";
|
|
17
|
+
}>>>;
|
|
18
|
+
agencies: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
19
|
+
published_after: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"">, z.ZodString]>>;
|
|
20
|
+
published_before: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"">, z.ZodString]>>;
|
|
21
|
+
per_page: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
22
|
+
page: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
23
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
24
|
+
results: z.ZodArray<z.ZodObject<{
|
|
25
|
+
documentNumber: z.ZodString;
|
|
26
|
+
title: z.ZodString;
|
|
27
|
+
type: z.ZodString;
|
|
28
|
+
abstract: z.ZodNullable<z.ZodString>;
|
|
29
|
+
publicationDate: z.ZodString;
|
|
30
|
+
agencies: z.ZodArray<z.ZodString>;
|
|
31
|
+
docketIds: z.ZodArray<z.ZodString>;
|
|
32
|
+
regulationIdNumbers: z.ZodArray<z.ZodString>;
|
|
33
|
+
cfrReferences: z.ZodArray<z.ZodObject<{
|
|
34
|
+
title: z.ZodNumber;
|
|
35
|
+
part: z.ZodString;
|
|
36
|
+
}, z.core.$strip>>;
|
|
37
|
+
commentsCloseOn: z.ZodNullable<z.ZodString>;
|
|
38
|
+
effectiveOn: z.ZodNullable<z.ZodString>;
|
|
39
|
+
htmlUrl: z.ZodString;
|
|
40
|
+
}, z.core.$strip>>;
|
|
41
|
+
}, z.core.$strip>, readonly [{
|
|
42
|
+
readonly reason: "no_results";
|
|
43
|
+
readonly code: JsonRpcErrorCode.NotFound;
|
|
44
|
+
readonly when: "Zero matches for the query and filters.";
|
|
45
|
+
readonly recovery: "Broaden the query, widen the date range, or drop an agency filter and retry.";
|
|
46
|
+
}, {
|
|
47
|
+
readonly reason: "upstream_unavailable";
|
|
48
|
+
readonly code: JsonRpcErrorCode.ServiceUnavailable;
|
|
49
|
+
readonly when: "Federal Register returned a 5xx, timed out, or served an HTML error page.";
|
|
50
|
+
readonly recovery: "Retry after a brief wait; the Federal Register API may be momentarily down.";
|
|
51
|
+
}], {
|
|
52
|
+
readonly totalCount: z.ZodNumber;
|
|
53
|
+
readonly truncated: z.ZodOptional<z.ZodBoolean>;
|
|
54
|
+
readonly shown: z.ZodOptional<z.ZodNumber>;
|
|
55
|
+
readonly notice: z.ZodOptional<z.ZodString>;
|
|
56
|
+
}>;
|
|
57
|
+
//# sourceMappingURL=search-rules.tool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-rules.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/search-rules.tool.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAOjE,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqM1B,CAAC"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview regulations_search_rules — the 80% entry point. Searches the
|
|
3
|
+
* Federal Register for proposed rules, final rules, notices, and presidential
|
|
4
|
+
* documents, filterable by agency, document type, date range, topic, and
|
|
5
|
+
* open-for-comment window. Keyless (Federal Register API).
|
|
6
|
+
* @module mcp-server/tools/definitions/search-rules.tool
|
|
7
|
+
*/
|
|
8
|
+
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
9
|
+
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
10
|
+
import { getFederalRegisterService } from '../../../services/federal-register/federal-register-service.js';
|
|
11
|
+
import { escapePipes } from './format-utils.js';
|
|
12
|
+
/** Above this match count the FR's 50-page navigation ceiling (5,000 records) bites. */
|
|
13
|
+
const FR_NAV_CEILING = 5000;
|
|
14
|
+
export const searchRulesTool = tool('regulations_search_rules', {
|
|
15
|
+
title: 'regulations_search_rules',
|
|
16
|
+
description: 'Search the Federal Register — the daily journal of US proposed rules, final rules, notices, and presidential documents (1994–present) — filtering by full-text query, document type, agency slug, publication date range, and whether the rule is open for comment. The primary discovery entry point: results carry the document number (open with regulations_get_document), docket IDs, RINs, and affected CFR parts that chain into the comment and codified-text tools. The Federal Register caps navigation at 50 pages and the match count at 10,000; when a result set is larger, narrow with published_after/published_before rather than paging deeper.',
|
|
17
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
18
|
+
input: z.object({
|
|
19
|
+
query: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe('Full-text search across document title and body. Omit to browse by filters alone (e.g. all EPA proposed rules in a date range).'),
|
|
23
|
+
type: z
|
|
24
|
+
.array(z.enum(['PRORULE', 'RULE', 'NOTICE', 'PRESDOCU']))
|
|
25
|
+
.optional()
|
|
26
|
+
.describe('Document types to include. PRORULE=Proposed Rule, RULE=Final Rule, NOTICE=Notice, PRESDOCU=Presidential Document. Omit for all types.'),
|
|
27
|
+
agencies: z
|
|
28
|
+
.array(z.string())
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('Filter to one or more agencies by Federal Register agency slug (e.g. "environmental-protection-agency", "securities-and-exchange-commission"). Slugs are the kebab-case agency name; if unsure, search by query and read the agency slugs off the results.'),
|
|
31
|
+
published_after: z
|
|
32
|
+
.union([
|
|
33
|
+
z.literal(''),
|
|
34
|
+
z
|
|
35
|
+
.string()
|
|
36
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
37
|
+
.describe('ISO 8601 date (YYYY-MM-DD).'),
|
|
38
|
+
])
|
|
39
|
+
.optional()
|
|
40
|
+
.describe('Earliest publication date, ISO 8601 (YYYY-MM-DD). Combine with published_before to window large result sets — the FR caps navigation at 50 pages.'),
|
|
41
|
+
published_before: z
|
|
42
|
+
.union([
|
|
43
|
+
z.literal(''),
|
|
44
|
+
z
|
|
45
|
+
.string()
|
|
46
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
47
|
+
.describe('ISO 8601 date (YYYY-MM-DD).'),
|
|
48
|
+
])
|
|
49
|
+
.optional()
|
|
50
|
+
.describe('Latest publication date, ISO 8601 (YYYY-MM-DD).'),
|
|
51
|
+
per_page: z
|
|
52
|
+
.number()
|
|
53
|
+
.int()
|
|
54
|
+
.min(1)
|
|
55
|
+
.max(100)
|
|
56
|
+
.optional()
|
|
57
|
+
.default(20)
|
|
58
|
+
.describe('Results per page (1–100, default 20).'),
|
|
59
|
+
page: z
|
|
60
|
+
.number()
|
|
61
|
+
.int()
|
|
62
|
+
.min(1)
|
|
63
|
+
.max(50)
|
|
64
|
+
.optional()
|
|
65
|
+
.default(1)
|
|
66
|
+
.describe('Page number (1–50, default 1). The FR API caps total_pages at 50 — with per_page=100 this allows navigating up to 5,000 results. To reach beyond that window, narrow with published_after/published_before rather than paging deeper.'),
|
|
67
|
+
}),
|
|
68
|
+
output: z.object({
|
|
69
|
+
results: z
|
|
70
|
+
.array(z
|
|
71
|
+
.object({
|
|
72
|
+
documentNumber: z
|
|
73
|
+
.string()
|
|
74
|
+
.describe('Federal Register document number — chains into regulations_get_document.'),
|
|
75
|
+
title: z.string().describe('Document title.'),
|
|
76
|
+
type: z
|
|
77
|
+
.string()
|
|
78
|
+
.describe('Document type: Proposed Rule, Final Rule, Notice, or Presidential Document.'),
|
|
79
|
+
abstract: z.string().nullable().describe('Abstract summary, or null when absent.'),
|
|
80
|
+
publicationDate: z.string().describe('Publication date (ISO 8601).'),
|
|
81
|
+
agencies: z.array(z.string()).describe('Issuing agency names.'),
|
|
82
|
+
docketIds: z
|
|
83
|
+
.array(z.string())
|
|
84
|
+
.describe('Docket IDs — chain into regulations_get_docket / regulations_find_comments.'),
|
|
85
|
+
regulationIdNumbers: z
|
|
86
|
+
.array(z.string())
|
|
87
|
+
.describe('Regulation Identifier Number(s) (RIN).'),
|
|
88
|
+
cfrReferences: z
|
|
89
|
+
.array(z
|
|
90
|
+
.object({
|
|
91
|
+
title: z.number().describe('CFR title number.'),
|
|
92
|
+
part: z.string().describe('CFR part.'),
|
|
93
|
+
})
|
|
94
|
+
.describe('A single affected CFR title + part.'))
|
|
95
|
+
.describe('Affected CFR parts — chain into regulations_get_cfr_section.'),
|
|
96
|
+
commentsCloseOn: z
|
|
97
|
+
.string()
|
|
98
|
+
.nullable()
|
|
99
|
+
.describe('Comment-period close date (ISO 8601); when set, still open for comment.'),
|
|
100
|
+
effectiveOn: z.string().nullable().describe('Effective date (ISO 8601), or null.'),
|
|
101
|
+
htmlUrl: z.string().describe('Federal Register web URL for the document.'),
|
|
102
|
+
})
|
|
103
|
+
.describe('One matching Federal Register document.'))
|
|
104
|
+
.describe('Matching Federal Register documents (this page).'),
|
|
105
|
+
}),
|
|
106
|
+
enrichment: {
|
|
107
|
+
totalCount: z
|
|
108
|
+
.number()
|
|
109
|
+
.describe('Total matches before pagination (FR count; capped at 10,000 by the API window).'),
|
|
110
|
+
truncated: z
|
|
111
|
+
.boolean()
|
|
112
|
+
.optional()
|
|
113
|
+
.describe('True when matches exceed the FR 50-page (5,000-record) navigation ceiling.'),
|
|
114
|
+
shown: z.number().optional().describe('Results returned on this page.'),
|
|
115
|
+
notice: z
|
|
116
|
+
.string()
|
|
117
|
+
.optional()
|
|
118
|
+
.describe('Guidance when nothing matched or when results were truncated.'),
|
|
119
|
+
},
|
|
120
|
+
errors: [
|
|
121
|
+
{
|
|
122
|
+
reason: 'no_results',
|
|
123
|
+
code: JsonRpcErrorCode.NotFound,
|
|
124
|
+
when: 'Zero matches for the query and filters.',
|
|
125
|
+
recovery: 'Broaden the query, widen the date range, or drop an agency filter and retry.',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
reason: 'upstream_unavailable',
|
|
129
|
+
code: JsonRpcErrorCode.ServiceUnavailable,
|
|
130
|
+
when: 'Federal Register returned a 5xx, timed out, or served an HTML error page.',
|
|
131
|
+
recovery: 'Retry after a brief wait; the Federal Register API may be momentarily down.',
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
async handler(input, ctx) {
|
|
135
|
+
const service = getFederalRegisterService();
|
|
136
|
+
const result = await service.search({
|
|
137
|
+
query: input.query || undefined,
|
|
138
|
+
types: input.type,
|
|
139
|
+
agencies: input.agencies?.length ? input.agencies : undefined,
|
|
140
|
+
publishedAfter: input.published_after || undefined,
|
|
141
|
+
publishedBefore: input.published_before || undefined,
|
|
142
|
+
perPage: input.per_page,
|
|
143
|
+
page: input.page,
|
|
144
|
+
}, ctx);
|
|
145
|
+
ctx.enrich.total(result.totalCount);
|
|
146
|
+
if (result.results.length === 0) {
|
|
147
|
+
ctx.enrich.notice('No Federal Register documents matched. Broaden the query, widen the date range, or drop an agency filter.');
|
|
148
|
+
return { results: [] };
|
|
149
|
+
}
|
|
150
|
+
if (result.totalCount > FR_NAV_CEILING) {
|
|
151
|
+
ctx.enrich.truncated({
|
|
152
|
+
shown: result.results.length,
|
|
153
|
+
cap: FR_NAV_CEILING,
|
|
154
|
+
guidance: `${result.totalCount} matches exceed the Federal Register's 5,000-record navigation ceiling. Narrow with published_after / published_before to reach the rest.`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return { results: result.results };
|
|
158
|
+
},
|
|
159
|
+
format: (result) => {
|
|
160
|
+
const lines = [
|
|
161
|
+
'| FR Number | Type | Title | Agency | Published | Comments Close |',
|
|
162
|
+
'|---|---|---|---|---|---|',
|
|
163
|
+
];
|
|
164
|
+
for (const r of result.results) {
|
|
165
|
+
const agency = r.agencies[0] ?? '—';
|
|
166
|
+
const cite = r.cfrReferences.map((c) => `${c.title} CFR ${c.part}`).join(', ');
|
|
167
|
+
lines.push(`| ${r.documentNumber} | ${r.type} | ${escapePipes(r.title)} | ${escapePipes(agency)} | ${r.publicationDate} | ${r.commentsCloseOn ?? '—'} |`);
|
|
168
|
+
const tail = [];
|
|
169
|
+
if (r.docketIds.length)
|
|
170
|
+
tail.push(`dockets: ${r.docketIds.join(', ')}`);
|
|
171
|
+
if (cite)
|
|
172
|
+
tail.push(`CFR: ${cite}`);
|
|
173
|
+
if (r.regulationIdNumbers.length)
|
|
174
|
+
tail.push(`RIN: ${r.regulationIdNumbers.join(', ')}`);
|
|
175
|
+
if (r.abstract)
|
|
176
|
+
tail.push(`abstract: ${r.abstract}`);
|
|
177
|
+
if (r.effectiveOn)
|
|
178
|
+
tail.push(`effective: ${r.effectiveOn}`);
|
|
179
|
+
tail.push(r.htmlUrl);
|
|
180
|
+
lines.push(`| | | ${escapePipes(tail.join(' · '))} | | | |`);
|
|
181
|
+
}
|
|
182
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
//# sourceMappingURL=search-rules.tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-rules.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/search-rules.tool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview EcfrService — keyless client for the eCFR API (ecfr.gov/api):
|
|
3
|
+
* the versioner (titles, structure, ancestry, full-text XML) and the search API.
|
|
4
|
+
* Backs `browse_cfr` and `get_cfr_section`, and supplies the title list + XML the
|
|
5
|
+
* eCFR mirror ingester walks. Section XML is parsed by `./xml.ts`. Retry wraps the
|
|
6
|
+
* full fetch + parse pipeline; HTML error pages become transient errors.
|
|
7
|
+
* @module services/ecfr/ecfr-service
|
|
8
|
+
*/
|
|
9
|
+
import type { Context } from '@cyanheads/mcp-ts-core';
|
|
10
|
+
import type { AppConfig } from '@cyanheads/mcp-ts-core/config';
|
|
11
|
+
import type { StorageService } from '@cyanheads/mcp-ts-core/storage';
|
|
12
|
+
import type { EcfrSearchResponse, EcfrSectionResult, EcfrStructureNode, EcfrTitle } from './types.js';
|
|
13
|
+
/** eCFR retains point-in-time versions back to roughly this date. */
|
|
14
|
+
export declare const ECFR_EARLIEST_DATE = "2017-01-01";
|
|
15
|
+
export declare class EcfrService {
|
|
16
|
+
private readonly baseUrl;
|
|
17
|
+
constructor(_config: AppConfig, _storage: StorageService);
|
|
18
|
+
/** List the CFR titles (`/versioner/v1/titles.json`). */
|
|
19
|
+
listTitles(ctx: Context): Promise<EcfrTitle[]>;
|
|
20
|
+
/**
|
|
21
|
+
* The latest issue date for a title — the point-in-time the versioner serves
|
|
22
|
+
* as "current". Falls back to today when the title can't be resolved.
|
|
23
|
+
*/
|
|
24
|
+
latestIssueDate(title: number, ctx: Context): Promise<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Browse a title's structure tree. With no `part`, returns the title's direct
|
|
27
|
+
* children; with a `part`, narrows to that part's subtree (flattened to one
|
|
28
|
+
* level of nodes for the agent). Each citable node carries an assembled cite.
|
|
29
|
+
*/
|
|
30
|
+
browseStructure(title: number, part: string | undefined, date: string, ctx: Context): Promise<EcfrStructureNode[]>;
|
|
31
|
+
/** List all titles as structure nodes (the top of the browse tree). */
|
|
32
|
+
listTitleNodes(ctx: Context): Promise<EcfrStructureNode[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Fetch codified text for a section (or whole part) from the versioner
|
|
35
|
+
* `/full/{date}/title-{n}.xml` endpoint, parsed into plain text.
|
|
36
|
+
*/
|
|
37
|
+
getSectionText(title: number, part: string, section: string | undefined, date: string, ctx: Context): Promise<EcfrSectionResult>;
|
|
38
|
+
/**
|
|
39
|
+
* Fetch a whole title's full XML (no part/section filter) — the bulk payload
|
|
40
|
+
* the mirror ingester walks. Long timeout: Title 40 alone is ~150 MB.
|
|
41
|
+
*/
|
|
42
|
+
fetchFullTitleXml(title: number, date: string, ctx: Context): Promise<string>;
|
|
43
|
+
/** Resolve a cite's full hierarchy path via the ancestry endpoint. */
|
|
44
|
+
hierarchyPath(title: number, part: string, section: string | undefined, date: string, ctx: Context): Promise<string>;
|
|
45
|
+
/** Full-text search the live eCFR (`/search/v1/results`). */
|
|
46
|
+
search(query: string, title: number | undefined, perPage: number, ctx: Context): Promise<EcfrSearchResponse>;
|
|
47
|
+
private fetchJson;
|
|
48
|
+
private fetchXml;
|
|
49
|
+
}
|
|
50
|
+
/** Today's date as YYYY-MM-DD (UTC). */
|
|
51
|
+
export declare function today(): string;
|
|
52
|
+
export declare function initEcfrService(config: AppConfig, storage: StorageService): void;
|
|
53
|
+
export declare function getEcfrService(): EcfrService;
|
|
54
|
+
//# sourceMappingURL=ecfr-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ecfr-service.d.ts","sourceRoot":"","sources":["../../../src/services/ecfr/ecfr-service.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAO/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAIrE,OAAO,KAAK,EAEV,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,SAAS,EAKV,MAAM,YAAY,CAAC;AASpB,qEAAqE;AACrE,eAAO,MAAM,kBAAkB,eAAe,CAAC;AAS/C,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,cAAc;IAIxD,yDAAyD;IACnD,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAmBpD;;;OAGG;IACG,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAMnE;;;;OAIG;IACG,eAAe,CACnB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAoB/B,uEAAuE;IACjE,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAYhE;;;OAGG;IACG,cAAc,CAClB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,iBAAiB,CAAC;IA2D7B;;;OAGG;IACH,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAK7E,sEAAsE;IAChE,aAAa,CACjB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,MAAM,CAAC;IAqBlB,6DAA6D;IACvD,MAAM,CACV,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,kBAAkB,CAAC;IAc9B,OAAO,CAAC,SAAS;IAiBjB,OAAO,CAAC,QAAQ;CAuBjB;AA4ED,wCAAwC;AACxC,wBAAgB,KAAK,IAAI,MAAM,CAE9B;AAMD,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAEhF;AAED,wBAAgB,cAAc,IAAI,WAAW,CAK5C"}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview EcfrService — keyless client for the eCFR API (ecfr.gov/api):
|
|
3
|
+
* the versioner (titles, structure, ancestry, full-text XML) and the search API.
|
|
4
|
+
* Backs `browse_cfr` and `get_cfr_section`, and supplies the title list + XML the
|
|
5
|
+
* eCFR mirror ingester walks. Section XML is parsed by `./xml.ts`. Retry wraps the
|
|
6
|
+
* full fetch + parse pipeline; HTML error pages become transient errors.
|
|
7
|
+
* @module services/ecfr/ecfr-service
|
|
8
|
+
*/
|
|
9
|
+
import { JsonRpcErrorCode, McpError, notFound, serviceUnavailable, } from '@cyanheads/mcp-ts-core/errors';
|
|
10
|
+
import { fetchWithTimeout, withRetry } from '@cyanheads/mcp-ts-core/utils';
|
|
11
|
+
import { getServerConfig } from '../../config/server-config.js';
|
|
12
|
+
import { toRequestContext } from '../../services/request-context.js';
|
|
13
|
+
import { parseSections } from './xml.js';
|
|
14
|
+
const TIMEOUT_MS = 20_000;
|
|
15
|
+
const XML_TIMEOUT_MS = 60_000;
|
|
16
|
+
/** Bulk whole-title XML can be ~150 MB (Title 40); allow up to 10 minutes. */
|
|
17
|
+
const FULL_TITLE_TIMEOUT_MS = 600_000;
|
|
18
|
+
const BASE_DELAY_MS = 400;
|
|
19
|
+
/** eCFR retains point-in-time versions back to roughly this date. */
|
|
20
|
+
export const ECFR_EARLIEST_DATE = '2017-01-01';
|
|
21
|
+
function looksLikeHtml(text) {
|
|
22
|
+
return /^\s*<(!DOCTYPE\s+html|html[\s>])/i.test(text);
|
|
23
|
+
}
|
|
24
|
+
/** Node types that carry a citable identifier into get_cfr_section. */
|
|
25
|
+
const CITABLE_TYPES = new Set(['part', 'section']);
|
|
26
|
+
export class EcfrService {
|
|
27
|
+
baseUrl;
|
|
28
|
+
constructor(_config, _storage) {
|
|
29
|
+
this.baseUrl = getServerConfig().ecfrBaseUrl.replace(/\/$/, '');
|
|
30
|
+
}
|
|
31
|
+
/** List the CFR titles (`/versioner/v1/titles.json`). */
|
|
32
|
+
async listTitles(ctx) {
|
|
33
|
+
const url = `${this.baseUrl}/versioner/v1/titles.json`;
|
|
34
|
+
const raw = await this.fetchJson(url, ctx, 'EcfrService.listTitles');
|
|
35
|
+
return (raw.titles ?? [])
|
|
36
|
+
.filter((t) => typeof t.number === 'number')
|
|
37
|
+
.map((t) => ({
|
|
38
|
+
number: t.number,
|
|
39
|
+
name: t.name ?? `Title ${t.number}`,
|
|
40
|
+
latestAmendedOn: t.latest_amended_on ?? null,
|
|
41
|
+
latestIssueDate: t.latest_issue_date ?? null,
|
|
42
|
+
upToDateAsOf: t.up_to_date_as_of ?? null,
|
|
43
|
+
reserved: t.reserved ?? false,
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The latest issue date for a title — the point-in-time the versioner serves
|
|
48
|
+
* as "current". Falls back to today when the title can't be resolved.
|
|
49
|
+
*/
|
|
50
|
+
async latestIssueDate(title, ctx) {
|
|
51
|
+
const titles = await this.listTitles(ctx);
|
|
52
|
+
const match = titles.find((t) => t.number === title);
|
|
53
|
+
return match?.latestIssueDate ?? match?.upToDateAsOf ?? today();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Browse a title's structure tree. With no `part`, returns the title's direct
|
|
57
|
+
* children; with a `part`, narrows to that part's subtree (flattened to one
|
|
58
|
+
* level of nodes for the agent). Each citable node carries an assembled cite.
|
|
59
|
+
*/
|
|
60
|
+
async browseStructure(title, part, date, ctx) {
|
|
61
|
+
const url = `${this.baseUrl}/versioner/v1/structure/${date}/title-${title}.json`;
|
|
62
|
+
const root = await this.fetchJson(url, ctx, 'EcfrService.browseStructure');
|
|
63
|
+
const partNode = part ? findPartNode(root, part) : root;
|
|
64
|
+
if (!partNode) {
|
|
65
|
+
throw notFound(`Part ${part} not found in CFR title ${title} as of ${date}.`, {
|
|
66
|
+
title,
|
|
67
|
+
part,
|
|
68
|
+
date,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const children = partNode.children ?? [];
|
|
72
|
+
return children.map((child) => normalizeNode(child, title));
|
|
73
|
+
}
|
|
74
|
+
/** List all titles as structure nodes (the top of the browse tree). */
|
|
75
|
+
async listTitleNodes(ctx) {
|
|
76
|
+
const titles = await this.listTitles(ctx);
|
|
77
|
+
return titles.map((t) => ({
|
|
78
|
+
type: 'title',
|
|
79
|
+
identifier: String(t.number),
|
|
80
|
+
label: `Title ${t.number}—${t.name}`,
|
|
81
|
+
description: t.name,
|
|
82
|
+
reserved: t.reserved,
|
|
83
|
+
cfrCite: null,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Fetch codified text for a section (or whole part) from the versioner
|
|
88
|
+
* `/full/{date}/title-{n}.xml` endpoint, parsed into plain text.
|
|
89
|
+
*/
|
|
90
|
+
async getSectionText(title, part, section, date, ctx) {
|
|
91
|
+
const search = new URLSearchParams({ part });
|
|
92
|
+
if (section)
|
|
93
|
+
search.set('section', section);
|
|
94
|
+
const url = `${this.baseUrl}/versioner/v1/full/${date}/title-${title}.xml?${search.toString()}`;
|
|
95
|
+
const cite = `${title} CFR ${part}${section ? ` § ${section}` : ''}`;
|
|
96
|
+
let xml;
|
|
97
|
+
try {
|
|
98
|
+
xml = await this.fetchXml(url, ctx, 'EcfrService.getSectionText');
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
// The versioner 404s for a nonexistent part/section (or a date past the
|
|
102
|
+
// title's latest issue). Translate that into an actionable not_found so the
|
|
103
|
+
// tool's contract recovery surfaces, rather than a raw "Fetch failed 404".
|
|
104
|
+
if (err instanceof McpError && err.code === JsonRpcErrorCode.NotFound) {
|
|
105
|
+
throw notFound(`No codified text found for ${cite} as of ${date}.`, {
|
|
106
|
+
title,
|
|
107
|
+
part,
|
|
108
|
+
section: section ?? null,
|
|
109
|
+
date,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
const sections = parseSections(xml);
|
|
115
|
+
if (sections.length === 0) {
|
|
116
|
+
throw notFound(`No codified text found for ${cite} as of ${date}.`, {
|
|
117
|
+
title,
|
|
118
|
+
part,
|
|
119
|
+
section: section ?? null,
|
|
120
|
+
date,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (section) {
|
|
124
|
+
// sections is non-empty (guarded above), so [0] is defined when find misses.
|
|
125
|
+
const found = sections.find((s) => s.section === section);
|
|
126
|
+
const match = found ?? sections[0];
|
|
127
|
+
return {
|
|
128
|
+
title,
|
|
129
|
+
part,
|
|
130
|
+
section,
|
|
131
|
+
heading: match?.heading ?? `§ ${section}`,
|
|
132
|
+
date,
|
|
133
|
+
bodyText: match?.bodyText ?? '',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
title,
|
|
138
|
+
part,
|
|
139
|
+
section: null,
|
|
140
|
+
heading: `Part ${part}`,
|
|
141
|
+
date,
|
|
142
|
+
bodyText: sections.map((s) => `${s.heading}\n${s.bodyText}`).join('\n\n'),
|
|
143
|
+
sections,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Fetch a whole title's full XML (no part/section filter) — the bulk payload
|
|
148
|
+
* the mirror ingester walks. Long timeout: Title 40 alone is ~150 MB.
|
|
149
|
+
*/
|
|
150
|
+
fetchFullTitleXml(title, date, ctx) {
|
|
151
|
+
const url = `${this.baseUrl}/versioner/v1/full/${date}/title-${title}.xml`;
|
|
152
|
+
return this.fetchXml(url, ctx, 'EcfrService.fetchFullTitleXml', FULL_TITLE_TIMEOUT_MS);
|
|
153
|
+
}
|
|
154
|
+
/** Resolve a cite's full hierarchy path via the ancestry endpoint. */
|
|
155
|
+
async hierarchyPath(title, part, section, date, ctx) {
|
|
156
|
+
const search = new URLSearchParams({ part });
|
|
157
|
+
if (section)
|
|
158
|
+
search.set('section', section);
|
|
159
|
+
const url = `${this.baseUrl}/versioner/v1/ancestry/${date}/title-${title}.json?${search.toString()}`;
|
|
160
|
+
try {
|
|
161
|
+
const raw = await this.fetchJson(url, ctx, 'EcfrService.hierarchyPath');
|
|
162
|
+
const ancestors = raw.ancestors ?? [];
|
|
163
|
+
const parts = ancestors
|
|
164
|
+
.map((a) => labelForAncestor(a))
|
|
165
|
+
.filter((s) => s !== null);
|
|
166
|
+
return parts.length > 0 ? parts.join(' › ') : `Title ${title}`;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// Hierarchy is a nicety; never fail the whole read because ancestry 404s.
|
|
170
|
+
return `Title ${title}`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** Full-text search the live eCFR (`/search/v1/results`). */
|
|
174
|
+
async search(query, title, perPage, ctx) {
|
|
175
|
+
const search = new URLSearchParams({
|
|
176
|
+
query,
|
|
177
|
+
per_page: String(perPage),
|
|
178
|
+
});
|
|
179
|
+
if (typeof title === 'number')
|
|
180
|
+
search.set('conditions[title]', String(title));
|
|
181
|
+
const url = `${this.baseUrl}/search/v1/results?${search.toString()}`;
|
|
182
|
+
const raw = await this.fetchJson(url, ctx, 'EcfrService.search');
|
|
183
|
+
return {
|
|
184
|
+
totalCount: raw.meta?.total_count ?? (raw.results ?? []).length,
|
|
185
|
+
results: (raw.results ?? []).map((r) => normalizeSearchHit(r)),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
fetchJson(url, ctx, operation) {
|
|
189
|
+
const reqCtx = toRequestContext(ctx, operation);
|
|
190
|
+
return withRetry(async () => {
|
|
191
|
+
const response = await fetchWithTimeout(url, TIMEOUT_MS, reqCtx, { signal: ctx.signal });
|
|
192
|
+
const text = await response.text();
|
|
193
|
+
if (looksLikeHtml(text)) {
|
|
194
|
+
throw serviceUnavailable('eCFR returned an HTML error page instead of JSON — likely momentarily unavailable.');
|
|
195
|
+
}
|
|
196
|
+
return JSON.parse(text);
|
|
197
|
+
}, { operation, context: reqCtx, baseDelayMs: BASE_DELAY_MS, signal: ctx.signal });
|
|
198
|
+
}
|
|
199
|
+
fetchXml(url, ctx, operation, timeoutMs = XML_TIMEOUT_MS) {
|
|
200
|
+
const reqCtx = toRequestContext(ctx, operation);
|
|
201
|
+
return withRetry(async () => {
|
|
202
|
+
const response = await fetchWithTimeout(url, timeoutMs, reqCtx, {
|
|
203
|
+
signal: ctx.signal,
|
|
204
|
+
});
|
|
205
|
+
const text = await response.text();
|
|
206
|
+
if (looksLikeHtml(text)) {
|
|
207
|
+
throw serviceUnavailable('eCFR returned an HTML error page instead of XML — likely momentarily unavailable.');
|
|
208
|
+
}
|
|
209
|
+
return text;
|
|
210
|
+
}, { operation, context: reqCtx, baseDelayMs: BASE_DELAY_MS, signal: ctx.signal });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** Build a `${title} CFR ${identifier}` cite for citable node types. */
|
|
214
|
+
function buildCite(node, title) {
|
|
215
|
+
if (!node.type || !CITABLE_TYPES.has(node.type) || !node.identifier)
|
|
216
|
+
return null;
|
|
217
|
+
return `${title} CFR ${node.identifier}`;
|
|
218
|
+
}
|
|
219
|
+
function normalizeNode(node, title) {
|
|
220
|
+
return {
|
|
221
|
+
type: node.type ?? 'unknown',
|
|
222
|
+
identifier: node.identifier ?? '',
|
|
223
|
+
label: node.label ?? node.identifier ?? '',
|
|
224
|
+
description: node.label_description ?? null,
|
|
225
|
+
reserved: node.reserved ?? false,
|
|
226
|
+
cfrCite: buildCite(node, title),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/** Depth-first search for a part node by identifier within a structure tree. */
|
|
230
|
+
function findPartNode(root, part) {
|
|
231
|
+
if (root.type === 'part' && root.identifier === part)
|
|
232
|
+
return root;
|
|
233
|
+
for (const child of root.children ?? []) {
|
|
234
|
+
const found = findPartNode(child, part);
|
|
235
|
+
if (found)
|
|
236
|
+
return found;
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
function labelForAncestor(node) {
|
|
241
|
+
if (!node.type || !node.identifier)
|
|
242
|
+
return null;
|
|
243
|
+
const typeLabel = node.type.charAt(0).toUpperCase() + node.type.slice(1);
|
|
244
|
+
if (node.type === 'section')
|
|
245
|
+
return `§ ${node.identifier}`;
|
|
246
|
+
return `${typeLabel} ${node.identifier}`;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Strip HTML tags and collapse whitespace. The eCFR search API wraps matched
|
|
250
|
+
* terms in `<strong>` in both `full_text_excerpt` and the `headings.*` values;
|
|
251
|
+
* those tags must not leak into the agent-facing excerpt or heading.
|
|
252
|
+
*/
|
|
253
|
+
function stripSearchHtml(text) {
|
|
254
|
+
return text
|
|
255
|
+
.replace(/<[^>]+>/g, '')
|
|
256
|
+
.replace(/\s+/g, ' ')
|
|
257
|
+
.trim();
|
|
258
|
+
}
|
|
259
|
+
function normalizeSearchHit(r) {
|
|
260
|
+
const h = r.hierarchy ?? {};
|
|
261
|
+
const headings = r.hierarchy_headings ?? {};
|
|
262
|
+
const title = typeof h.title === 'number' ? h.title : Number.parseInt(String(h.title ?? '0'), 10);
|
|
263
|
+
const part = h.part ?? '';
|
|
264
|
+
const section = h.section ?? null;
|
|
265
|
+
const pathSegments = [];
|
|
266
|
+
if (h.title)
|
|
267
|
+
pathSegments.push(`Title ${h.title}`);
|
|
268
|
+
if (headings.chapter)
|
|
269
|
+
pathSegments.push(stripSearchHtml(headings.chapter));
|
|
270
|
+
if (headings.subchapter)
|
|
271
|
+
pathSegments.push(stripSearchHtml(headings.subchapter));
|
|
272
|
+
if (headings.part)
|
|
273
|
+
pathSegments.push(stripSearchHtml(headings.part));
|
|
274
|
+
if (section)
|
|
275
|
+
pathSegments.push(`§ ${section}`);
|
|
276
|
+
const rawHeading = headings.section ?? headings.part ?? r.headings?.section ?? '(untitled)';
|
|
277
|
+
const cfrCite = section ? `${title} CFR ${section}` : `${title} CFR ${part}`;
|
|
278
|
+
return {
|
|
279
|
+
title: Number.isNaN(title) ? 0 : title,
|
|
280
|
+
part,
|
|
281
|
+
section,
|
|
282
|
+
heading: stripSearchHtml(rawHeading),
|
|
283
|
+
hierarchyPath: pathSegments.join(' › '),
|
|
284
|
+
excerpt: stripSearchHtml(r.full_text_excerpt ?? ''),
|
|
285
|
+
cfrCite,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/** Today's date as YYYY-MM-DD (UTC). */
|
|
289
|
+
export function today() {
|
|
290
|
+
return new Date().toISOString().slice(0, 10);
|
|
291
|
+
}
|
|
292
|
+
// --- Init/accessor pattern ---
|
|
293
|
+
let _service;
|
|
294
|
+
export function initEcfrService(config, storage) {
|
|
295
|
+
_service = new EcfrService(config, storage);
|
|
296
|
+
}
|
|
297
|
+
export function getEcfrService() {
|
|
298
|
+
if (!_service) {
|
|
299
|
+
throw new Error('EcfrService not initialized — call initEcfrService() in setup()');
|
|
300
|
+
}
|
|
301
|
+
return _service;
|
|
302
|
+
}
|
|
303
|
+
//# sourceMappingURL=ecfr-service.js.map
|