@cyanheads/congressgov-mcp-server 0.4.2 → 0.5.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 +19 -5
- package/CLAUDE.md +19 -5
- package/README.md +3 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/prompts/definitions/legislative-research.prompt.d.ts +9 -0
- package/dist/mcp-server/prompts/definitions/legislative-research.prompt.d.ts.map +1 -1
- package/dist/mcp-server/prompts/definitions/legislative-research.prompt.js +46 -15
- package/dist/mcp-server/prompts/definitions/legislative-research.prompt.js.map +1 -1
- package/dist/mcp-server/tools/definitions/bill-lookup.tool.d.ts +38 -0
- package/dist/mcp-server/tools/definitions/bill-lookup.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/bill-lookup.tool.js +72 -4
- package/dist/mcp-server/tools/definitions/bill-lookup.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/committee-reports.tool.d.ts +37 -0
- package/dist/mcp-server/tools/definitions/committee-reports.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/committee-reports.tool.js +66 -7
- package/dist/mcp-server/tools/definitions/committee-reports.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/daily-record.tool.d.ts +41 -3
- package/dist/mcp-server/tools/definitions/daily-record.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/daily-record.tool.js +61 -8
- package/dist/mcp-server/tools/definitions/daily-record.tool.js.map +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 +16 -0
- package/dist/mcp-server/tools/format-helpers.d.ts.map +1 -1
- package/dist/mcp-server/tools/format-helpers.js +597 -130
- package/dist/mcp-server/tools/format-helpers.js.map +1 -1
- package/dist/mcp-server/tools/tool-helpers.d.ts +88 -0
- package/dist/mcp-server/tools/tool-helpers.d.ts.map +1 -1
- package/dist/mcp-server/tools/tool-helpers.js +133 -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/dist/services/congress-documents/congress-documents-service.d.ts +71 -0
- package/dist/services/congress-documents/congress-documents-service.d.ts.map +1 -0
- package/dist/services/congress-documents/congress-documents-service.js +271 -0
- package/dist/services/congress-documents/congress-documents-service.js.map +1 -0
- package/dist/services/congress-documents/document-formats.d.ts +29 -0
- package/dist/services/congress-documents/document-formats.d.ts.map +1 -0
- package/dist/services/congress-documents/document-formats.js +51 -0
- package/dist/services/congress-documents/document-formats.js.map +1 -0
- package/dist/services/congress-documents/extract-text.d.ts +21 -0
- package/dist/services/congress-documents/extract-text.d.ts.map +1 -0
- package/dist/services/congress-documents/extract-text.js +81 -0
- package/dist/services/congress-documents/extract-text.js.map +1 -0
- package/dist/services/congress-documents/types.d.ts +45 -0
- package/dist/services/congress-documents/types.d.ts.map +1 -0
- package/dist/services/congress-documents/types.js +6 -0
- package/dist/services/congress-documents/types.js.map +1 -0
- package/package.json +1 -1
- package/server.json +3 -3
|
@@ -6,6 +6,22 @@
|
|
|
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 any renderer — detail view or
|
|
10
|
+
* list row — is rendered whole: every upstream field appears, and no value is
|
|
11
|
+
* cut mid-string. A hand-built header may curate presentation, but a detail
|
|
12
|
+
* header ends in `renderDetailRest`, a list row ends in `withRowRest`, and a
|
|
13
|
+
* curated single-line row ends in `withUnnamedFields` — so a field none of them
|
|
14
|
+
* names still reaches the reader. A collection a curated line renders lossily
|
|
15
|
+
* (its latest entry only, or only some of each entry's fields) is deliberately
|
|
16
|
+
* left out of the row's named set, so the fall-through carries it whole. The
|
|
17
|
+
* single deliberate exception is a
|
|
18
|
+
* `{count, url}` sub-resource reference, rendered as "N available": the URL is
|
|
19
|
+
* the upstream REST endpoint, and the tool's own operation is the callable path
|
|
20
|
+
* to those rows. Caps belong to pagination, which has a continuation path
|
|
21
|
+
* (`pagination.nextOffset`, surfaced in every list header) — a nested collection
|
|
22
|
+
* inside one record has none, so slicing it would drop data no follow-up call
|
|
23
|
+
* could recover.
|
|
24
|
+
*
|
|
9
25
|
* See: https://github.com/cyanheads/mcp-ts-core/issues/19
|
|
10
26
|
*
|
|
11
27
|
* @module mcp-server/tools/format-helpers
|
|
@@ -41,20 +57,40 @@ function stripHtml(html, { inline = false } = {}) {
|
|
|
41
57
|
.replace(/\n{3,}/g, '\n\n')
|
|
42
58
|
.trim();
|
|
43
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Wrap an emphasis span's content in Markdown markers with the span's own
|
|
62
|
+
* boundary whitespace kept *outside* them.
|
|
63
|
+
*
|
|
64
|
+
* Congress.gov summaries routinely place a space inside the tag
|
|
65
|
+
* (`<em>de minimis </em>treatment`, `an<em> de minimis</em> threshold`).
|
|
66
|
+
* Substituting the marker in place produces `*de minimis *treatment` — markers
|
|
67
|
+
* that no Markdown parser binds as emphasis, and altered visible spacing either
|
|
68
|
+
* way. A span with no non-whitespace content gets no markers at all.
|
|
69
|
+
*/
|
|
70
|
+
function wrapEmphasis(inner, marker) {
|
|
71
|
+
const core = inner.trim();
|
|
72
|
+
if (!core)
|
|
73
|
+
return inner;
|
|
74
|
+
const lead = inner.slice(0, inner.length - inner.trimStart().length);
|
|
75
|
+
const trail = inner.slice(inner.trimEnd().length);
|
|
76
|
+
return `${lead}${marker}${core}${marker}${trail}`;
|
|
77
|
+
}
|
|
44
78
|
/**
|
|
45
79
|
* Convert upstream HTML (Congress.gov bill summaries are returned with `<p>`,
|
|
46
80
|
* `<strong>`, `<em>`, anchor tags) into readable Markdown that preserves
|
|
47
81
|
* paragraph and emphasis structure.
|
|
82
|
+
*
|
|
83
|
+
* Emphasis is matched as a whole span rather than tag-by-tag so boundary
|
|
84
|
+
* whitespace can be normalized (see `wrapEmphasis`) and an unclosed tag falls
|
|
85
|
+
* through to the generic strip instead of leaving a stray marker.
|
|
48
86
|
*/
|
|
49
87
|
function htmlToMarkdown(html) {
|
|
50
88
|
return html
|
|
51
89
|
.replace(/<\s*br\s*\/?\s*>/gi, '\n')
|
|
52
90
|
.replace(/<\s*\/p\s*>/gi, '\n\n')
|
|
53
91
|
.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, '*')
|
|
92
|
+
.replace(/<\s*(?:strong|b)\s*>([\s\S]*?)<\s*\/\s*(?:strong|b)\s*>/gi, (_m, inner) => wrapEmphasis(inner, '**'))
|
|
93
|
+
.replace(/<\s*(?:em|i)\s*>([\s\S]*?)<\s*\/\s*(?:em|i)\s*>/gi, (_m, inner) => wrapEmphasis(inner, '*'))
|
|
58
94
|
.replace(/<\s*a[^>]*href=["']([^"']*)["'][^>]*>([\s\S]*?)<\s*\/\s*a\s*>/gi, '[$2]($1)')
|
|
59
95
|
.replace(/<[^>]*>/g, '')
|
|
60
96
|
.replace(/ /g, ' ')
|
|
@@ -167,7 +203,7 @@ function renderDetail(obj) {
|
|
|
167
203
|
if (val.length === 0)
|
|
168
204
|
continue;
|
|
169
205
|
lines.push(`\n**${key}** (${val.length}):`);
|
|
170
|
-
for (const item of val
|
|
206
|
+
for (const item of val) {
|
|
171
207
|
if (typeof item === 'object' && item) {
|
|
172
208
|
lines.push(`- ${renderInline(item)}`);
|
|
173
209
|
}
|
|
@@ -175,8 +211,6 @@ function renderDetail(obj) {
|
|
|
175
211
|
lines.push(`- ${String(item)}`);
|
|
176
212
|
}
|
|
177
213
|
}
|
|
178
|
-
if (val.length > 20)
|
|
179
|
-
lines.push(`- _...${val.length - 20} more_`);
|
|
180
214
|
}
|
|
181
215
|
else if (typeof val === 'object') {
|
|
182
216
|
const nested = val;
|
|
@@ -188,11 +222,10 @@ function renderDetail(obj) {
|
|
|
188
222
|
lines.push(`**${key}:** ${count} available`);
|
|
189
223
|
continue;
|
|
190
224
|
}
|
|
191
|
-
// latestAction: { actionDate, text }
|
|
192
225
|
if (key === 'latestAction') {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
226
|
+
const line = latestActionLine(record);
|
|
227
|
+
if (line)
|
|
228
|
+
lines.push(line);
|
|
196
229
|
continue;
|
|
197
230
|
}
|
|
198
231
|
// Small objects inline, larger ones nested
|
|
@@ -218,22 +251,28 @@ function renderDetail(obj) {
|
|
|
218
251
|
}
|
|
219
252
|
/** Render any list item with all its fields. */
|
|
220
253
|
function renderGenericItem(item, index) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
254
|
+
/**
|
|
255
|
+
* Only the keys the heading actually consumed are skipped below. A fixed
|
|
256
|
+
* skip-list swallows the alternates: a committee-report row carries both a
|
|
257
|
+
* `citation` and a `type`/`number` pair, and the heading built from the pair
|
|
258
|
+
* would drop `H. Rept. 113-118` — the label Congress.gov itself publishes.
|
|
259
|
+
*/
|
|
260
|
+
const named = new Set();
|
|
261
|
+
let id;
|
|
262
|
+
if (item.type && item.number != null) {
|
|
263
|
+
named.add('type').add('number');
|
|
264
|
+
id = `${String(item.type).toUpperCase()} ${item.number}`;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
id = pick(item, named, 'citation', 'bioguideId', 'systemCode');
|
|
268
|
+
}
|
|
269
|
+
const name = pick(item, named, 'title', 'name', 'fullName', 'directOrderName', 'description', 'question');
|
|
231
270
|
const heading = [id, name].filter(Boolean).join(': ') || 'Item';
|
|
232
271
|
const lines = [`### ${index + 1}. ${heading}`];
|
|
233
272
|
for (const [key, val] of Object.entries(item)) {
|
|
234
273
|
if (val == null || val === '')
|
|
235
274
|
continue;
|
|
236
|
-
if (
|
|
275
|
+
if (named.has(key))
|
|
237
276
|
continue;
|
|
238
277
|
if (typeof val === 'string') {
|
|
239
278
|
const inline = stripHtml(val, { inline: true });
|
|
@@ -249,10 +288,9 @@ function renderGenericItem(item, index) {
|
|
|
249
288
|
lines.push(`**${key}:** ${val}`);
|
|
250
289
|
}
|
|
251
290
|
else if (typeof val === 'object' && !Array.isArray(val) && val !== null) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
lines.push(`**Latest Action:** ${[date, text].filter(Boolean).join(' — ')}`);
|
|
291
|
+
const action = key === 'latestAction' ? latestActionLine(item) : undefined;
|
|
292
|
+
if (action) {
|
|
293
|
+
lines.push(action);
|
|
256
294
|
}
|
|
257
295
|
else {
|
|
258
296
|
lines.push(`**${key}:** ${renderInline(val)}`);
|
|
@@ -264,50 +302,133 @@ function renderGenericItem(item, index) {
|
|
|
264
302
|
}
|
|
265
303
|
else {
|
|
266
304
|
lines.push(`**${key}:** ${val.length} items`);
|
|
267
|
-
for (const sub of val
|
|
305
|
+
for (const sub of val) {
|
|
268
306
|
if (typeof sub === 'object' && sub !== null)
|
|
269
307
|
lines.push(` - ${renderInline(sub)}`);
|
|
270
308
|
}
|
|
271
|
-
if (val.length > 5)
|
|
272
|
-
lines.push(` - _...${val.length - 5} more_`);
|
|
273
309
|
}
|
|
274
310
|
}
|
|
275
311
|
}
|
|
276
312
|
return lines.join('\n');
|
|
277
313
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
314
|
+
/** Fields of a `latestAction` object the curated line names — `actionTime` is not one. */
|
|
315
|
+
const LATEST_ACTION_KEYS = new Set(['actionDate', 'text']);
|
|
316
|
+
/**
|
|
317
|
+
* The `**Latest Action:** date — text` line, followed by any sub-field that
|
|
318
|
+
* curation does not name. Every renderer that shows a `latestAction` object
|
|
319
|
+
* shows it the same lossy way, so the fall-through lives here rather than in
|
|
320
|
+
* each caller's key set — upstream carries `actionTime` on bill rows reached
|
|
321
|
+
* through `member_lookup sponsored`, and it reached no `content[]` client.
|
|
322
|
+
* Returns undefined when the row carries no latest action at all.
|
|
323
|
+
*/
|
|
324
|
+
function latestActionLine(item) {
|
|
325
|
+
const action = item.latestAction;
|
|
326
|
+
if (!action || typeof action !== 'object' || Array.isArray(action))
|
|
327
|
+
return;
|
|
328
|
+
const record = action;
|
|
329
|
+
const curated = [s(record, 'actionDate'), s(record, 'text')].filter(Boolean).join(' — ');
|
|
330
|
+
const line = withUnnamedFields(record, curated, LATEST_ACTION_KEYS);
|
|
331
|
+
return line ? `**Latest Action:** ${line.replace(/^ — /, '')}` : undefined;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Compact one-line render of a small object. Compact but never lossy: values are
|
|
335
|
+
* carried in full and nested objects/arrays recurse, so no field or character is
|
|
336
|
+
* dropped at any depth.
|
|
337
|
+
*/
|
|
292
338
|
function renderInline(obj) {
|
|
339
|
+
return inlineFields(obj) || JSON.stringify(obj);
|
|
340
|
+
}
|
|
341
|
+
/** `key: value` pairs for one object level, recursing into nested values. */
|
|
342
|
+
function inlineFields(obj) {
|
|
293
343
|
const parts = [];
|
|
294
344
|
for (const [key, val] of Object.entries(obj)) {
|
|
295
345
|
if (val == null || val === '')
|
|
296
346
|
continue;
|
|
297
347
|
if (typeof val === 'string') {
|
|
298
|
-
|
|
299
|
-
const preview = cleaned.length > 120 ? `${cleaned.slice(0, 117)}...` : cleaned;
|
|
300
|
-
parts.push(`${key}: ${preview}`);
|
|
348
|
+
parts.push(`${key}: ${stripHtml(val, { inline: true })}`);
|
|
301
349
|
}
|
|
302
|
-
else if (typeof val === 'number' || typeof val === 'boolean')
|
|
350
|
+
else if (typeof val === 'number' || typeof val === 'boolean') {
|
|
303
351
|
parts.push(`${key}: ${val}`);
|
|
352
|
+
}
|
|
353
|
+
else if (Array.isArray(val)) {
|
|
354
|
+
if (val.length === 0)
|
|
355
|
+
continue;
|
|
356
|
+
const items = val.map((item) => typeof item === 'object' && item
|
|
357
|
+
? `{${inlineFields(item)}}`
|
|
358
|
+
: String(item));
|
|
359
|
+
parts.push(`${key}: [${items.join('; ')}]`);
|
|
360
|
+
}
|
|
361
|
+
else if (typeof val === 'object') {
|
|
362
|
+
const nested = inlineFields(val);
|
|
363
|
+
if (nested)
|
|
364
|
+
parts.push(`${key}: {${nested}}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return parts.join(', ');
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* A curated one-line render of a collection row, with any of the row's own
|
|
371
|
+
* fields the curation never named appended.
|
|
372
|
+
*
|
|
373
|
+
* The `renderDetailRest` fall-through only reaches a record's top level: a
|
|
374
|
+
* collection the header renders itself is skipped there, so a field its curated
|
|
375
|
+
* line omits would otherwise leave `content[]` while staying in
|
|
376
|
+
* `structuredContent` — the same loss for a term's `district` or a nominee's
|
|
377
|
+
* `url` that a dropped top-level field used to be.
|
|
378
|
+
*/
|
|
379
|
+
function withUnnamedFields(row, curated, named) {
|
|
380
|
+
const rest = {};
|
|
381
|
+
for (const [k, v] of Object.entries(row))
|
|
382
|
+
if (!named.has(k))
|
|
383
|
+
rest[k] = v;
|
|
384
|
+
const unnamed = inlineFields(rest);
|
|
385
|
+
return unnamed ? `${curated} — ${unnamed}` : curated;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* A curated multi-line list row followed by every field of the row the curation
|
|
389
|
+
* never named — the list-row counterpart to a detail header's `renderDetailRest`
|
|
390
|
+
* tail. Without it a hand-built row renders a fixed field set, and anything
|
|
391
|
+
* upstream adds (or the curation never reached, like a subcommittee's `parent`)
|
|
392
|
+
* stays in `structuredContent` and never reaches a `content[]`-only client.
|
|
393
|
+
*/
|
|
394
|
+
function withRowRest(item, lines, named) {
|
|
395
|
+
const rest = renderDetailRest(item, named);
|
|
396
|
+
return rest ? [...lines, '', rest].join('\n') : lines.join('\n');
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* First present value among `keys`, marking only the key actually consumed as
|
|
400
|
+
* named. An alias chain (`updateDate ?? publishDate`) otherwise swallows the
|
|
401
|
+
* alternates: they are distinct upstream facts, so the ones the header did not
|
|
402
|
+
* render have to stay available to the fall-through. Renderers that call this
|
|
403
|
+
* hold their key constant as an array and copy it into a fresh set per row —
|
|
404
|
+
* `pick` mutates the set it is given.
|
|
405
|
+
*/
|
|
406
|
+
function pick(item, named, ...keys) {
|
|
407
|
+
for (const key of keys) {
|
|
408
|
+
const val = s(item, key);
|
|
409
|
+
if (val != null && val !== '') {
|
|
410
|
+
named.add(key);
|
|
411
|
+
return val;
|
|
412
|
+
}
|
|
304
413
|
}
|
|
305
|
-
|
|
306
|
-
return parts.join(', ');
|
|
307
|
-
const json = JSON.stringify(obj);
|
|
308
|
-
return json.length > 200 ? `${json.slice(0, 197)}...` : json;
|
|
414
|
+
return;
|
|
309
415
|
}
|
|
310
416
|
// ── Domain-Specific Item Renderers ──────────────────────────────────
|
|
417
|
+
/**
|
|
418
|
+
* Public/private law citations from a bill or law record's `laws[]`, e.g.
|
|
419
|
+
* "Public Law 118-90". Upstream `/law` rows mirror `/bill`, so the citation is
|
|
420
|
+
* the only field distinguishing an enacted law from the bill it came from — it
|
|
421
|
+
* has to reach both the list rows and the detail view.
|
|
422
|
+
*/
|
|
423
|
+
function lawCitations(item) {
|
|
424
|
+
const laws = item.laws;
|
|
425
|
+
if (!Array.isArray(laws) || laws.length === 0)
|
|
426
|
+
return;
|
|
427
|
+
const cites = laws
|
|
428
|
+
.map((law) => join([s(law, 'type'), s(law, 'number')], ' '))
|
|
429
|
+
.filter(Boolean);
|
|
430
|
+
return cites.length > 0 ? cites.join(', ') : undefined;
|
|
431
|
+
}
|
|
311
432
|
function renderBillItem(item, i) {
|
|
312
433
|
const type = s(item, 'type')?.toUpperCase() ?? '';
|
|
313
434
|
const number = s(item, 'number') ?? '';
|
|
@@ -322,6 +443,9 @@ function renderBillItem(item, i) {
|
|
|
322
443
|
]);
|
|
323
444
|
if (meta)
|
|
324
445
|
lines.push(meta);
|
|
446
|
+
const citation = lawCitations(item);
|
|
447
|
+
if (citation)
|
|
448
|
+
lines.push(`**Law:** ${citation}`);
|
|
325
449
|
if (Array.isArray(item.sponsors) && item.sponsors.length > 0) {
|
|
326
450
|
const sponsors = item.sponsors.map((sp) => {
|
|
327
451
|
const name = s(sp, 'fullName') ?? s(sp, 'firstName') ?? '?';
|
|
@@ -331,17 +455,30 @@ function renderBillItem(item, i) {
|
|
|
331
455
|
});
|
|
332
456
|
lines.push(`**Sponsor:** ${sponsors.join(', ')}`);
|
|
333
457
|
}
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
lines.push(`**Latest Action:** ${[actionDate, actionText].filter(Boolean).join(' — ')}`);
|
|
458
|
+
const latestAction = latestActionLine(item);
|
|
459
|
+
if (latestAction)
|
|
460
|
+
lines.push(latestAction);
|
|
338
461
|
const updated = s(item, 'updateDate');
|
|
339
462
|
if (updated)
|
|
340
463
|
lines.push(`**Updated:** ${updated}`);
|
|
341
464
|
if (url)
|
|
342
465
|
lines.push(`**URL:** ${url}`);
|
|
343
|
-
return lines
|
|
466
|
+
return withRowRest(item, lines, BILL_ROW_KEYS);
|
|
344
467
|
}
|
|
468
|
+
/** `sponsors` is absent: the curated line drops each sponsor's `bioguideId`. */
|
|
469
|
+
const BILL_ROW_KEYS = new Set([
|
|
470
|
+
'type',
|
|
471
|
+
'number',
|
|
472
|
+
'title',
|
|
473
|
+
'congress',
|
|
474
|
+
'originChamber',
|
|
475
|
+
'originChamberCode',
|
|
476
|
+
'policyArea',
|
|
477
|
+
'laws',
|
|
478
|
+
'latestAction',
|
|
479
|
+
'updateDate',
|
|
480
|
+
'url',
|
|
481
|
+
]);
|
|
345
482
|
function renderMemberItem(item, i) {
|
|
346
483
|
const name = s(item, 'name') ?? s(item, 'directOrderName') ?? s(item, 'fullName') ?? 'Unknown Member';
|
|
347
484
|
const url = s(item, 'url');
|
|
@@ -374,15 +511,28 @@ function renderMemberItem(item, i) {
|
|
|
374
511
|
}
|
|
375
512
|
if (url)
|
|
376
513
|
lines.push(`**URL:** ${url}`);
|
|
377
|
-
return lines
|
|
514
|
+
return withRowRest(item, lines, MEMBER_ROW_KEYS);
|
|
378
515
|
}
|
|
516
|
+
/** `terms` is absent: the curated line renders the latest term only. */
|
|
517
|
+
const MEMBER_ROW_KEYS = new Set([
|
|
518
|
+
'bioguideId',
|
|
519
|
+
'name',
|
|
520
|
+
'directOrderName',
|
|
521
|
+
'fullName',
|
|
522
|
+
'partyName',
|
|
523
|
+
'party',
|
|
524
|
+
'state',
|
|
525
|
+
'district',
|
|
526
|
+
'url',
|
|
527
|
+
]);
|
|
379
528
|
function renderSummaryItem(item, i) {
|
|
529
|
+
const named = new Set(SUMMARY_ROW_KEYS);
|
|
380
530
|
const billType = s(item, 'bill', 'type')?.toUpperCase() ?? '';
|
|
381
531
|
const billNum = s(item, 'bill', 'number') ?? '';
|
|
382
532
|
const congress = s(item, 'bill', 'congress') ?? '';
|
|
383
|
-
const version =
|
|
533
|
+
const version = pick(item, named, 'actionDesc', 'versionCode') ?? '';
|
|
384
534
|
const actionDate = s(item, 'actionDate') ?? '';
|
|
385
|
-
const summaryUpdate =
|
|
535
|
+
const summaryUpdate = pick(item, named, 'lastSummaryUpdateDate', 'updateDate') ?? '';
|
|
386
536
|
const rawText = typeof item.text === 'string' ? item.text : '';
|
|
387
537
|
const text = rawText ? htmlToMarkdown(rawText) : '';
|
|
388
538
|
const url = s(item, 'url') ?? s(item, 'bill', 'url');
|
|
@@ -397,19 +547,28 @@ function renderSummaryItem(item, i) {
|
|
|
397
547
|
if (meta)
|
|
398
548
|
lines.push(meta);
|
|
399
549
|
const billTitle = s(item, 'bill', 'title');
|
|
400
|
-
|
|
550
|
+
const billLine = `**Bill Title:** ${billTitle ?? 'Not available'}`;
|
|
551
|
+
const bill = item.bill;
|
|
552
|
+
lines.push(bill && typeof bill === 'object' && !Array.isArray(bill)
|
|
553
|
+
? withUnnamedFields(bill, billLine, BILL_REF_KEYS)
|
|
554
|
+
: billLine);
|
|
401
555
|
// The summary text is the critical data — the whole point of this tool
|
|
402
556
|
lines.push('');
|
|
403
557
|
lines.push(text || '_Summary text not available._');
|
|
404
558
|
if (url)
|
|
405
559
|
lines.push(`\n**URL:** ${url}`);
|
|
406
|
-
return lines
|
|
560
|
+
return withRowRest(item, lines, named);
|
|
407
561
|
}
|
|
562
|
+
/** `actionDesc`/`versionCode` and the two update dates are resolved by `pick`. */
|
|
563
|
+
const SUMMARY_ROW_KEYS = ['actionDate', 'text', 'url', 'bill'];
|
|
564
|
+
/** Fields of a summary row's nested bill reference the heading already renders. */
|
|
565
|
+
const BILL_REF_KEYS = new Set(['congress', 'type', 'number', 'title', 'url']);
|
|
408
566
|
function renderCrsReportItem(item, i) {
|
|
567
|
+
const named = new Set(CRS_REPORT_ROW_KEYS);
|
|
409
568
|
const reportNumber = s(item, 'reportNumber') ?? s(item, 'number') ?? s(item, 'id') ?? 'Report number not available';
|
|
410
569
|
const title = s(item, 'title') ?? 'Title not available';
|
|
411
|
-
const updated =
|
|
412
|
-
const summary =
|
|
570
|
+
const updated = pick(item, named, 'updateDate', 'publishDate', 'date') ?? '';
|
|
571
|
+
const summary = pick(item, named, 'summary', 'abstract') ?? '';
|
|
413
572
|
const contentType = s(item, 'contentType');
|
|
414
573
|
const status = s(item, 'status');
|
|
415
574
|
const version = s(item, 'version');
|
|
@@ -427,8 +586,23 @@ function renderCrsReportItem(item, i) {
|
|
|
427
586
|
lines.push(summary);
|
|
428
587
|
if (url)
|
|
429
588
|
lines.push(`**URL:** ${url}`);
|
|
430
|
-
return lines
|
|
589
|
+
return withRowRest(item, lines, named);
|
|
431
590
|
}
|
|
591
|
+
/**
|
|
592
|
+
* The three identifier aliases are all named: upstream sends one of them, and
|
|
593
|
+
* the heading renders whichever arrived. The date and summary aliases are
|
|
594
|
+
* distinct facts, so `pick` names only the one the header consumed.
|
|
595
|
+
*/
|
|
596
|
+
const CRS_REPORT_ROW_KEYS = [
|
|
597
|
+
'reportNumber',
|
|
598
|
+
'number',
|
|
599
|
+
'id',
|
|
600
|
+
'title',
|
|
601
|
+
'contentType',
|
|
602
|
+
'status',
|
|
603
|
+
'version',
|
|
604
|
+
'url',
|
|
605
|
+
];
|
|
432
606
|
/** Daily Congressional Record articles — flattened from section-wrapped shape. */
|
|
433
607
|
function renderDailyArticleItem(item, i) {
|
|
434
608
|
const title = s(item, 'title') ?? 'Untitled article';
|
|
@@ -449,8 +623,9 @@ function renderDailyArticleItem(item, i) {
|
|
|
449
623
|
lines.push(`**${type}:** ${url}`);
|
|
450
624
|
}
|
|
451
625
|
}
|
|
452
|
-
return lines
|
|
626
|
+
return withRowRest(item, lines, DAILY_ARTICLE_ROW_KEYS);
|
|
453
627
|
}
|
|
628
|
+
const DAILY_ARTICLE_ROW_KEYS = new Set(['title', 'sectionName', 'startPage', 'endPage', 'text']);
|
|
454
629
|
/** Daily Congressional Record volumes and issues. */
|
|
455
630
|
function renderDailyRecordItem(item, i) {
|
|
456
631
|
const volume = s(item, 'volumeNumber');
|
|
@@ -473,8 +648,17 @@ function renderDailyRecordItem(item, i) {
|
|
|
473
648
|
lines.push(meta);
|
|
474
649
|
if (url)
|
|
475
650
|
lines.push(`**URL:** ${url}`);
|
|
476
|
-
return lines
|
|
651
|
+
return withRowRest(item, lines, DAILY_RECORD_ROW_KEYS);
|
|
477
652
|
}
|
|
653
|
+
/** `issueDate` is absent: the heading shows the date, the fall-through the time. */
|
|
654
|
+
const DAILY_RECORD_ROW_KEYS = new Set([
|
|
655
|
+
'volumeNumber',
|
|
656
|
+
'issueNumber',
|
|
657
|
+
'congress',
|
|
658
|
+
'sessionNumber',
|
|
659
|
+
'updateDate',
|
|
660
|
+
'url',
|
|
661
|
+
]);
|
|
478
662
|
/** House roll call votes. */
|
|
479
663
|
function renderRollVoteItem(item, i) {
|
|
480
664
|
const roll = s(item, 'rollCallNumber');
|
|
@@ -511,8 +695,23 @@ function renderRollVoteItem(item, i) {
|
|
|
511
695
|
lines.push(`**URL:** ${url}`);
|
|
512
696
|
if (sourceUrl)
|
|
513
697
|
lines.push(`**Source Data URL:** ${sourceUrl}`);
|
|
514
|
-
return lines
|
|
698
|
+
return withRowRest(item, lines, ROLL_VOTE_ROW_KEYS);
|
|
515
699
|
}
|
|
700
|
+
const ROLL_VOTE_ROW_KEYS = new Set([
|
|
701
|
+
'rollCallNumber',
|
|
702
|
+
'identifier',
|
|
703
|
+
'legislationType',
|
|
704
|
+
'legislationNumber',
|
|
705
|
+
'legislationUrl',
|
|
706
|
+
'result',
|
|
707
|
+
'voteType',
|
|
708
|
+
'startDate',
|
|
709
|
+
'congress',
|
|
710
|
+
'sessionNumber',
|
|
711
|
+
'updateDate',
|
|
712
|
+
'sourceDataURL',
|
|
713
|
+
'url',
|
|
714
|
+
]);
|
|
516
715
|
/** Bill legislative actions. */
|
|
517
716
|
function renderBillActionItem(item, i) {
|
|
518
717
|
const actionDate = s(item, 'actionDate');
|
|
@@ -531,8 +730,10 @@ function renderBillActionItem(item, i) {
|
|
|
531
730
|
if (names.length > 0)
|
|
532
731
|
lines.push(`**Committees:** ${names.join(', ')}`);
|
|
533
732
|
}
|
|
534
|
-
return lines
|
|
733
|
+
return withRowRest(item, lines, BILL_ACTION_ROW_KEYS);
|
|
535
734
|
}
|
|
735
|
+
/** `committees` is absent: the name list drops each committee's `systemCode`. */
|
|
736
|
+
const BILL_ACTION_ROW_KEYS = new Set(['actionDate', 'text', 'type', 'actionCode', 'sourceSystem']);
|
|
536
737
|
/** Committee report text — items wrap a `formats` array of {type, url, isErrata}. */
|
|
537
738
|
function renderCommitteeReportTextItem(item, i) {
|
|
538
739
|
const formats = item.formats;
|
|
@@ -554,8 +755,9 @@ function renderCommitteeReportTextItem(item, i) {
|
|
|
554
755
|
const label = e.isErrata ? `${e.type} (Errata)` : e.type;
|
|
555
756
|
lines.push(`**${label}:** ${e.url}`);
|
|
556
757
|
}
|
|
557
|
-
return lines
|
|
758
|
+
return withRowRest(item, lines, COMMITTEE_REPORT_TEXT_ROW_KEYS);
|
|
558
759
|
}
|
|
760
|
+
const COMMITTEE_REPORT_TEXT_ROW_KEYS = new Set(['formats']);
|
|
559
761
|
/** Member-sponsored amendments — `type`/`title` are null upstream; identify by `amendmentNumber`. */
|
|
560
762
|
function renderAmendmentItem(item, i) {
|
|
561
763
|
const number = s(item, 'amendmentNumber');
|
|
@@ -576,14 +778,20 @@ function renderAmendmentItem(item, i) {
|
|
|
576
778
|
]);
|
|
577
779
|
if (meta)
|
|
578
780
|
lines.push(meta);
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
lines.push(`**Latest Action:** ${[actionDate, actionText].filter(Boolean).join(' — ')}`);
|
|
781
|
+
const latestAction = latestActionLine(item);
|
|
782
|
+
if (latestAction)
|
|
783
|
+
lines.push(latestAction);
|
|
583
784
|
if (url)
|
|
584
785
|
lines.push(`**URL:** ${url}`);
|
|
585
|
-
return lines
|
|
786
|
+
return withRowRest(item, lines, AMENDMENT_ROW_KEYS);
|
|
586
787
|
}
|
|
788
|
+
const AMENDMENT_ROW_KEYS = new Set([
|
|
789
|
+
'amendmentNumber',
|
|
790
|
+
'congress',
|
|
791
|
+
'introducedDate',
|
|
792
|
+
'latestAction',
|
|
793
|
+
'url',
|
|
794
|
+
]);
|
|
587
795
|
/** Bill text versions — heading from `type` (e.g. "Enrolled Bill"), formats[] as labeled URLs. */
|
|
588
796
|
function renderBillTextItem(item, i) {
|
|
589
797
|
const type = s(item, 'type') ?? 'Bill Text';
|
|
@@ -600,8 +808,9 @@ function renderBillTextItem(item, i) {
|
|
|
600
808
|
lines.push(`**${fType}:** ${fUrl}`);
|
|
601
809
|
}
|
|
602
810
|
}
|
|
603
|
-
return lines
|
|
811
|
+
return withRowRest(item, lines, BILL_TEXT_ROW_KEYS);
|
|
604
812
|
}
|
|
813
|
+
const BILL_TEXT_ROW_KEYS = new Set(['type', 'date', 'formats']);
|
|
605
814
|
/** Nomination type wrapper: `{isCivilian: true}` / `{isMilitary: true}` → readable label. */
|
|
606
815
|
function nominationTypeLabel(raw) {
|
|
607
816
|
if (!raw || typeof raw !== 'object')
|
|
@@ -641,15 +850,27 @@ function renderNominationListItem(item, i) {
|
|
|
641
850
|
]);
|
|
642
851
|
if (meta)
|
|
643
852
|
lines.push(meta);
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
lines.push(`**Latest Action:** ${[actionDate, actionText].filter(Boolean).join(' — ')}`);
|
|
853
|
+
const latestAction = latestActionLine(item);
|
|
854
|
+
if (latestAction)
|
|
855
|
+
lines.push(latestAction);
|
|
648
856
|
const url = s(item, 'url');
|
|
649
857
|
if (url)
|
|
650
858
|
lines.push(`**URL:** ${url}`);
|
|
651
|
-
return lines
|
|
859
|
+
return withRowRest(item, lines, NOMINATION_ROW_KEYS);
|
|
652
860
|
}
|
|
861
|
+
const NOMINATION_ROW_KEYS = new Set([
|
|
862
|
+
'citation',
|
|
863
|
+
'number',
|
|
864
|
+
'partNumber',
|
|
865
|
+
'nominationType',
|
|
866
|
+
'description',
|
|
867
|
+
'congress',
|
|
868
|
+
'receivedDate',
|
|
869
|
+
'authorityDate',
|
|
870
|
+
'updateDate',
|
|
871
|
+
'latestAction',
|
|
872
|
+
'url',
|
|
873
|
+
]);
|
|
653
874
|
/** Nomination committee items — shape {name, systemCode, chamber, type, activities[], url}. */
|
|
654
875
|
function renderNominationCommitteeItem(item, i) {
|
|
655
876
|
const name = s(item, 'name') ?? s(item, 'systemCode') ?? 'Committee';
|
|
@@ -673,8 +894,10 @@ function renderNominationCommitteeItem(item, i) {
|
|
|
673
894
|
const url = s(item, 'url');
|
|
674
895
|
if (url)
|
|
675
896
|
lines.push(`**URL:** ${url}`);
|
|
676
|
-
return lines
|
|
897
|
+
return withRowRest(item, lines, NOMINATION_COMMITTEE_ROW_KEYS);
|
|
677
898
|
}
|
|
899
|
+
/** `activities` is absent: the curated lines cut each activity date to 10 chars. */
|
|
900
|
+
const NOMINATION_COMMITTEE_ROW_KEYS = new Set(['name', 'systemCode', 'chamber', 'type', 'url']);
|
|
678
901
|
/** Individual nominee items — shape {firstName, middleName, lastName, ordinal, state, prefix?, suffix?}. */
|
|
679
902
|
function renderNomineeItem(item, i) {
|
|
680
903
|
const prefix = s(item, 'prefix');
|
|
@@ -688,8 +911,17 @@ function renderNomineeItem(item, i) {
|
|
|
688
911
|
const meta = join([f('Ordinal', s(item, 'ordinal')), f('State', s(item, 'state'))]);
|
|
689
912
|
if (meta)
|
|
690
913
|
lines.push(meta);
|
|
691
|
-
return lines
|
|
914
|
+
return withRowRest(item, lines, NOMINEE_ITEM_KEYS);
|
|
692
915
|
}
|
|
916
|
+
const NOMINEE_ITEM_KEYS = new Set([
|
|
917
|
+
'prefix',
|
|
918
|
+
'firstName',
|
|
919
|
+
'middleName',
|
|
920
|
+
'lastName',
|
|
921
|
+
'suffix',
|
|
922
|
+
'ordinal',
|
|
923
|
+
'state',
|
|
924
|
+
]);
|
|
693
925
|
/** Nomination hearing items — shape {chamber, citation, date, jacketNumber, number, partNumber, errata?}. */
|
|
694
926
|
function renderNominationHearingItem(item, i) {
|
|
695
927
|
const citation = s(item, 'citation');
|
|
@@ -699,7 +931,7 @@ function renderNominationHearingItem(item, i) {
|
|
|
699
931
|
const partNumber = s(item, 'partNumber');
|
|
700
932
|
const meta = join([
|
|
701
933
|
f('Chamber', s(item, 'chamber')),
|
|
702
|
-
f('Date', s(item, 'date')
|
|
934
|
+
f('Date', s(item, 'date')),
|
|
703
935
|
f('Number', number),
|
|
704
936
|
partNumber && partNumber !== '1' && partNumber !== '01' ? f('Part', partNumber) : undefined,
|
|
705
937
|
f('Jacket', s(item, 'jacketNumber')),
|
|
@@ -707,8 +939,17 @@ function renderNominationHearingItem(item, i) {
|
|
|
707
939
|
]);
|
|
708
940
|
if (meta)
|
|
709
941
|
lines.push(meta);
|
|
710
|
-
return lines
|
|
942
|
+
return withRowRest(item, lines, NOMINATION_HEARING_ROW_KEYS);
|
|
711
943
|
}
|
|
944
|
+
const NOMINATION_HEARING_ROW_KEYS = new Set([
|
|
945
|
+
'citation',
|
|
946
|
+
'number',
|
|
947
|
+
'chamber',
|
|
948
|
+
'date',
|
|
949
|
+
'partNumber',
|
|
950
|
+
'jacketNumber',
|
|
951
|
+
'errata',
|
|
952
|
+
]);
|
|
712
953
|
/** Dispatch nomination list rows to the right renderer by shape signal. */
|
|
713
954
|
function pickNominationListRenderer(first) {
|
|
714
955
|
/** Action rows share the bill-action shape (actionDate, text, type, actionCode);
|
|
@@ -744,10 +985,9 @@ function renderNominationDetail(item) {
|
|
|
744
985
|
]);
|
|
745
986
|
if (meta)
|
|
746
987
|
lines.push(meta);
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
lines.push(`**Latest Action:** ${[actionDate, actionText].filter(Boolean).join(' — ')}`);
|
|
988
|
+
const latestAction = latestActionLine(item);
|
|
989
|
+
if (latestAction)
|
|
990
|
+
lines.push(latestAction);
|
|
751
991
|
const subResources = [];
|
|
752
992
|
for (const key of ['actions', 'committees', 'hearings']) {
|
|
753
993
|
const sub = item[key];
|
|
@@ -759,7 +999,7 @@ function renderNominationDetail(item) {
|
|
|
759
999
|
const nominees = item.nominees;
|
|
760
1000
|
if (Array.isArray(nominees) && nominees.length > 0) {
|
|
761
1001
|
lines.push(`\n**Nominees (${nominees.length}):**`);
|
|
762
|
-
for (const n of nominees
|
|
1002
|
+
for (const n of nominees) {
|
|
763
1003
|
const ord = s(n, 'ordinal');
|
|
764
1004
|
const count = s(n, 'nomineeCount');
|
|
765
1005
|
const org = s(n, 'organization');
|
|
@@ -770,10 +1010,8 @@ function renderNominationDetail(item) {
|
|
|
770
1010
|
org,
|
|
771
1011
|
title,
|
|
772
1012
|
].filter(Boolean);
|
|
773
|
-
lines.push(`- ${parts.join(' — ')}`);
|
|
1013
|
+
lines.push(`- ${withUnnamedFields(n, parts.join(' — '), NOMINEE_ROW_KEYS)}`);
|
|
774
1014
|
}
|
|
775
|
-
if (nominees.length > 20)
|
|
776
|
-
lines.push(`- _...${nominees.length - 20} more_`);
|
|
777
1015
|
}
|
|
778
1016
|
else if (s(item, 'partNumber') === '00') {
|
|
779
1017
|
/** Parent nominations (partNumber=00) have no nominees array. Sub-resources
|
|
@@ -783,8 +1021,29 @@ function renderNominationDetail(item) {
|
|
|
783
1021
|
const url = s(item, 'url');
|
|
784
1022
|
if (url)
|
|
785
1023
|
lines.push(`\n**URL:** ${url}`);
|
|
1024
|
+
const rest = renderDetailRest(item, HEADER_NOMINATION_KEYS);
|
|
1025
|
+
if (rest)
|
|
1026
|
+
lines.push('', rest);
|
|
786
1027
|
return lines.join('\n');
|
|
787
1028
|
}
|
|
1029
|
+
const NOMINEE_ROW_KEYS = new Set(['ordinal', 'nomineeCount', 'organization', 'positionTitle']);
|
|
1030
|
+
const HEADER_NOMINATION_KEYS = new Set([
|
|
1031
|
+
'citation',
|
|
1032
|
+
'number',
|
|
1033
|
+
'partNumber',
|
|
1034
|
+
'nominationType',
|
|
1035
|
+
'description',
|
|
1036
|
+
'congress',
|
|
1037
|
+
'receivedDate',
|
|
1038
|
+
'authorityDate',
|
|
1039
|
+
'updateDate',
|
|
1040
|
+
'latestAction',
|
|
1041
|
+
'actions',
|
|
1042
|
+
'committees',
|
|
1043
|
+
'hearings',
|
|
1044
|
+
'nominees',
|
|
1045
|
+
'url',
|
|
1046
|
+
]);
|
|
788
1047
|
/** Roll call vote detail — question, result, date, party totals. */
|
|
789
1048
|
function renderRollVoteDetail(item) {
|
|
790
1049
|
const roll = s(item, 'rollCallNumber');
|
|
@@ -826,13 +1085,30 @@ function renderRollVoteDetail(item) {
|
|
|
826
1085
|
}
|
|
827
1086
|
if (sourceUrl)
|
|
828
1087
|
lines.push(`\n**Source Data URL:** ${sourceUrl}`);
|
|
1088
|
+
const rest = renderDetailRest(item, HEADER_ROLL_VOTE_KEYS);
|
|
1089
|
+
if (rest)
|
|
1090
|
+
lines.push('', rest);
|
|
829
1091
|
return lines.join('\n');
|
|
830
1092
|
}
|
|
1093
|
+
const HEADER_ROLL_VOTE_KEYS = new Set([
|
|
1094
|
+
'rollCallNumber',
|
|
1095
|
+
'congress',
|
|
1096
|
+
'sessionNumber',
|
|
1097
|
+
'result',
|
|
1098
|
+
'voteQuestion',
|
|
1099
|
+
'voteType',
|
|
1100
|
+
'startDate',
|
|
1101
|
+
'updateDate',
|
|
1102
|
+
'identifier',
|
|
1103
|
+
'sourceDataURL',
|
|
1104
|
+
'votePartyTotal',
|
|
1105
|
+
]);
|
|
831
1106
|
/** One member's position: "Warren Davidson (R-OH) → Nay". */
|
|
832
1107
|
function renderMemberVoteRow(r) {
|
|
1108
|
+
const named = new Set(MEMBER_VOTE_ROW_KEYS);
|
|
833
1109
|
const first = s(r, 'firstName');
|
|
834
1110
|
const last = s(r, 'lastName');
|
|
835
|
-
const name = first && last ? `${first} ${last}` : (last ?? first ??
|
|
1111
|
+
const name = (first && last ? `${first} ${last}` : (last ?? first)) ?? pick(r, named, 'bioguideId') ?? '?';
|
|
836
1112
|
const party = s(r, 'voteParty');
|
|
837
1113
|
const state = s(r, 'voteState');
|
|
838
1114
|
const cast = s(r, 'voteCast');
|
|
@@ -843,8 +1119,11 @@ function renderMemberVoteRow(r) {
|
|
|
843
1119
|
: state
|
|
844
1120
|
? `(${state})`
|
|
845
1121
|
: undefined;
|
|
846
|
-
|
|
1122
|
+
const curated = `- ${[name, loc, cast ? `→ ${cast}` : undefined].filter(Boolean).join(' ')}`;
|
|
1123
|
+
return withUnnamedFields(r, curated, named);
|
|
847
1124
|
}
|
|
1125
|
+
/** `bioguideId` is named only when it stood in for a missing name. */
|
|
1126
|
+
const MEMBER_VOTE_ROW_KEYS = ['firstName', 'lastName', 'voteParty', 'voteState', 'voteCast'];
|
|
848
1127
|
/**
|
|
849
1128
|
* Member voting positions for one roll call — vote-context header, the
|
|
850
1129
|
* `pagination`-derived range footer, then the paginated roster from `data[]`.
|
|
@@ -870,6 +1149,9 @@ function renderVoteMembers(result) {
|
|
|
870
1149
|
const legNum = s(vote, 'legislationNumber');
|
|
871
1150
|
if (legType && legNum)
|
|
872
1151
|
lines.push(`**Legislation:** ${legType} ${legNum}`);
|
|
1152
|
+
const voteRest = renderDetailRest(vote, VOTE_CONTEXT_KEYS);
|
|
1153
|
+
if (voteRest)
|
|
1154
|
+
lines.push('', voteRest);
|
|
873
1155
|
if (rows.length === 0) {
|
|
874
1156
|
lines.push('', total > 0
|
|
875
1157
|
? `_Page is empty — offset is past the end of ${total} member position${total !== 1 ? 's' : ''}._`
|
|
@@ -887,6 +1169,16 @@ function renderVoteMembers(result) {
|
|
|
887
1169
|
}
|
|
888
1170
|
return lines.join('\n');
|
|
889
1171
|
}
|
|
1172
|
+
/** Fields of the `vote` context sibling the members header already renders. */
|
|
1173
|
+
const VOTE_CONTEXT_KEYS = new Set([
|
|
1174
|
+
'rollCallNumber',
|
|
1175
|
+
'congress',
|
|
1176
|
+
'sessionNumber',
|
|
1177
|
+
'voteQuestion',
|
|
1178
|
+
'result',
|
|
1179
|
+
'legislationType',
|
|
1180
|
+
'legislationNumber',
|
|
1181
|
+
]);
|
|
890
1182
|
// ── Senate roll call votes (LIS feed) ───────────────────────────────
|
|
891
1183
|
/**
|
|
892
1184
|
* Senate vote payloads come from the LIS XML feed, not the Congress.gov JSON API,
|
|
@@ -916,17 +1208,34 @@ function renderSenateVoteItem(item, i) {
|
|
|
916
1208
|
const title = s(item, 'title');
|
|
917
1209
|
if (title)
|
|
918
1210
|
lines.push(title);
|
|
919
|
-
return lines
|
|
1211
|
+
return withRowRest(item, lines, SENATE_VOTE_ROW_KEYS);
|
|
920
1212
|
}
|
|
1213
|
+
/** `chamber` is the dispatch marker, implicit in the Senate heading. */
|
|
1214
|
+
const SENATE_VOTE_ROW_KEYS = new Set([
|
|
1215
|
+
'chamber',
|
|
1216
|
+
'voteNumber',
|
|
1217
|
+
'issue',
|
|
1218
|
+
'result',
|
|
1219
|
+
'question',
|
|
1220
|
+
'measure',
|
|
1221
|
+
'voteDate',
|
|
1222
|
+
'yeas',
|
|
1223
|
+
'nays',
|
|
1224
|
+
'title',
|
|
1225
|
+
]);
|
|
921
1226
|
/** One Senate member's position — uses the feed's pre-formatted "Baldwin (D-WI)" label. */
|
|
922
1227
|
function renderSenateMemberRow(r) {
|
|
923
1228
|
const cast = s(r, 'voteCast');
|
|
924
1229
|
const full = s(r, 'memberFull');
|
|
925
|
-
if (full)
|
|
926
|
-
|
|
1230
|
+
if (full) {
|
|
1231
|
+
/** The feed composes memberFull from the last name, party, and state. */
|
|
1232
|
+
const named = new Set([...SENATE_MEMBER_ROW_KEYS, 'memberFull', 'lastName', 'party', 'state']);
|
|
1233
|
+
return withUnnamedFields(r, `- ${full}${cast ? ` → ${cast}` : ''}`, named);
|
|
1234
|
+
}
|
|
1235
|
+
const named = new Set([...SENATE_MEMBER_ROW_KEYS, 'firstName', 'lastName', 'party', 'state']);
|
|
927
1236
|
const first = s(r, 'firstName');
|
|
928
1237
|
const last = s(r, 'lastName');
|
|
929
|
-
const name = first && last ? `${first} ${last}` : (last ?? first ??
|
|
1238
|
+
const name = (first && last ? `${first} ${last}` : (last ?? first)) ?? pick(r, named, 'lisMemberId') ?? '?';
|
|
930
1239
|
const party = s(r, 'party');
|
|
931
1240
|
const state = s(r, 'state');
|
|
932
1241
|
const loc = party && state
|
|
@@ -936,8 +1245,10 @@ function renderSenateMemberRow(r) {
|
|
|
936
1245
|
: state
|
|
937
1246
|
? `(${state})`
|
|
938
1247
|
: undefined;
|
|
939
|
-
|
|
1248
|
+
const curated = `- ${[name, loc, cast ? `→ ${cast}` : undefined].filter(Boolean).join(' ')}`;
|
|
1249
|
+
return withUnnamedFields(r, curated, named);
|
|
940
1250
|
}
|
|
1251
|
+
const SENATE_MEMBER_ROW_KEYS = ['chamber', 'voteCast'];
|
|
941
1252
|
/** Senate vote detail — question, result, tally, derived party totals, document/amendment. */
|
|
942
1253
|
function renderSenateVoteDetail(item) {
|
|
943
1254
|
const num = s(item, 'voteNumber');
|
|
@@ -988,8 +1299,11 @@ function renderSenateVoteDetail(item) {
|
|
|
988
1299
|
if (amd) {
|
|
989
1300
|
const amdNum = s(amd, 'number');
|
|
990
1301
|
const to = s(amd, 'toDocumentNumber');
|
|
1302
|
+
const toTitle = s(amd, 'toDocumentShortTitle');
|
|
991
1303
|
amendmentPurpose = s(amd, 'purpose');
|
|
992
|
-
const head = [amdNum, to ? `to ${to}` : undefined]
|
|
1304
|
+
const head = [amdNum, to ? `to ${to}` : undefined, toTitle ? `— ${toTitle}` : undefined]
|
|
1305
|
+
.filter(Boolean)
|
|
1306
|
+
.join(' ');
|
|
993
1307
|
if (head)
|
|
994
1308
|
lines.push(`\n**Amendment:** ${head}`);
|
|
995
1309
|
if (amendmentPurpose)
|
|
@@ -1000,8 +1314,34 @@ function renderSenateVoteDetail(item) {
|
|
|
1000
1314
|
const docText = s(item, 'voteDocumentText');
|
|
1001
1315
|
if (docText && docText !== amendmentPurpose && docText !== docTitle)
|
|
1002
1316
|
lines.push(`\n${docText}`);
|
|
1317
|
+
const rest = renderDetailRest(item, HEADER_SENATE_VOTE_KEYS);
|
|
1318
|
+
if (rest)
|
|
1319
|
+
lines.push('', rest);
|
|
1003
1320
|
return lines.join('\n');
|
|
1004
1321
|
}
|
|
1322
|
+
/**
|
|
1323
|
+
* `chamber` is implicit in the "Senate Vote" heading; `voteResult` / `question`
|
|
1324
|
+
* are the short forms of `voteResultText` / `voteQuestionText`, one of which the
|
|
1325
|
+
* header always renders.
|
|
1326
|
+
*/
|
|
1327
|
+
const HEADER_SENATE_VOTE_KEYS = new Set([
|
|
1328
|
+
'chamber',
|
|
1329
|
+
'voteNumber',
|
|
1330
|
+
'voteResult',
|
|
1331
|
+
'voteResultText',
|
|
1332
|
+
'question',
|
|
1333
|
+
'voteQuestionText',
|
|
1334
|
+
'voteTitle',
|
|
1335
|
+
'congress',
|
|
1336
|
+
'session',
|
|
1337
|
+
'voteDate',
|
|
1338
|
+
'majorityRequirement',
|
|
1339
|
+
'count',
|
|
1340
|
+
'partyTotals',
|
|
1341
|
+
'document',
|
|
1342
|
+
'amendment',
|
|
1343
|
+
'voteDocumentText',
|
|
1344
|
+
]);
|
|
1005
1345
|
/** Senate member voting positions for one roll call — mirrors the House members view. */
|
|
1006
1346
|
function renderSenateVoteMembers(result) {
|
|
1007
1347
|
const vote = (result.vote ?? {});
|
|
@@ -1025,6 +1365,9 @@ function renderSenateVoteMembers(result) {
|
|
|
1025
1365
|
], ' — ');
|
|
1026
1366
|
if (context)
|
|
1027
1367
|
lines.push(context);
|
|
1368
|
+
const voteRest = renderDetailRest(vote, SENATE_VOTE_CONTEXT_KEYS);
|
|
1369
|
+
if (voteRest)
|
|
1370
|
+
lines.push('', voteRest);
|
|
1028
1371
|
if (rows.length === 0) {
|
|
1029
1372
|
lines.push('', total > 0
|
|
1030
1373
|
? `_Page is empty — offset is past the end of ${total} member position${total !== 1 ? 's' : ''}._`
|
|
@@ -1040,6 +1383,17 @@ function renderSenateVoteMembers(result) {
|
|
|
1040
1383
|
}
|
|
1041
1384
|
return lines.join('\n');
|
|
1042
1385
|
}
|
|
1386
|
+
/** Fields of the Senate `vote` context sibling the members header already renders. */
|
|
1387
|
+
const SENATE_VOTE_CONTEXT_KEYS = new Set([
|
|
1388
|
+
'chamber',
|
|
1389
|
+
'voteNumber',
|
|
1390
|
+
'congress',
|
|
1391
|
+
'session',
|
|
1392
|
+
'voteQuestionText',
|
|
1393
|
+
'question',
|
|
1394
|
+
'voteResultText',
|
|
1395
|
+
'voteResult',
|
|
1396
|
+
]);
|
|
1043
1397
|
/** Senate payloads carry a `chamber: 'senate'` marker on the envelope, vote, and items. */
|
|
1044
1398
|
function isSenateResult(result) {
|
|
1045
1399
|
if (result.chamber === 'senate')
|
|
@@ -1068,22 +1422,12 @@ function renderBillDetail(item) {
|
|
|
1068
1422
|
]);
|
|
1069
1423
|
if (meta)
|
|
1070
1424
|
lines.push(meta);
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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
|
-
}
|
|
1425
|
+
const latestAction = latestActionLine(item);
|
|
1426
|
+
if (latestAction)
|
|
1427
|
+
lines.push(latestAction);
|
|
1428
|
+
const citation = lawCitations(item);
|
|
1429
|
+
if (citation)
|
|
1430
|
+
lines.push(`**Law:** ${citation}`);
|
|
1087
1431
|
const rest = renderDetailRest(item, HEADER_BILL_KEYS);
|
|
1088
1432
|
if (rest)
|
|
1089
1433
|
lines.push('', rest);
|
|
@@ -1175,8 +1519,7 @@ function renderMemberDetail(item) {
|
|
|
1175
1519
|
: undefined;
|
|
1176
1520
|
if (termsArr && termsArr.length > 0) {
|
|
1177
1521
|
lines.push(`\n**Terms (${termsArr.length}):**`);
|
|
1178
|
-
const
|
|
1179
|
-
for (const term of recent) {
|
|
1522
|
+
for (const term of termsArr) {
|
|
1180
1523
|
const chamber = s(term, 'chamber');
|
|
1181
1524
|
const start = s(term, 'startYear');
|
|
1182
1525
|
const end = s(term, 'endYear');
|
|
@@ -1184,10 +1527,8 @@ function renderMemberDetail(item) {
|
|
|
1184
1527
|
const stateName = s(term, 'stateName');
|
|
1185
1528
|
const range = start && end ? `${start}–${end}` : start;
|
|
1186
1529
|
const parts = [chamber, range, party, stateName].filter(Boolean);
|
|
1187
|
-
lines.push(`- ${parts.join(', ')}`);
|
|
1530
|
+
lines.push(`- ${withUnnamedFields(term, parts.join(', '), TERM_ROW_KEYS)}`);
|
|
1188
1531
|
}
|
|
1189
|
-
if (termsArr.length > 5)
|
|
1190
|
-
lines.push(`- _...${termsArr.length - 5} earlier_`);
|
|
1191
1532
|
}
|
|
1192
1533
|
const partyHistory = item.partyHistory;
|
|
1193
1534
|
if (Array.isArray(partyHistory) && partyHistory.length > 0) {
|
|
@@ -1198,19 +1539,20 @@ function renderMemberDetail(item) {
|
|
|
1198
1539
|
const end = s(p, 'endYear');
|
|
1199
1540
|
const range = start && end ? `${start}–${end}` : start;
|
|
1200
1541
|
const parts = [partyName, range && `(${range})`].filter(Boolean);
|
|
1201
|
-
lines.push(`- ${parts.join(' ')}`);
|
|
1542
|
+
lines.push(`- ${withUnnamedFields(p, parts.join(' '), PARTY_HISTORY_ROW_KEYS)}`);
|
|
1202
1543
|
}
|
|
1203
1544
|
}
|
|
1204
1545
|
const leadership = item.leadership;
|
|
1205
1546
|
if (Array.isArray(leadership) && leadership.length > 0) {
|
|
1206
1547
|
lines.push(`\n**Leadership Roles (${leadership.length}):**`);
|
|
1207
|
-
for (const l of leadership
|
|
1548
|
+
for (const l of leadership) {
|
|
1208
1549
|
const type = s(l, 'type');
|
|
1209
1550
|
const congress = s(l, 'congress');
|
|
1210
|
-
|
|
1551
|
+
const curated = [type, congress ? `Congress ${congress}` : undefined]
|
|
1552
|
+
.filter(Boolean)
|
|
1553
|
+
.join(' — ');
|
|
1554
|
+
lines.push(`- ${withUnnamedFields(l, curated, LEADERSHIP_ROW_KEYS)}`);
|
|
1211
1555
|
}
|
|
1212
|
-
if (leadership.length > 10)
|
|
1213
|
-
lines.push(`- _...${leadership.length - 10} more_`);
|
|
1214
1556
|
}
|
|
1215
1557
|
const subResources = [];
|
|
1216
1558
|
for (const key of ['sponsoredLegislation', 'cosponsoredLegislation']) {
|
|
@@ -1225,8 +1567,33 @@ function renderMemberDetail(item) {
|
|
|
1225
1567
|
const url = s(item, 'url');
|
|
1226
1568
|
if (url)
|
|
1227
1569
|
lines.push(`\n**URL:** ${url}`);
|
|
1570
|
+
const rest = renderDetailRest(item, HEADER_MEMBER_KEYS);
|
|
1571
|
+
if (rest)
|
|
1572
|
+
lines.push('', rest);
|
|
1228
1573
|
return lines.join('\n');
|
|
1229
1574
|
}
|
|
1575
|
+
const TERM_ROW_KEYS = new Set(['chamber', 'startYear', 'endYear', 'partyName', 'stateName']);
|
|
1576
|
+
const PARTY_HISTORY_ROW_KEYS = new Set(['partyName', 'startYear', 'endYear']);
|
|
1577
|
+
const LEADERSHIP_ROW_KEYS = new Set(['type', 'congress']);
|
|
1578
|
+
const HEADER_MEMBER_KEYS = new Set([
|
|
1579
|
+
'bioguideId',
|
|
1580
|
+
'directOrderName',
|
|
1581
|
+
'invertedOrderName',
|
|
1582
|
+
'honorificName',
|
|
1583
|
+
'partyName',
|
|
1584
|
+
'currentParty',
|
|
1585
|
+
'state',
|
|
1586
|
+
'district',
|
|
1587
|
+
'currentMember',
|
|
1588
|
+
'birthYear',
|
|
1589
|
+
'updateDate',
|
|
1590
|
+
'terms',
|
|
1591
|
+
'partyHistory',
|
|
1592
|
+
'leadership',
|
|
1593
|
+
'sponsoredLegislation',
|
|
1594
|
+
'cosponsoredLegislation',
|
|
1595
|
+
'url',
|
|
1596
|
+
]);
|
|
1230
1597
|
/** Committee list item — name + key fields. Fuzzy-matched rows carry `approximate: true`. */
|
|
1231
1598
|
function renderCommitteeListItem(item, i) {
|
|
1232
1599
|
const name = s(item, 'name') ?? s(item, 'systemCode') ?? 'Committee';
|
|
@@ -1243,8 +1610,18 @@ function renderCommitteeListItem(item, i) {
|
|
|
1243
1610
|
const url = s(item, 'url');
|
|
1244
1611
|
if (url)
|
|
1245
1612
|
lines.push(`**URL:** ${url}`);
|
|
1246
|
-
return lines
|
|
1613
|
+
return withRowRest(item, lines, COMMITTEE_ROW_KEYS);
|
|
1247
1614
|
}
|
|
1615
|
+
/** `parent` is absent: a subcommittee row's owning committee code has to reach the reader. */
|
|
1616
|
+
const COMMITTEE_ROW_KEYS = new Set([
|
|
1617
|
+
'name',
|
|
1618
|
+
'systemCode',
|
|
1619
|
+
'chamber',
|
|
1620
|
+
'committeeTypeCode',
|
|
1621
|
+
'updateDate',
|
|
1622
|
+
'url',
|
|
1623
|
+
'approximate',
|
|
1624
|
+
]);
|
|
1248
1625
|
/** Committee report list item — citation-first; upstream omits title and bill ref. */
|
|
1249
1626
|
function renderCommitteeReportListItem(item, i) {
|
|
1250
1627
|
const citation = s(item, 'citation');
|
|
@@ -1269,8 +1646,76 @@ function renderCommitteeReportListItem(item, i) {
|
|
|
1269
1646
|
lines.push(meta);
|
|
1270
1647
|
if (url)
|
|
1271
1648
|
lines.push(`**URL:** ${url}`);
|
|
1649
|
+
return withRowRest(item, lines, COMMITTEE_REPORT_ROW_KEYS);
|
|
1650
|
+
}
|
|
1651
|
+
const COMMITTEE_REPORT_ROW_KEYS = new Set([
|
|
1652
|
+
'citation',
|
|
1653
|
+
'type',
|
|
1654
|
+
'number',
|
|
1655
|
+
'part',
|
|
1656
|
+
'congress',
|
|
1657
|
+
'chamber',
|
|
1658
|
+
'updateDate',
|
|
1659
|
+
'url',
|
|
1660
|
+
]);
|
|
1661
|
+
// ── Document Content (the `content` operation) ──────────────────────
|
|
1662
|
+
/**
|
|
1663
|
+
* One character window of a legislative document's text.
|
|
1664
|
+
*
|
|
1665
|
+
* The other renderers in this file reshape upstream JSON records; this one
|
|
1666
|
+
* carries a document body, so the text is emitted verbatim — no entity work, no
|
|
1667
|
+
* whitespace collapsing, no markdown wrapper. GPO's pre-formatted layout (column
|
|
1668
|
+
* alignment, indentation, blank-line structure) *is* the document's structure,
|
|
1669
|
+
* and a code fence would break on the doubled backticks bill text uses for
|
|
1670
|
+
* opening quotation marks.
|
|
1671
|
+
*
|
|
1672
|
+
* The header states the window in both vocabularies: a 1-based human range, and
|
|
1673
|
+
* the 0-based `offset` / `nextOffset` a caller actually feeds back.
|
|
1674
|
+
*/
|
|
1675
|
+
function renderDocumentContent(content) {
|
|
1676
|
+
const total = typeof content.totalCharacters === 'number' ? content.totalCharacters : 0;
|
|
1677
|
+
const offset = typeof content.offset === 'number' ? content.offset : 0;
|
|
1678
|
+
const text = typeof content.text === 'string' ? content.text : '';
|
|
1679
|
+
const nextOffset = typeof content.nextOffset === 'number' ? content.nextOffset : null;
|
|
1680
|
+
const truncated = content.truncated === true;
|
|
1681
|
+
const title = s(content, 'documentTitle') ?? 'Document';
|
|
1682
|
+
const lines = [`# ${title}`];
|
|
1683
|
+
const range = text.length > 0
|
|
1684
|
+
? `**Characters ${(offset + 1).toLocaleString('en-US')}–${(offset + text.length).toLocaleString('en-US')} of ${total.toLocaleString('en-US')}**`
|
|
1685
|
+
: `**0 characters**`;
|
|
1686
|
+
lines.push(join([
|
|
1687
|
+
f('Format', s(content, 'format')),
|
|
1688
|
+
range,
|
|
1689
|
+
`**Truncated:** ${truncated}`,
|
|
1690
|
+
`offset: ${offset}`,
|
|
1691
|
+
nextOffset != null ? `next offset: ${nextOffset}` : '_end of document_',
|
|
1692
|
+
]));
|
|
1693
|
+
const sourceUrl = s(content, 'sourceUrl');
|
|
1694
|
+
if (sourceUrl)
|
|
1695
|
+
lines.push(`**Source:** ${sourceUrl}`);
|
|
1696
|
+
const rest = renderDetailRest(content, DOCUMENT_CONTENT_KEYS);
|
|
1697
|
+
if (rest)
|
|
1698
|
+
lines.push('', rest);
|
|
1699
|
+
lines.push('', text || '_This document is empty._');
|
|
1272
1700
|
return lines.join('\n');
|
|
1273
1701
|
}
|
|
1702
|
+
const DOCUMENT_CONTENT_KEYS = new Set([
|
|
1703
|
+
'documentTitle',
|
|
1704
|
+
'format',
|
|
1705
|
+
'sourceUrl',
|
|
1706
|
+
'text',
|
|
1707
|
+
'totalCharacters',
|
|
1708
|
+
'offset',
|
|
1709
|
+
'truncated',
|
|
1710
|
+
'nextOffset',
|
|
1711
|
+
]);
|
|
1712
|
+
/** The `content` payload, when the result carries one. */
|
|
1713
|
+
function documentContentOf(result) {
|
|
1714
|
+
const content = result.content;
|
|
1715
|
+
return typeof content === 'object' && content !== null && !Array.isArray(content)
|
|
1716
|
+
? content
|
|
1717
|
+
: undefined;
|
|
1718
|
+
}
|
|
1274
1719
|
// ── Per-Tool Format Exports ─────────────────────────────────────────
|
|
1275
1720
|
function makeFormatter(detailKeys, itemRenderer, detailRenderer) {
|
|
1276
1721
|
return (result) => {
|
|
@@ -1290,6 +1735,9 @@ function makeFormatter(detailKeys, itemRenderer, detailRenderer) {
|
|
|
1290
1735
|
}
|
|
1291
1736
|
/** Bill browse, detail, and sub-resources (actions, amendments, cosponsors, etc.). */
|
|
1292
1737
|
export function formatBills(result) {
|
|
1738
|
+
const content = documentContentOf(result);
|
|
1739
|
+
if (content)
|
|
1740
|
+
return tb(renderDocumentContent(content));
|
|
1293
1741
|
if (Array.isArray(result.data)) {
|
|
1294
1742
|
const first = result.data[0];
|
|
1295
1743
|
const firstRecord = typeof first === 'object' && first !== null ? first : undefined;
|
|
@@ -1306,7 +1754,8 @@ export function formatBills(result) {
|
|
|
1306
1754
|
* survive into the rendered Markdown.
|
|
1307
1755
|
*/
|
|
1308
1756
|
function renderBillSubresourceSummaryItem(item, i) {
|
|
1309
|
-
const
|
|
1757
|
+
const named = new Set(BILL_SUMMARY_SUBRESOURCE_ROW_KEYS);
|
|
1758
|
+
const version = pick(item, named, 'actionDesc', 'versionCode') ?? 'Summary';
|
|
1310
1759
|
const actionDate = s(item, 'actionDate');
|
|
1311
1760
|
const updated = s(item, 'updateDate');
|
|
1312
1761
|
const lines = [`### ${i + 1}. ${version}`];
|
|
@@ -1316,8 +1765,9 @@ function renderBillSubresourceSummaryItem(item, i) {
|
|
|
1316
1765
|
const text = typeof item.text === 'string' ? htmlToMarkdown(item.text) : '';
|
|
1317
1766
|
if (text)
|
|
1318
1767
|
lines.push('', text);
|
|
1319
|
-
return lines
|
|
1768
|
+
return withRowRest(item, lines, named);
|
|
1320
1769
|
}
|
|
1770
|
+
const BILL_SUMMARY_SUBRESOURCE_ROW_KEYS = ['actionDate', 'updateDate', 'text'];
|
|
1321
1771
|
function pickBillListRenderer(first) {
|
|
1322
1772
|
if ('title' in first && 'number' in first)
|
|
1323
1773
|
return renderBillItem;
|
|
@@ -1363,8 +1813,19 @@ function renderSearchBillItem(item, i) {
|
|
|
1363
1813
|
const summary = s(item, 'summaryPreview');
|
|
1364
1814
|
if (summary)
|
|
1365
1815
|
lines.push('', summary);
|
|
1366
|
-
return lines
|
|
1816
|
+
return withRowRest(item, lines, SEARCH_BILL_ROW_KEYS);
|
|
1367
1817
|
}
|
|
1818
|
+
const SEARCH_BILL_ROW_KEYS = new Set([
|
|
1819
|
+
'billId',
|
|
1820
|
+
'billType',
|
|
1821
|
+
'billNumber',
|
|
1822
|
+
'congress',
|
|
1823
|
+
'title',
|
|
1824
|
+
'originChamber',
|
|
1825
|
+
'latestActionDate',
|
|
1826
|
+
'latestActionText',
|
|
1827
|
+
'summaryPreview',
|
|
1828
|
+
]);
|
|
1368
1829
|
/** Local bill keyword search over the FTS mirror. */
|
|
1369
1830
|
export const formatSearchBills = makeFormatter([], renderSearchBillItem);
|
|
1370
1831
|
/** Member browse, detail, and sponsored/cosponsored legislation. */
|
|
@@ -1419,6 +1880,9 @@ export function formatCommittees(result) {
|
|
|
1419
1880
|
}
|
|
1420
1881
|
/** Committee reports — list, detail, and text. */
|
|
1421
1882
|
export function formatCommitteeReports(result) {
|
|
1883
|
+
const content = documentContentOf(result);
|
|
1884
|
+
if (content)
|
|
1885
|
+
return tb(renderDocumentContent(content));
|
|
1422
1886
|
if (Array.isArray(result.data))
|
|
1423
1887
|
return tb(renderList(result, renderCommitteeReportListItem));
|
|
1424
1888
|
if (Array.isArray(result.text)) {
|
|
@@ -1441,6 +1905,9 @@ export function formatCrsReports(result) {
|
|
|
1441
1905
|
}
|
|
1442
1906
|
/** Daily Congressional Record. Dispatches between volumes/issues and flattened articles. */
|
|
1443
1907
|
export function formatDailyRecord(result) {
|
|
1908
|
+
const content = documentContentOf(result);
|
|
1909
|
+
if (content)
|
|
1910
|
+
return tb(renderDocumentContent(content));
|
|
1444
1911
|
if (Array.isArray(result.data)) {
|
|
1445
1912
|
const first = result.data[0];
|
|
1446
1913
|
const firstRecord = typeof first === 'object' && first !== null ? first : undefined;
|