@cyanheads/congressgov-mcp-server 0.4.2 → 0.4.3
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 +1 -1
- package/CLAUDE.md +1 -1
- package/README.md +1 -1
- package/dist/mcp-server/tools/definitions/enacted-laws.tool.d.ts +1 -1
- package/dist/mcp-server/tools/definitions/enacted-laws.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/enacted-laws.tool.js +7 -5
- package/dist/mcp-server/tools/definitions/enacted-laws.tool.js.map +1 -1
- package/dist/mcp-server/tools/format-helpers.d.ts +12 -0
- package/dist/mcp-server/tools/format-helpers.d.ts.map +1 -1
- package/dist/mcp-server/tools/format-helpers.js +205 -46
- package/dist/mcp-server/tools/format-helpers.js.map +1 -1
- package/dist/mcp-server/tools/tool-helpers.d.ts +21 -0
- package/dist/mcp-server/tools/tool-helpers.d.ts.map +1 -1
- package/dist/mcp-server/tools/tool-helpers.js +48 -0
- package/dist/mcp-server/tools/tool-helpers.js.map +1 -1
- package/dist/services/congress-api/congress-api-service.d.ts +6 -0
- package/dist/services/congress-api/congress-api-service.d.ts.map +1 -1
- package/dist/services/congress-api/congress-api-service.js +41 -6
- package/dist/services/congress-api/congress-api-service.js.map +1 -1
- package/package.json +1 -1
- package/server.json +3 -3
package/AGENTS.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Agent Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** congressgov-mcp-server
|
|
4
|
-
**Version:** 0.4.
|
|
4
|
+
**Version:** 0.4.3
|
|
5
5
|
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.11.5`
|
|
6
6
|
**Engines:** Bun ≥1.3.0, Node ≥24.0.0
|
|
7
7
|
**MCP SDK:** `@modelcontextprotocol/sdk` ^1.30.0
|
package/CLAUDE.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Agent Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** congressgov-mcp-server
|
|
4
|
-
**Version:** 0.4.
|
|
4
|
+
**Version:** 0.4.3
|
|
5
5
|
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.11.5`
|
|
6
6
|
**Engines:** Bun ≥1.3.0, Node ≥24.0.0
|
|
7
7
|
**MCP SDK:** `@modelcontextprotocol/sdk` ^1.30.0
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
<div align="center">
|
|
9
9
|
|
|
10
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/congressgov-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/congressgov-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
|
|
11
11
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
@@ -13,7 +13,7 @@ export declare const enactedLawsTool: import("@cyanheads/mcp-ts-core").ToolDefin
|
|
|
13
13
|
priv: "priv";
|
|
14
14
|
pub: "pub";
|
|
15
15
|
}>>;
|
|
16
|
-
lawNumber: z.ZodOptional<z.ZodNumber
|
|
16
|
+
lawNumber: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
|
|
17
17
|
limit: z.ZodDefault<z.ZodNumber>;
|
|
18
18
|
offset: z.ZodDefault<z.ZodNumber>;
|
|
19
19
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enacted-laws.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/enacted-laws.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"enacted-laws.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/enacted-laws.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAejD,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiF1B,CAAC"}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
6
6
|
import { validationError } from '@cyanheads/mcp-ts-core/errors';
|
|
7
7
|
import { formatLaws } from '../../../mcp-server/tools/format-helpers.js';
|
|
8
|
-
import { buildEffectiveQuery, congressErrorContracts, listEnrichment, listOrDetail, notifyIfNoMatches, } from '../../../mcp-server/tools/tool-helpers.js';
|
|
8
|
+
import { buildEffectiveQuery, congressErrorContracts, lawNumberIdentifier, listEnrichment, listOrDetail, notifyIfNoMatches, toLawNumber, } from '../../../mcp-server/tools/tool-helpers.js';
|
|
9
9
|
import { getCongressApi } from '../../../services/congress-api/congress-api-service.js';
|
|
10
10
|
export const enactedLawsTool = tool('congressgov_enacted_laws', {
|
|
11
11
|
description: `Browse enacted public and private laws from Congress.gov by congress and law type ('pub' for public laws, 'priv' for private). 'list' filters by enactment status and law type — the discovery path 'bill_lookup' does not offer. 'get' returns the origin bill record (sponsor, actions, summaries, text), with the public/private law citation on the bill's 'laws' array (e.g. {"number":"118-2","type":"Public Law"}).`,
|
|
@@ -18,7 +18,7 @@ export const enactedLawsTool = tool('congressgov_enacted_laws', {
|
|
|
18
18
|
.enum(['pub', 'priv'])
|
|
19
19
|
.optional()
|
|
20
20
|
.describe("Law type — 'pub' (public laws, general application, most common) or 'priv' (private laws, specific individuals or entities). Required for 'get'."),
|
|
21
|
-
lawNumber:
|
|
21
|
+
lawNumber: lawNumberIdentifier("Law number, as carried by a 'list' row's `laws[].number` — either the full citation ('118-90', rendered as 'Public Law 118-90') or the portion after the hyphen ('90'); the prefix is the congress and must match `congress`. Not the row's own `number`, which is the origin bill. Required for 'get'.").optional(),
|
|
22
22
|
limit: z.number().int().min(1).max(250).default(20).describe('Results per page (1-250).'),
|
|
23
23
|
offset: z.number().int().min(0).default(0).describe('Pagination offset.'),
|
|
24
24
|
}),
|
|
@@ -43,17 +43,19 @@ export const enactedLawsTool = tool('congressgov_enacted_laws', {
|
|
|
43
43
|
if (!input.lawType || !input.lawNumber) {
|
|
44
44
|
throw validationError("The 'get' operation requires lawType ('pub' or 'priv') and lawNumber. Use 'list' to browse laws by congress.", { lawType: input.lawType, lawNumber: input.lawNumber });
|
|
45
45
|
}
|
|
46
|
+
/** List rows carry the law number only inside the `{congress}-{number}` citation. */
|
|
47
|
+
const lawNumber = toLawNumber(input.lawNumber, input.congress);
|
|
46
48
|
const result = await api.getLaw({
|
|
47
49
|
congress: input.congress,
|
|
48
50
|
lawType: input.lawType,
|
|
49
|
-
lawNumber
|
|
51
|
+
lawNumber,
|
|
50
52
|
}, ctx);
|
|
51
53
|
ctx.log.info('Law retrieved', {
|
|
52
54
|
congress: input.congress,
|
|
53
55
|
lawType: input.lawType,
|
|
54
|
-
lawNumber
|
|
56
|
+
lawNumber,
|
|
55
57
|
});
|
|
56
|
-
ctx.enrich.echo(`${input.lawType === 'pub' ? 'Public' : 'Private'} Law ${input.congress}-${
|
|
58
|
+
ctx.enrich.echo(`${input.lawType === 'pub' ? 'Public' : 'Private'} Law ${input.congress}-${lawNumber}`);
|
|
57
59
|
ctx.enrich.total(1);
|
|
58
60
|
return result;
|
|
59
61
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enacted-laws.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/enacted-laws.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAEhE,OAAO,EAAE,UAAU,EAAE,MAAM,sCAAsC,CAAC;AAClE,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,cAAc,EACd,YAAY,EACZ,iBAAiB,
|
|
1
|
+
{"version":3,"file":"enacted-laws.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/enacted-laws.tool.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,6 +6,18 @@
|
|
|
6
6
|
* NOT reliably forwarded. These formatters render complete, structured
|
|
7
7
|
* markdown so the LLM can reason about all returned data.
|
|
8
8
|
*
|
|
9
|
+
* **Completeness invariant.** A record reaching a detail renderer is rendered
|
|
10
|
+
* whole: every upstream field appears, and no value is cut mid-string. A
|
|
11
|
+
* hand-built header may curate presentation, but it ends in `renderDetailRest`,
|
|
12
|
+
* and a curated collection row ends in `withUnnamedFields` — so a field neither
|
|
13
|
+
* one names still reaches the reader. The single deliberate exception is a
|
|
14
|
+
* `{count, url}` sub-resource reference, rendered as "N available": the URL is
|
|
15
|
+
* the upstream REST endpoint, and the tool's own operation is the callable path
|
|
16
|
+
* to those rows. Caps belong to pagination, which has a continuation path
|
|
17
|
+
* (`pagination.nextOffset`, surfaced in every list header) — a nested collection
|
|
18
|
+
* inside one record has none, so slicing it would drop data no follow-up call
|
|
19
|
+
* could recover.
|
|
20
|
+
*
|
|
9
21
|
* See: https://github.com/cyanheads/mcp-ts-core/issues/19
|
|
10
22
|
*
|
|
11
23
|
* @module mcp-server/tools/format-helpers
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format-helpers.d.ts","sourceRoot":"","sources":["../../../src/mcp-server/tools/format-helpers.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"format-helpers.d.ts","sourceRoot":"","sources":["../../../src/mcp-server/tools/format-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,KAAK,SAAS,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAohDhD,sFAAsF;AACtF,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,CAUxE;AAqCD,2DAA2D;AAC3D,eAAO,MAAM,eAAe,WAlEhB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,SAAS,EAkEkB,CAAC;AAkCpE,qDAAqD;AACrD,eAAO,MAAM,iBAAiB,WArGlB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,SAAS,EAqGuB,CAAC;AAEzE,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,CAoB1E;AAWD,iFAAiF;AACjF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,CAkB7E;AAED,kDAAkD;AAClD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,CASnF;AAED,mCAAmC;AACnC,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,CAK7E;AAED,4FAA4F;AAC5F,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,CAY9E;AAED,8FAA8F;AAC9F,eAAO,MAAM,UAAU,WAhMX,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,SAAS,EAgMiC,CAAC;AAEnF;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,CAcxE;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,CAa9E"}
|
|
@@ -6,6 +6,18 @@
|
|
|
6
6
|
* NOT reliably forwarded. These formatters render complete, structured
|
|
7
7
|
* markdown so the LLM can reason about all returned data.
|
|
8
8
|
*
|
|
9
|
+
* **Completeness invariant.** A record reaching a detail renderer is rendered
|
|
10
|
+
* whole: every upstream field appears, and no value is cut mid-string. A
|
|
11
|
+
* hand-built header may curate presentation, but it ends in `renderDetailRest`,
|
|
12
|
+
* and a curated collection row ends in `withUnnamedFields` — so a field neither
|
|
13
|
+
* one names still reaches the reader. The single deliberate exception is a
|
|
14
|
+
* `{count, url}` sub-resource reference, rendered as "N available": the URL is
|
|
15
|
+
* the upstream REST endpoint, and the tool's own operation is the callable path
|
|
16
|
+
* to those rows. Caps belong to pagination, which has a continuation path
|
|
17
|
+
* (`pagination.nextOffset`, surfaced in every list header) — a nested collection
|
|
18
|
+
* inside one record has none, so slicing it would drop data no follow-up call
|
|
19
|
+
* could recover.
|
|
20
|
+
*
|
|
9
21
|
* See: https://github.com/cyanheads/mcp-ts-core/issues/19
|
|
10
22
|
*
|
|
11
23
|
* @module mcp-server/tools/format-helpers
|
|
@@ -41,20 +53,40 @@ function stripHtml(html, { inline = false } = {}) {
|
|
|
41
53
|
.replace(/\n{3,}/g, '\n\n')
|
|
42
54
|
.trim();
|
|
43
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Wrap an emphasis span's content in Markdown markers with the span's own
|
|
58
|
+
* boundary whitespace kept *outside* them.
|
|
59
|
+
*
|
|
60
|
+
* Congress.gov summaries routinely place a space inside the tag
|
|
61
|
+
* (`<em>de minimis </em>treatment`, `an<em> de minimis</em> threshold`).
|
|
62
|
+
* Substituting the marker in place produces `*de minimis *treatment` — markers
|
|
63
|
+
* that no Markdown parser binds as emphasis, and altered visible spacing either
|
|
64
|
+
* way. A span with no non-whitespace content gets no markers at all.
|
|
65
|
+
*/
|
|
66
|
+
function wrapEmphasis(inner, marker) {
|
|
67
|
+
const core = inner.trim();
|
|
68
|
+
if (!core)
|
|
69
|
+
return inner;
|
|
70
|
+
const lead = inner.slice(0, inner.length - inner.trimStart().length);
|
|
71
|
+
const trail = inner.slice(inner.trimEnd().length);
|
|
72
|
+
return `${lead}${marker}${core}${marker}${trail}`;
|
|
73
|
+
}
|
|
44
74
|
/**
|
|
45
75
|
* Convert upstream HTML (Congress.gov bill summaries are returned with `<p>`,
|
|
46
76
|
* `<strong>`, `<em>`, anchor tags) into readable Markdown that preserves
|
|
47
77
|
* paragraph and emphasis structure.
|
|
78
|
+
*
|
|
79
|
+
* Emphasis is matched as a whole span rather than tag-by-tag so boundary
|
|
80
|
+
* whitespace can be normalized (see `wrapEmphasis`) and an unclosed tag falls
|
|
81
|
+
* through to the generic strip instead of leaving a stray marker.
|
|
48
82
|
*/
|
|
49
83
|
function htmlToMarkdown(html) {
|
|
50
84
|
return html
|
|
51
85
|
.replace(/<\s*br\s*\/?\s*>/gi, '\n')
|
|
52
86
|
.replace(/<\s*\/p\s*>/gi, '\n\n')
|
|
53
87
|
.replace(/<\s*p[^>]*>/gi, '')
|
|
54
|
-
.replace(/<\s*(strong|b)\s*>/gi, '**')
|
|
55
|
-
.replace(/<\s*\/\s*(
|
|
56
|
-
.replace(/<\s*(em|i)\s*>/gi, '*')
|
|
57
|
-
.replace(/<\s*\/\s*(em|i)\s*>/gi, '*')
|
|
88
|
+
.replace(/<\s*(?:strong|b)\s*>([\s\S]*?)<\s*\/\s*(?:strong|b)\s*>/gi, (_m, inner) => wrapEmphasis(inner, '**'))
|
|
89
|
+
.replace(/<\s*(?:em|i)\s*>([\s\S]*?)<\s*\/\s*(?:em|i)\s*>/gi, (_m, inner) => wrapEmphasis(inner, '*'))
|
|
58
90
|
.replace(/<\s*a[^>]*href=["']([^"']*)["'][^>]*>([\s\S]*?)<\s*\/\s*a\s*>/gi, '[$2]($1)')
|
|
59
91
|
.replace(/<[^>]*>/g, '')
|
|
60
92
|
.replace(/ /g, ' ')
|
|
@@ -167,7 +199,7 @@ function renderDetail(obj) {
|
|
|
167
199
|
if (val.length === 0)
|
|
168
200
|
continue;
|
|
169
201
|
lines.push(`\n**${key}** (${val.length}):`);
|
|
170
|
-
for (const item of val
|
|
202
|
+
for (const item of val) {
|
|
171
203
|
if (typeof item === 'object' && item) {
|
|
172
204
|
lines.push(`- ${renderInline(item)}`);
|
|
173
205
|
}
|
|
@@ -175,8 +207,6 @@ function renderDetail(obj) {
|
|
|
175
207
|
lines.push(`- ${String(item)}`);
|
|
176
208
|
}
|
|
177
209
|
}
|
|
178
|
-
if (val.length > 20)
|
|
179
|
-
lines.push(`- _...${val.length - 20} more_`);
|
|
180
210
|
}
|
|
181
211
|
else if (typeof val === 'object') {
|
|
182
212
|
const nested = val;
|
|
@@ -264,12 +294,10 @@ function renderGenericItem(item, index) {
|
|
|
264
294
|
}
|
|
265
295
|
else {
|
|
266
296
|
lines.push(`**${key}:** ${val.length} items`);
|
|
267
|
-
for (const sub of val
|
|
297
|
+
for (const sub of val) {
|
|
268
298
|
if (typeof sub === 'object' && sub !== null)
|
|
269
299
|
lines.push(` - ${renderInline(sub)}`);
|
|
270
300
|
}
|
|
271
|
-
if (val.length > 5)
|
|
272
|
-
lines.push(` - _...${val.length - 5} more_`);
|
|
273
301
|
}
|
|
274
302
|
}
|
|
275
303
|
}
|
|
@@ -288,26 +316,76 @@ const HEADING_FIELDS = new Set([
|
|
|
288
316
|
'description',
|
|
289
317
|
'question',
|
|
290
318
|
]);
|
|
291
|
-
/**
|
|
319
|
+
/**
|
|
320
|
+
* Compact one-line render of a small object. Compact but never lossy: values are
|
|
321
|
+
* carried in full and nested objects/arrays recurse, so no field or character is
|
|
322
|
+
* dropped at any depth.
|
|
323
|
+
*/
|
|
292
324
|
function renderInline(obj) {
|
|
325
|
+
return inlineFields(obj) || JSON.stringify(obj);
|
|
326
|
+
}
|
|
327
|
+
/** `key: value` pairs for one object level, recursing into nested values. */
|
|
328
|
+
function inlineFields(obj) {
|
|
293
329
|
const parts = [];
|
|
294
330
|
for (const [key, val] of Object.entries(obj)) {
|
|
295
331
|
if (val == null || val === '')
|
|
296
332
|
continue;
|
|
297
333
|
if (typeof val === 'string') {
|
|
298
|
-
|
|
299
|
-
const preview = cleaned.length > 120 ? `${cleaned.slice(0, 117)}...` : cleaned;
|
|
300
|
-
parts.push(`${key}: ${preview}`);
|
|
334
|
+
parts.push(`${key}: ${stripHtml(val, { inline: true })}`);
|
|
301
335
|
}
|
|
302
|
-
else if (typeof val === 'number' || typeof val === 'boolean')
|
|
336
|
+
else if (typeof val === 'number' || typeof val === 'boolean') {
|
|
303
337
|
parts.push(`${key}: ${val}`);
|
|
338
|
+
}
|
|
339
|
+
else if (Array.isArray(val)) {
|
|
340
|
+
if (val.length === 0)
|
|
341
|
+
continue;
|
|
342
|
+
const items = val.map((item) => typeof item === 'object' && item
|
|
343
|
+
? `{${inlineFields(item)}}`
|
|
344
|
+
: String(item));
|
|
345
|
+
parts.push(`${key}: [${items.join('; ')}]`);
|
|
346
|
+
}
|
|
347
|
+
else if (typeof val === 'object') {
|
|
348
|
+
const nested = inlineFields(val);
|
|
349
|
+
if (nested)
|
|
350
|
+
parts.push(`${key}: {${nested}}`);
|
|
351
|
+
}
|
|
304
352
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
353
|
+
return parts.join(', ');
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* A curated one-line render of a collection row, with any of the row's own
|
|
357
|
+
* fields the curation never named appended.
|
|
358
|
+
*
|
|
359
|
+
* The `renderDetailRest` fall-through only reaches a record's top level: a
|
|
360
|
+
* collection the header renders itself is skipped there, so a field its curated
|
|
361
|
+
* line omits would otherwise leave `content[]` while staying in
|
|
362
|
+
* `structuredContent` — the same loss for a term's `district` or a nominee's
|
|
363
|
+
* `url` that a dropped top-level field used to be.
|
|
364
|
+
*/
|
|
365
|
+
function withUnnamedFields(row, curated, named) {
|
|
366
|
+
const rest = {};
|
|
367
|
+
for (const [k, v] of Object.entries(row))
|
|
368
|
+
if (!named.has(k))
|
|
369
|
+
rest[k] = v;
|
|
370
|
+
const unnamed = inlineFields(rest);
|
|
371
|
+
return unnamed ? `${curated} — ${unnamed}` : curated;
|
|
309
372
|
}
|
|
310
373
|
// ── Domain-Specific Item Renderers ──────────────────────────────────
|
|
374
|
+
/**
|
|
375
|
+
* Public/private law citations from a bill or law record's `laws[]`, e.g.
|
|
376
|
+
* "Public Law 118-90". Upstream `/law` rows mirror `/bill`, so the citation is
|
|
377
|
+
* the only field distinguishing an enacted law from the bill it came from — it
|
|
378
|
+
* has to reach both the list rows and the detail view.
|
|
379
|
+
*/
|
|
380
|
+
function lawCitations(item) {
|
|
381
|
+
const laws = item.laws;
|
|
382
|
+
if (!Array.isArray(laws) || laws.length === 0)
|
|
383
|
+
return;
|
|
384
|
+
const cites = laws
|
|
385
|
+
.map((law) => join([s(law, 'type'), s(law, 'number')], ' '))
|
|
386
|
+
.filter(Boolean);
|
|
387
|
+
return cites.length > 0 ? cites.join(', ') : undefined;
|
|
388
|
+
}
|
|
311
389
|
function renderBillItem(item, i) {
|
|
312
390
|
const type = s(item, 'type')?.toUpperCase() ?? '';
|
|
313
391
|
const number = s(item, 'number') ?? '';
|
|
@@ -322,6 +400,9 @@ function renderBillItem(item, i) {
|
|
|
322
400
|
]);
|
|
323
401
|
if (meta)
|
|
324
402
|
lines.push(meta);
|
|
403
|
+
const citation = lawCitations(item);
|
|
404
|
+
if (citation)
|
|
405
|
+
lines.push(`**Law:** ${citation}`);
|
|
325
406
|
if (Array.isArray(item.sponsors) && item.sponsors.length > 0) {
|
|
326
407
|
const sponsors = item.sponsors.map((sp) => {
|
|
327
408
|
const name = s(sp, 'fullName') ?? s(sp, 'firstName') ?? '?';
|
|
@@ -759,7 +840,7 @@ function renderNominationDetail(item) {
|
|
|
759
840
|
const nominees = item.nominees;
|
|
760
841
|
if (Array.isArray(nominees) && nominees.length > 0) {
|
|
761
842
|
lines.push(`\n**Nominees (${nominees.length}):**`);
|
|
762
|
-
for (const n of nominees
|
|
843
|
+
for (const n of nominees) {
|
|
763
844
|
const ord = s(n, 'ordinal');
|
|
764
845
|
const count = s(n, 'nomineeCount');
|
|
765
846
|
const org = s(n, 'organization');
|
|
@@ -770,10 +851,8 @@ function renderNominationDetail(item) {
|
|
|
770
851
|
org,
|
|
771
852
|
title,
|
|
772
853
|
].filter(Boolean);
|
|
773
|
-
lines.push(`- ${parts.join(' — ')}`);
|
|
854
|
+
lines.push(`- ${withUnnamedFields(n, parts.join(' — '), NOMINEE_ROW_KEYS)}`);
|
|
774
855
|
}
|
|
775
|
-
if (nominees.length > 20)
|
|
776
|
-
lines.push(`- _...${nominees.length - 20} more_`);
|
|
777
856
|
}
|
|
778
857
|
else if (s(item, 'partNumber') === '00') {
|
|
779
858
|
/** Parent nominations (partNumber=00) have no nominees array. Sub-resources
|
|
@@ -783,8 +862,29 @@ function renderNominationDetail(item) {
|
|
|
783
862
|
const url = s(item, 'url');
|
|
784
863
|
if (url)
|
|
785
864
|
lines.push(`\n**URL:** ${url}`);
|
|
865
|
+
const rest = renderDetailRest(item, HEADER_NOMINATION_KEYS);
|
|
866
|
+
if (rest)
|
|
867
|
+
lines.push('', rest);
|
|
786
868
|
return lines.join('\n');
|
|
787
869
|
}
|
|
870
|
+
const NOMINEE_ROW_KEYS = new Set(['ordinal', 'nomineeCount', 'organization', 'positionTitle']);
|
|
871
|
+
const HEADER_NOMINATION_KEYS = new Set([
|
|
872
|
+
'citation',
|
|
873
|
+
'number',
|
|
874
|
+
'partNumber',
|
|
875
|
+
'nominationType',
|
|
876
|
+
'description',
|
|
877
|
+
'congress',
|
|
878
|
+
'receivedDate',
|
|
879
|
+
'authorityDate',
|
|
880
|
+
'updateDate',
|
|
881
|
+
'latestAction',
|
|
882
|
+
'actions',
|
|
883
|
+
'committees',
|
|
884
|
+
'hearings',
|
|
885
|
+
'nominees',
|
|
886
|
+
'url',
|
|
887
|
+
]);
|
|
788
888
|
/** Roll call vote detail — question, result, date, party totals. */
|
|
789
889
|
function renderRollVoteDetail(item) {
|
|
790
890
|
const roll = s(item, 'rollCallNumber');
|
|
@@ -826,8 +926,24 @@ function renderRollVoteDetail(item) {
|
|
|
826
926
|
}
|
|
827
927
|
if (sourceUrl)
|
|
828
928
|
lines.push(`\n**Source Data URL:** ${sourceUrl}`);
|
|
929
|
+
const rest = renderDetailRest(item, HEADER_ROLL_VOTE_KEYS);
|
|
930
|
+
if (rest)
|
|
931
|
+
lines.push('', rest);
|
|
829
932
|
return lines.join('\n');
|
|
830
933
|
}
|
|
934
|
+
const HEADER_ROLL_VOTE_KEYS = new Set([
|
|
935
|
+
'rollCallNumber',
|
|
936
|
+
'congress',
|
|
937
|
+
'sessionNumber',
|
|
938
|
+
'result',
|
|
939
|
+
'voteQuestion',
|
|
940
|
+
'voteType',
|
|
941
|
+
'startDate',
|
|
942
|
+
'updateDate',
|
|
943
|
+
'identifier',
|
|
944
|
+
'sourceDataURL',
|
|
945
|
+
'votePartyTotal',
|
|
946
|
+
]);
|
|
831
947
|
/** One member's position: "Warren Davidson (R-OH) → Nay". */
|
|
832
948
|
function renderMemberVoteRow(r) {
|
|
833
949
|
const first = s(r, 'firstName');
|
|
@@ -988,8 +1104,11 @@ function renderSenateVoteDetail(item) {
|
|
|
988
1104
|
if (amd) {
|
|
989
1105
|
const amdNum = s(amd, 'number');
|
|
990
1106
|
const to = s(amd, 'toDocumentNumber');
|
|
1107
|
+
const toTitle = s(amd, 'toDocumentShortTitle');
|
|
991
1108
|
amendmentPurpose = s(amd, 'purpose');
|
|
992
|
-
const head = [amdNum, to ? `to ${to}` : undefined]
|
|
1109
|
+
const head = [amdNum, to ? `to ${to}` : undefined, toTitle ? `— ${toTitle}` : undefined]
|
|
1110
|
+
.filter(Boolean)
|
|
1111
|
+
.join(' ');
|
|
993
1112
|
if (head)
|
|
994
1113
|
lines.push(`\n**Amendment:** ${head}`);
|
|
995
1114
|
if (amendmentPurpose)
|
|
@@ -1000,8 +1119,34 @@ function renderSenateVoteDetail(item) {
|
|
|
1000
1119
|
const docText = s(item, 'voteDocumentText');
|
|
1001
1120
|
if (docText && docText !== amendmentPurpose && docText !== docTitle)
|
|
1002
1121
|
lines.push(`\n${docText}`);
|
|
1122
|
+
const rest = renderDetailRest(item, HEADER_SENATE_VOTE_KEYS);
|
|
1123
|
+
if (rest)
|
|
1124
|
+
lines.push('', rest);
|
|
1003
1125
|
return lines.join('\n');
|
|
1004
1126
|
}
|
|
1127
|
+
/**
|
|
1128
|
+
* `chamber` is implicit in the "Senate Vote" heading; `voteResult` / `question`
|
|
1129
|
+
* are the short forms of `voteResultText` / `voteQuestionText`, one of which the
|
|
1130
|
+
* header always renders.
|
|
1131
|
+
*/
|
|
1132
|
+
const HEADER_SENATE_VOTE_KEYS = new Set([
|
|
1133
|
+
'chamber',
|
|
1134
|
+
'voteNumber',
|
|
1135
|
+
'voteResult',
|
|
1136
|
+
'voteResultText',
|
|
1137
|
+
'question',
|
|
1138
|
+
'voteQuestionText',
|
|
1139
|
+
'voteTitle',
|
|
1140
|
+
'congress',
|
|
1141
|
+
'session',
|
|
1142
|
+
'voteDate',
|
|
1143
|
+
'majorityRequirement',
|
|
1144
|
+
'count',
|
|
1145
|
+
'partyTotals',
|
|
1146
|
+
'document',
|
|
1147
|
+
'amendment',
|
|
1148
|
+
'voteDocumentText',
|
|
1149
|
+
]);
|
|
1005
1150
|
/** Senate member voting positions for one roll call — mirrors the House members view. */
|
|
1006
1151
|
function renderSenateVoteMembers(result) {
|
|
1007
1152
|
const vote = (result.vote ?? {});
|
|
@@ -1072,18 +1217,9 @@ function renderBillDetail(item) {
|
|
|
1072
1217
|
const actionText = s(item, 'latestAction', 'text');
|
|
1073
1218
|
if (actionDate || actionText)
|
|
1074
1219
|
lines.push(`**Latest Action:** ${[actionDate, actionText].filter(Boolean).join(' — ')}`);
|
|
1075
|
-
const
|
|
1076
|
-
if (
|
|
1077
|
-
|
|
1078
|
-
.map((law) => {
|
|
1079
|
-
const num = s(law, 'number');
|
|
1080
|
-
const lawType = s(law, 'type');
|
|
1081
|
-
return num && lawType ? `${lawType} ${num}` : (num ?? lawType);
|
|
1082
|
-
})
|
|
1083
|
-
.filter(Boolean);
|
|
1084
|
-
if (cites.length)
|
|
1085
|
-
lines.push(`**Law:** ${cites.join(', ')}`);
|
|
1086
|
-
}
|
|
1220
|
+
const citation = lawCitations(item);
|
|
1221
|
+
if (citation)
|
|
1222
|
+
lines.push(`**Law:** ${citation}`);
|
|
1087
1223
|
const rest = renderDetailRest(item, HEADER_BILL_KEYS);
|
|
1088
1224
|
if (rest)
|
|
1089
1225
|
lines.push('', rest);
|
|
@@ -1175,8 +1311,7 @@ function renderMemberDetail(item) {
|
|
|
1175
1311
|
: undefined;
|
|
1176
1312
|
if (termsArr && termsArr.length > 0) {
|
|
1177
1313
|
lines.push(`\n**Terms (${termsArr.length}):**`);
|
|
1178
|
-
const
|
|
1179
|
-
for (const term of recent) {
|
|
1314
|
+
for (const term of termsArr) {
|
|
1180
1315
|
const chamber = s(term, 'chamber');
|
|
1181
1316
|
const start = s(term, 'startYear');
|
|
1182
1317
|
const end = s(term, 'endYear');
|
|
@@ -1184,10 +1319,8 @@ function renderMemberDetail(item) {
|
|
|
1184
1319
|
const stateName = s(term, 'stateName');
|
|
1185
1320
|
const range = start && end ? `${start}–${end}` : start;
|
|
1186
1321
|
const parts = [chamber, range, party, stateName].filter(Boolean);
|
|
1187
|
-
lines.push(`- ${parts.join(', ')}`);
|
|
1322
|
+
lines.push(`- ${withUnnamedFields(term, parts.join(', '), TERM_ROW_KEYS)}`);
|
|
1188
1323
|
}
|
|
1189
|
-
if (termsArr.length > 5)
|
|
1190
|
-
lines.push(`- _...${termsArr.length - 5} earlier_`);
|
|
1191
1324
|
}
|
|
1192
1325
|
const partyHistory = item.partyHistory;
|
|
1193
1326
|
if (Array.isArray(partyHistory) && partyHistory.length > 0) {
|
|
@@ -1198,19 +1331,20 @@ function renderMemberDetail(item) {
|
|
|
1198
1331
|
const end = s(p, 'endYear');
|
|
1199
1332
|
const range = start && end ? `${start}–${end}` : start;
|
|
1200
1333
|
const parts = [partyName, range && `(${range})`].filter(Boolean);
|
|
1201
|
-
lines.push(`- ${parts.join(' ')}`);
|
|
1334
|
+
lines.push(`- ${withUnnamedFields(p, parts.join(' '), PARTY_HISTORY_ROW_KEYS)}`);
|
|
1202
1335
|
}
|
|
1203
1336
|
}
|
|
1204
1337
|
const leadership = item.leadership;
|
|
1205
1338
|
if (Array.isArray(leadership) && leadership.length > 0) {
|
|
1206
1339
|
lines.push(`\n**Leadership Roles (${leadership.length}):**`);
|
|
1207
|
-
for (const l of leadership
|
|
1340
|
+
for (const l of leadership) {
|
|
1208
1341
|
const type = s(l, 'type');
|
|
1209
1342
|
const congress = s(l, 'congress');
|
|
1210
|
-
|
|
1343
|
+
const curated = [type, congress ? `Congress ${congress}` : undefined]
|
|
1344
|
+
.filter(Boolean)
|
|
1345
|
+
.join(' — ');
|
|
1346
|
+
lines.push(`- ${withUnnamedFields(l, curated, LEADERSHIP_ROW_KEYS)}`);
|
|
1211
1347
|
}
|
|
1212
|
-
if (leadership.length > 10)
|
|
1213
|
-
lines.push(`- _...${leadership.length - 10} more_`);
|
|
1214
1348
|
}
|
|
1215
1349
|
const subResources = [];
|
|
1216
1350
|
for (const key of ['sponsoredLegislation', 'cosponsoredLegislation']) {
|
|
@@ -1225,8 +1359,33 @@ function renderMemberDetail(item) {
|
|
|
1225
1359
|
const url = s(item, 'url');
|
|
1226
1360
|
if (url)
|
|
1227
1361
|
lines.push(`\n**URL:** ${url}`);
|
|
1362
|
+
const rest = renderDetailRest(item, HEADER_MEMBER_KEYS);
|
|
1363
|
+
if (rest)
|
|
1364
|
+
lines.push('', rest);
|
|
1228
1365
|
return lines.join('\n');
|
|
1229
1366
|
}
|
|
1367
|
+
const TERM_ROW_KEYS = new Set(['chamber', 'startYear', 'endYear', 'partyName', 'stateName']);
|
|
1368
|
+
const PARTY_HISTORY_ROW_KEYS = new Set(['partyName', 'startYear', 'endYear']);
|
|
1369
|
+
const LEADERSHIP_ROW_KEYS = new Set(['type', 'congress']);
|
|
1370
|
+
const HEADER_MEMBER_KEYS = new Set([
|
|
1371
|
+
'bioguideId',
|
|
1372
|
+
'directOrderName',
|
|
1373
|
+
'invertedOrderName',
|
|
1374
|
+
'honorificName',
|
|
1375
|
+
'partyName',
|
|
1376
|
+
'currentParty',
|
|
1377
|
+
'state',
|
|
1378
|
+
'district',
|
|
1379
|
+
'currentMember',
|
|
1380
|
+
'birthYear',
|
|
1381
|
+
'updateDate',
|
|
1382
|
+
'terms',
|
|
1383
|
+
'partyHistory',
|
|
1384
|
+
'leadership',
|
|
1385
|
+
'sponsoredLegislation',
|
|
1386
|
+
'cosponsoredLegislation',
|
|
1387
|
+
'url',
|
|
1388
|
+
]);
|
|
1230
1389
|
/** Committee list item — name + key fields. Fuzzy-matched rows carry `approximate: true`. */
|
|
1231
1390
|
function renderCommitteeListItem(item, i) {
|
|
1232
1391
|
const name = s(item, 'name') ?? s(item, 'systemCode') ?? 'Committee';
|