@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # Agent Protocol
2
2
 
3
3
  **Server:** congressgov-mcp-server
4
- **Version:** 0.4.2
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.2
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
- [![Version](https://img.shields.io/badge/Version-0.4.2-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/congressgov-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.30.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/congressgov-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/congressgov-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.3.14-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-0.4.3-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/congressgov-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.30.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/congressgov-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/congressgov-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.3.14-blueviolet.svg?style=flat-square)](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;AAajD,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4E1B,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: z.number().int().positive().optional().describe("Law number. Required for 'get'."),
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: input.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: input.lawNumber,
56
+ lawNumber,
55
57
  });
56
- ctx.enrich.echo(`${input.lawType === 'pub' ? 'Public' : 'Private'} Law ${input.congress}-${input.lawNumber}`);
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":"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"}
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;;;;;;;;;;;GAWG;AAEH,KAAK,SAAS,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAk3ChD,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"}
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*(strong|b)\s*>/gi, '**')
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(/&nbsp;/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.slice(0, 20)) {
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.slice(0, 5)) {
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
- /** Compact inline render of a small object. */
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
- const cleaned = stripHtml(val, { inline: true });
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
- if (parts.length > 0)
306
- return parts.join(', ');
307
- const json = JSON.stringify(obj);
308
- return json.length > 200 ? `${json.slice(0, 197)}...` : json;
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.slice(0, 20)) {
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].filter(Boolean).join(' ');
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 laws = item.laws;
1076
- if (Array.isArray(laws) && laws.length > 0) {
1077
- const cites = laws
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 recent = termsArr.slice(-5);
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.slice(0, 10)) {
1340
+ for (const l of leadership) {
1208
1341
  const type = s(l, 'type');
1209
1342
  const congress = s(l, 'congress');
1210
- lines.push(`- ${[type, congress ? `Congress ${congress}` : undefined].filter(Boolean).join(' — ')}`);
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';