@cyanheads/congressgov-mcp-server 0.3.31 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/AGENTS.md +26 -5
  2. package/CLAUDE.md +26 -5
  3. package/Dockerfile +13 -0
  4. package/README.md +18 -7
  5. package/dist/config/server-config.d.ts +15 -0
  6. package/dist/config/server-config.d.ts.map +1 -1
  7. package/dist/config/server-config.js +66 -1
  8. package/dist/config/server-config.js.map +1 -1
  9. package/dist/index.js +30 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/mcp-server/tools/definitions/committee-lookup.tool.d.ts.map +1 -1
  12. package/dist/mcp-server/tools/definitions/committee-lookup.tool.js +40 -10
  13. package/dist/mcp-server/tools/definitions/committee-lookup.tool.js.map +1 -1
  14. package/dist/mcp-server/tools/definitions/search-bills.tool.d.ts +47 -0
  15. package/dist/mcp-server/tools/definitions/search-bills.tool.d.ts.map +1 -0
  16. package/dist/mcp-server/tools/definitions/search-bills.tool.js +99 -0
  17. package/dist/mcp-server/tools/definitions/search-bills.tool.js.map +1 -0
  18. package/dist/mcp-server/tools/format-helpers.d.ts +2 -0
  19. package/dist/mcp-server/tools/format-helpers.d.ts.map +1 -1
  20. package/dist/mcp-server/tools/format-helpers.js +31 -0
  21. package/dist/mcp-server/tools/format-helpers.js.map +1 -1
  22. package/dist/services/congress-mirror/congress-mirror-service.d.ts +45 -0
  23. package/dist/services/congress-mirror/congress-mirror-service.d.ts.map +1 -0
  24. package/dist/services/congress-mirror/congress-mirror-service.js +116 -0
  25. package/dist/services/congress-mirror/congress-mirror-service.js.map +1 -0
  26. package/dist/services/congress-mirror/ingest.d.ts +33 -0
  27. package/dist/services/congress-mirror/ingest.d.ts.map +1 -0
  28. package/dist/services/congress-mirror/ingest.js +179 -0
  29. package/dist/services/congress-mirror/ingest.js.map +1 -0
  30. package/dist/services/congress-mirror/normalize.d.ts +25 -0
  31. package/dist/services/congress-mirror/normalize.d.ts.map +1 -0
  32. package/dist/services/congress-mirror/normalize.js +47 -0
  33. package/dist/services/congress-mirror/normalize.js.map +1 -0
  34. package/dist/services/congress-mirror/schema.d.ts +19 -0
  35. package/dist/services/congress-mirror/schema.d.ts.map +1 -0
  36. package/dist/services/congress-mirror/schema.js +44 -0
  37. package/dist/services/congress-mirror/schema.js.map +1 -0
  38. package/dist/services/congress-mirror/types.d.ts +55 -0
  39. package/dist/services/congress-mirror/types.d.ts.map +1 -0
  40. package/dist/services/congress-mirror/types.js +9 -0
  41. package/dist/services/congress-mirror/types.js.map +1 -0
  42. package/package.json +33 -7
  43. package/scripts/_mirror-context.ts +30 -0
  44. package/scripts/congress-mirror-init.ts +23 -0
  45. package/scripts/congress-mirror-refresh.ts +24 -0
  46. package/scripts/congress-mirror-verify.ts +36 -0
  47. package/server.json +5 -5
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @fileoverview Congress bill mirror service — the read path over the local
3
+ * SQLite FTS index of Congress.gov bills. Built on the framework MirrorService:
4
+ * owns the store + ingester, exposes the mirror instance to the lifecycle CLI and
5
+ * the refresh scheduler, and translates the search tool's query grammar into an
6
+ * FTS5 `MATCH` + indexed filters. There is no live-API fallback — keyword search
7
+ * is the entire point — so a cold mirror is a readiness signal the caller turns
8
+ * into an empty result + notice, never a throw.
9
+ * @module services/congress-mirror/congress-mirror-service
10
+ */
11
+ import type { Context } from '@cyanheads/mcp-ts-core';
12
+ import { type Mirror } from '@cyanheads/mcp-ts-core/mirror';
13
+ import { type MirrorApi } from './ingest.js';
14
+ import type { SearchBillsFilters, SearchBillsPage } from './types.js';
15
+ /** Options for constructing the mirror service. */
16
+ export interface CongressMirrorOptions {
17
+ /** Congress numbers to mirror. */
18
+ congresses: number[];
19
+ /** API accessor override — defaults to the shared `CongressApiService` singleton. */
20
+ getApi?: () => MirrorApi;
21
+ /** Filesystem path to the SQLite mirror index. */
22
+ mirrorPath: string;
23
+ }
24
+ export declare class CongressMirrorService {
25
+ private readonly mirror;
26
+ constructor(options: CongressMirrorOptions);
27
+ /** The underlying mirror — exposed for the lifecycle CLI and the refresh scheduler. */
28
+ get mirrorInstance(): Mirror;
29
+ /** `true` once a full sync has ever completed (queryable even mid-refresh). */
30
+ ready(): Promise<boolean>;
31
+ /**
32
+ * Keyword-search the mirror. The query is tokenized and each token individually
33
+ * quoted before the FTS5 `MATCH` is built; an input with no searchable tokens
34
+ * yields an empty page rather than a thrown query. Optional exact-match filters
35
+ * ride the indexed columns; results are BM25-ranked.
36
+ */
37
+ search(filters: SearchBillsFilters, ctx: Context): Promise<SearchBillsPage>;
38
+ }
39
+ /** Initialize the mirror service. Call once in `createApp`'s `setup()` (when enabled). */
40
+ export declare function initCongressMirror(options: CongressMirrorOptions): CongressMirrorService;
41
+ /** Access the initialized mirror service. */
42
+ export declare function getCongressMirror(): CongressMirrorService;
43
+ /** Reset the singleton — test-only. */
44
+ export declare function resetCongressMirror(): void;
45
+ //# sourceMappingURL=congress-mirror-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"congress-mirror-service.d.ts","sourceRoot":"","sources":["../../../src/services/congress-mirror/congress-mirror-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAEL,KAAK,MAAM,EAIZ,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EAA8B,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAGzE,OAAO,KAAK,EAAoB,kBAAkB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAKxF,mDAAmD;AACnD,MAAM,WAAW,qBAAqB;IACpC,kCAAkC;IAClC,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,qFAAqF;IACrF,MAAM,CAAC,EAAE,MAAM,SAAS,CAAC;IACzB,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAC;CACpB;AAwBD,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,OAAO,EAAE,qBAAqB;IAY1C,uFAAuF;IACvF,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,+EAA+E;IAC/E,KAAK,IAAI,OAAO,CAAC,OAAO,CAAC;IAIzB;;;;;OAKG;IACG,MAAM,CAAC,OAAO,EAAE,kBAAkB,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC;CAuClF;AAMD,0FAA0F;AAC1F,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,qBAAqB,GAAG,qBAAqB,CAGxF;AAED,6CAA6C;AAC7C,wBAAgB,iBAAiB,IAAI,qBAAqB,CAOzD;AAED,uCAAuC;AACvC,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C"}
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @fileoverview Congress bill mirror service — the read path over the local
3
+ * SQLite FTS index of Congress.gov bills. Built on the framework MirrorService:
4
+ * owns the store + ingester, exposes the mirror instance to the lifecycle CLI and
5
+ * the refresh scheduler, and translates the search tool's query grammar into an
6
+ * FTS5 `MATCH` + indexed filters. There is no live-API fallback — keyword search
7
+ * is the entire point — so a cold mirror is a readiness signal the caller turns
8
+ * into an empty result + notice, never a throw.
9
+ * @module services/congress-mirror/congress-mirror-service
10
+ */
11
+ import { defineMirror, sqliteMirrorStore, } from '@cyanheads/mcp-ts-core/mirror';
12
+ import { getCongressApi } from '../../services/congress-api/congress-api-service.js';
13
+ import { createCongressBillIngester } from './ingest.js';
14
+ import { toFtsMatch } from './normalize.js';
15
+ import { billStoreSpec } from './schema.js';
16
+ /** Max stored-summary characters surfaced as a search-result preview. */
17
+ const SUMMARY_PREVIEW_LIMIT = 400;
18
+ /** Project a raw mirror row into a search result, truncating the summary preview. */
19
+ function toSearchResult(row) {
20
+ const summary = typeof row.summary === 'string' && row.summary.trim() !== '' ? row.summary : undefined;
21
+ const summaryPreview = summary
22
+ ? summary.length > SUMMARY_PREVIEW_LIMIT
23
+ ? `${summary.slice(0, SUMMARY_PREVIEW_LIMIT - 1).trimEnd()}…`
24
+ : summary
25
+ : undefined;
26
+ return {
27
+ billId: String(row.billId),
28
+ congress: Number(row.congress),
29
+ billType: String(row.billType),
30
+ billNumber: Number(row.billNumber),
31
+ title: typeof row.title === 'string' ? row.title : '',
32
+ ...(row.originChamber ? { originChamber: String(row.originChamber) } : {}),
33
+ ...(row.latestActionDate ? { latestActionDate: String(row.latestActionDate) } : {}),
34
+ ...(row.latestActionText ? { latestActionText: String(row.latestActionText) } : {}),
35
+ ...(summaryPreview ? { summaryPreview } : {}),
36
+ };
37
+ }
38
+ export class CongressMirrorService {
39
+ mirror;
40
+ constructor(options) {
41
+ const store = sqliteMirrorStore(billStoreSpec(options.mirrorPath));
42
+ this.mirror = defineMirror({
43
+ name: 'congress-bills',
44
+ store,
45
+ sync: createCongressBillIngester(options.getApi ?? (() => getCongressApi()), options.congresses),
46
+ });
47
+ }
48
+ /** The underlying mirror — exposed for the lifecycle CLI and the refresh scheduler. */
49
+ get mirrorInstance() {
50
+ return this.mirror;
51
+ }
52
+ /** `true` once a full sync has ever completed (queryable even mid-refresh). */
53
+ ready() {
54
+ return this.mirror.ready();
55
+ }
56
+ /**
57
+ * Keyword-search the mirror. The query is tokenized and each token individually
58
+ * quoted before the FTS5 `MATCH` is built; an input with no searchable tokens
59
+ * yields an empty page rather than a thrown query. Optional exact-match filters
60
+ * ride the indexed columns; results are BM25-ranked.
61
+ */
62
+ async search(filters, ctx) {
63
+ const match = toFtsMatch(filters.query);
64
+ if (match === '') {
65
+ return { items: [], total: 0, offset: filters.offset, limit: filters.limit };
66
+ }
67
+ const structured = [];
68
+ if (filters.congress !== undefined) {
69
+ structured.push({ column: 'congress', op: 'eq', value: filters.congress });
70
+ }
71
+ if (filters.billType) {
72
+ structured.push({ column: 'billType', op: 'eq', value: filters.billType });
73
+ }
74
+ if (filters.originChamber) {
75
+ structured.push({ column: 'originChamber', op: 'eq', value: filters.originChamber });
76
+ }
77
+ ctx.log.debug('Searching bill mirror', {
78
+ match,
79
+ filters: structured.length,
80
+ limit: filters.limit,
81
+ offset: filters.offset,
82
+ });
83
+ const result = await this.mirror.query({
84
+ match,
85
+ ...(structured.length > 0 ? { filters: structured } : {}),
86
+ sort: 'relevance',
87
+ limit: filters.limit,
88
+ offset: filters.offset,
89
+ });
90
+ return {
91
+ items: result.rows.map(toSearchResult),
92
+ total: result.total,
93
+ offset: filters.offset,
94
+ limit: filters.limit,
95
+ };
96
+ }
97
+ }
98
+ // --- Init/accessor pattern ---
99
+ let _service;
100
+ /** Initialize the mirror service. Call once in `createApp`'s `setup()` (when enabled). */
101
+ export function initCongressMirror(options) {
102
+ _service = new CongressMirrorService(options);
103
+ return _service;
104
+ }
105
+ /** Access the initialized mirror service. */
106
+ export function getCongressMirror() {
107
+ if (!_service) {
108
+ throw new Error('CongressMirrorService not initialized — call initCongressMirror() in setup().');
109
+ }
110
+ return _service;
111
+ }
112
+ /** Reset the singleton — test-only. */
113
+ export function resetCongressMirror() {
114
+ _service = undefined;
115
+ }
116
+ //# sourceMappingURL=congress-mirror-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"congress-mirror-service.js","sourceRoot":"","sources":["../../../src/services/congress-mirror/congress-mirror-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EACL,YAAY,EAIZ,iBAAiB,GAClB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,iDAAiD,CAAC;AACjF,OAAO,EAAE,0BAA0B,EAAkB,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C,yEAAyE;AACzE,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAYlC,qFAAqF;AACrF,SAAS,cAAc,CAAC,GAAc;IACpC,MAAM,OAAO,GACX,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IACzF,MAAM,cAAc,GAAG,OAAO;QAC5B,CAAC,CAAC,OAAO,CAAC,MAAM,GAAG,qBAAqB;YACtC,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,qBAAqB,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG;YAC7D,CAAC,CAAC,OAAO;QACX,CAAC,CAAC,SAAS,CAAC;IACd,OAAO;QACL,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;QAC1B,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAC9B,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAC9B,UAAU,EAAE,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC;QAClC,KAAK,EAAE,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;QACrD,GAAG,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1E,GAAG,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnF,GAAG,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnF,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9C,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,qBAAqB;IACf,MAAM,CAAS;IAEhC,YAAY,OAA8B;QACxC,MAAM,KAAK,GAAG,iBAAiB,CAAC,aAAa,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;QACnE,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC;YACzB,IAAI,EAAE,gBAAgB;YACtB,KAAK;YACL,IAAI,EAAE,0BAA0B,CAC9B,OAAO,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,EAC1C,OAAO,CAAC,UAAU,CACnB;SACF,CAAC,CAAC;IACL,CAAC;IAED,uFAAuF;IACvF,IAAI,cAAc;QAChB,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,+EAA+E;IAC/E,KAAK;QACH,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IAC7B,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,MAAM,CAAC,OAA2B,EAAE,GAAY;QACpD,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACxC,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;YACjB,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;QAC/E,CAAC;QAED,MAAM,UAAU,GAAkB,EAAE,CAAC;QACrC,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YACnC,UAAU,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC7E,CAAC;QACD,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;YACrB,UAAU,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC7E,CAAC;QACD,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;YAC1B,UAAU,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,eAAe,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;QACvF,CAAC;QAED,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,uBAAuB,EAAE;YACrC,KAAK;YACL,OAAO,EAAE,UAAU,CAAC,MAAM;YAC1B,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;YACrC,KAAK;YACL,GAAG,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACzD,IAAI,EAAE,WAAW;YACjB,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;QAEH,OAAO;YACL,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC;YACtC,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,KAAK,EAAE,OAAO,CAAC,KAAK;SACrB,CAAC;IACJ,CAAC;CACF;AAED,gCAAgC;AAEhC,IAAI,QAA2C,CAAC;AAEhD,0FAA0F;AAC1F,MAAM,UAAU,kBAAkB,CAAC,OAA8B;IAC/D,QAAQ,GAAG,IAAI,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC9C,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,6CAA6C;AAC7C,MAAM,UAAU,iBAAiB;IAC/B,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CACb,+EAA+E,CAChF,CAAC;IACJ,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,uCAAuC;AACvC,MAAM,UAAU,mBAAmB;IACjC,QAAQ,GAAG,SAAS,CAAC;AACvB,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @fileoverview The Congress bill ingester — the one irreducibly per-source part
3
+ * of the mirror. Merges two upstream Congress.gov sources into one row per bill:
4
+ * the bill list (`/bill/{congress}`, sorted by updateDate) supplies the skeleton
5
+ * (title, chamber, latest action, update date); the CRS summaries list
6
+ * (`/summaries/{congress}`) supplies the summary text. Both reuse the existing
7
+ * `CongressApiService` — no new HTTP-calling code, only merge/orchestration.
8
+ *
9
+ * Merge order matters: the framework upsert writes every declared column for each
10
+ * record, so a partial (summary-only) upsert would null the title. The summaries
11
+ * are therefore loaded into a per-congress map first, then attached as each full
12
+ * bill row is emitted. Summaries are fully re-walked every sync (not date-filtered)
13
+ * so a re-emitted bill never loses a previously-stored summary. The durable
14
+ * checkpoint is the bills' `updateDate` high-water mark; refresh re-scans bills
15
+ * from it inclusively (`sort=updateDate asc`), and the upsert makes the overlap
16
+ * harmless.
17
+ * @module services/congress-mirror/ingest
18
+ */
19
+ import type { SyncGenerator } from '@cyanheads/mcp-ts-core/mirror';
20
+ import type { CongressApiService } from '../../services/congress-api/congress-api-service.js';
21
+ /** The two service methods the ingester needs — injectable so tests can mock the API. */
22
+ export type MirrorApi = Pick<CongressApiService, 'listBills' | 'listSummaries'>;
23
+ /**
24
+ * Build the Congress bill ingester. The returned generator walks the configured
25
+ * congresses; for each, it loads the summary map, then streams bill pages
26
+ * (`sort=updateDate asc`) as full merged rows, advancing the durable checkpoint
27
+ * to the max `updateDate` seen.
28
+ *
29
+ * @param getApi - Accessor for the Congress API service (injectable for tests).
30
+ * @param congresses - Congress numbers to mirror.
31
+ */
32
+ export declare function createCongressBillIngester(getApi: () => MirrorApi, congresses: number[]): SyncGenerator;
33
+ //# sourceMappingURL=ingest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../../../src/services/congress-mirror/ingest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAGV,aAAa,EAEd,MAAM,+BAA+B,CAAC;AAEvC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iDAAiD,CAAC;AAM1F,yFAAyF;AACzF,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC,kBAAkB,EAAE,WAAW,GAAG,eAAe,CAAC,CAAC;AA8GhF;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,MAAM,SAAS,EACvB,UAAU,EAAE,MAAM,EAAE,GACnB,aAAa,CA+Cf"}
@@ -0,0 +1,179 @@
1
+ /**
2
+ * @fileoverview The Congress bill ingester — the one irreducibly per-source part
3
+ * of the mirror. Merges two upstream Congress.gov sources into one row per bill:
4
+ * the bill list (`/bill/{congress}`, sorted by updateDate) supplies the skeleton
5
+ * (title, chamber, latest action, update date); the CRS summaries list
6
+ * (`/summaries/{congress}`) supplies the summary text. Both reuse the existing
7
+ * `CongressApiService` — no new HTTP-calling code, only merge/orchestration.
8
+ *
9
+ * Merge order matters: the framework upsert writes every declared column for each
10
+ * record, so a partial (summary-only) upsert would null the title. The summaries
11
+ * are therefore loaded into a per-congress map first, then attached as each full
12
+ * bill row is emitted. Summaries are fully re-walked every sync (not date-filtered)
13
+ * so a re-emitted bill never loses a previously-stored summary. The durable
14
+ * checkpoint is the bills' `updateDate` high-water mark; refresh re-scans bills
15
+ * from it inclusively (`sort=updateDate asc`), and the upsert makes the overlap
16
+ * harmless.
17
+ * @module services/congress-mirror/ingest
18
+ */
19
+ import { logger } from '@cyanheads/mcp-ts-core/utils';
20
+ import { htmlToPlainText } from './normalize.js';
21
+ /** Congress.gov list endpoints cap the page size at 250 (confirmed live). */
22
+ const PAGE_SIZE = 250;
23
+ function isRecord(value) {
24
+ return typeof value === 'object' && value !== null;
25
+ }
26
+ /** Read a non-empty string (or a stringified number) from a raw field. */
27
+ function str(value) {
28
+ if (typeof value === 'string') {
29
+ const trimmed = value.trim();
30
+ return trimmed === '' ? undefined : trimmed;
31
+ }
32
+ if (typeof value === 'number' && Number.isFinite(value))
33
+ return String(value);
34
+ return;
35
+ }
36
+ /** Read an integer from a raw field (number or numeric string). */
37
+ function int(value) {
38
+ if (typeof value === 'number' && Number.isFinite(value))
39
+ return Math.trunc(value);
40
+ if (typeof value === 'string' && value.trim() !== '') {
41
+ const parsed = Number.parseInt(value.trim(), 10);
42
+ return Number.isNaN(parsed) ? undefined : parsed;
43
+ }
44
+ return;
45
+ }
46
+ /**
47
+ * Derive the composite primary key. Matches the existing
48
+ * `congress://bill/{congress}/{billType}/{billNumber}` resource addressing; the
49
+ * bill type is lowercased so both upstream sources key identically.
50
+ */
51
+ function deriveBillId(congress, billType, billNumber) {
52
+ return `${congress}/${billType.toLowerCase()}/${billNumber}`;
53
+ }
54
+ /**
55
+ * The bill list emits date-only `updateDate` ("2026-06-30"); the API's
56
+ * `fromDateTime` filter wants full ISO 8601. Widen a date-only checkpoint to the
57
+ * start of that day so the inclusive re-scan catches same-day updates.
58
+ */
59
+ function toFromDateTime(checkpoint) {
60
+ return /^\d{4}-\d{2}-\d{2}$/.test(checkpoint) ? `${checkpoint}T00:00:00Z` : checkpoint;
61
+ }
62
+ /**
63
+ * Page the CRS summaries list for one congress into a `billId → plain-text
64
+ * summary` map, keeping the latest version per bill (by summary update/action
65
+ * stamp). Summaries ship as HTML and are stripped to plain text before storage.
66
+ */
67
+ async function buildSummaryMap(api, congress, signal) {
68
+ const latest = new Map();
69
+ let offset = 0;
70
+ while (!signal.aborted) {
71
+ const result = await api.listSummaries({ congress, limit: PAGE_SIZE, offset });
72
+ const rows = result.data;
73
+ if (rows.length === 0)
74
+ break;
75
+ for (const raw of rows) {
76
+ if (!isRecord(raw) || !isRecord(raw.bill))
77
+ continue;
78
+ const bill = raw.bill;
79
+ const c = int(bill.congress);
80
+ const billType = str(bill.type)?.toLowerCase();
81
+ const billNumber = int(bill.number);
82
+ if (c === undefined || billType === undefined || billNumber === undefined)
83
+ continue;
84
+ const text = typeof raw.text === 'string' ? htmlToPlainText(raw.text) : '';
85
+ if (!text)
86
+ continue;
87
+ const id = deriveBillId(c, billType, billNumber);
88
+ const stamp = str(raw.updateDate) ?? str(raw.actionDate) ?? '';
89
+ const existing = latest.get(id);
90
+ if (!existing || stamp >= existing.stamp)
91
+ latest.set(id, { text, stamp });
92
+ }
93
+ if (rows.length < PAGE_SIZE || result.pagination.nextOffset == null)
94
+ break;
95
+ offset += PAGE_SIZE;
96
+ }
97
+ const map = new Map();
98
+ for (const [id, value] of latest)
99
+ map.set(id, value.text);
100
+ return map;
101
+ }
102
+ /** Map one raw bill-list record to a full mirror row, joining the summary map. */
103
+ function toBillRow(raw, fallbackCongress, summaries) {
104
+ const billType = str(raw.type)?.toLowerCase();
105
+ const billNumber = int(raw.number);
106
+ if (billType === undefined || billNumber === undefined)
107
+ return null;
108
+ const congress = int(raw.congress) ?? fallbackCongress;
109
+ const id = deriveBillId(congress, billType, billNumber);
110
+ const latestAction = isRecord(raw.latestAction) ? raw.latestAction : undefined;
111
+ return {
112
+ billId: id,
113
+ congress,
114
+ billType,
115
+ billNumber,
116
+ title: str(raw.title) ?? null,
117
+ summary: summaries.get(id) ?? null,
118
+ originChamber: str(raw.originChamber) ?? null,
119
+ latestActionDate: latestAction ? (str(latestAction.actionDate) ?? null) : null,
120
+ latestActionText: latestAction ? (str(latestAction.text) ?? null) : null,
121
+ updateDate: str(raw.updateDate) ?? null,
122
+ };
123
+ }
124
+ /**
125
+ * Build the Congress bill ingester. The returned generator walks the configured
126
+ * congresses; for each, it loads the summary map, then streams bill pages
127
+ * (`sort=updateDate asc`) as full merged rows, advancing the durable checkpoint
128
+ * to the max `updateDate` seen.
129
+ *
130
+ * @param getApi - Accessor for the Congress API service (injectable for tests).
131
+ * @param congresses - Congress numbers to mirror.
132
+ */
133
+ export function createCongressBillIngester(getApi, congresses) {
134
+ return async function* sync({ mode, checkpoint, signal }) {
135
+ const api = getApi();
136
+ const fromDateTime = mode === 'refresh' && checkpoint ? toFromDateTime(checkpoint) : undefined;
137
+ let maxUpdate = checkpoint;
138
+ logger.info(`Congress bill mirror sync starting (mode=${mode}, congresses=${congresses.join(',')}${fromDateTime ? `, since=${fromDateTime}` : ''})`);
139
+ for (const congress of congresses) {
140
+ if (signal.aborted)
141
+ return;
142
+ const summaries = await buildSummaryMap(api, congress, signal);
143
+ logger.info(`Congress ${congress}: ${summaries.size} summaries loaded`);
144
+ let offset = 0;
145
+ let billsMirrored = 0;
146
+ while (!signal.aborted) {
147
+ const result = await api.listBills({
148
+ congress,
149
+ sort: 'updateDate asc',
150
+ limit: PAGE_SIZE,
151
+ offset,
152
+ ...(fromDateTime ? { fromDateTime } : {}),
153
+ });
154
+ const rows = result.data;
155
+ if (rows.length === 0)
156
+ break;
157
+ const records = [];
158
+ for (const raw of rows) {
159
+ if (!isRecord(raw))
160
+ continue;
161
+ const record = toBillRow(raw, congress, summaries);
162
+ if (!record)
163
+ continue;
164
+ records.push(record);
165
+ const updated = typeof record.updateDate === 'string' ? record.updateDate : undefined;
166
+ if (updated && (maxUpdate === undefined || updated > maxUpdate))
167
+ maxUpdate = updated;
168
+ }
169
+ billsMirrored += records.length;
170
+ yield { records, ...(maxUpdate ? { checkpoint: maxUpdate } : {}) };
171
+ if (rows.length < PAGE_SIZE || result.pagination.nextOffset == null)
172
+ break;
173
+ offset += PAGE_SIZE;
174
+ }
175
+ logger.info(`Congress ${congress}: ${billsMirrored} bills mirrored`);
176
+ }
177
+ };
178
+ }
179
+ //# sourceMappingURL=ingest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest.js","sourceRoot":"","sources":["../../../src/services/congress-mirror/ingest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @fileoverview Pure normalization helpers for the bill mirror — HTML→plain-text
3
+ * stripping (summaries ship as HTML; the FTS index and snippet surface want plain
4
+ * text) and FTS5 `MATCH` escaping (tokenize + individually quote so a stray `-`,
5
+ * an unbalanced quote, or an FTS5 reserved word can't break the query). No I/O.
6
+ * @module services/congress-mirror/normalize
7
+ */
8
+ /**
9
+ * Strip HTML tags to plain text and decode the handful of entities Congress.gov
10
+ * emits in summary bodies. Block-ish tags collapse to spaces so adjacent words
11
+ * don't fuse (`</p><p>` → space), and whitespace is collapsed to single spaces —
12
+ * a discovery/snippet surface doesn't need paragraph structure, and flat text
13
+ * tokenizes more cleanly for FTS5.
14
+ */
15
+ export declare function htmlToPlainText(html: string): string;
16
+ /**
17
+ * Build an FTS5 `MATCH` expression from free-text input. Each token is stripped
18
+ * of embedded quotes and wrapped in double quotes, which neutralizes every FTS5
19
+ * operator (`-`, `*`, `:`, `NEAR`, `AND`, `OR`, `NOT`) so punctuation or a
20
+ * reserved word can't raise a SQLite syntax error. Tokens are AND-combined
21
+ * (implicit-AND across terms). Returns an empty string when the input has no
22
+ * searchable tokens — callers treat that as "no match", not a thrown query.
23
+ */
24
+ export declare function toFtsMatch(input: string): string;
25
+ //# sourceMappingURL=normalize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../../../src/services/congress-mirror/normalize.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAcpD;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQhD"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @fileoverview Pure normalization helpers for the bill mirror — HTML→plain-text
3
+ * stripping (summaries ship as HTML; the FTS index and snippet surface want plain
4
+ * text) and FTS5 `MATCH` escaping (tokenize + individually quote so a stray `-`,
5
+ * an unbalanced quote, or an FTS5 reserved word can't break the query). No I/O.
6
+ * @module services/congress-mirror/normalize
7
+ */
8
+ /**
9
+ * Strip HTML tags to plain text and decode the handful of entities Congress.gov
10
+ * emits in summary bodies. Block-ish tags collapse to spaces so adjacent words
11
+ * don't fuse (`</p><p>` → space), and whitespace is collapsed to single spaces —
12
+ * a discovery/snippet surface doesn't need paragraph structure, and flat text
13
+ * tokenizes more cleanly for FTS5.
14
+ */
15
+ export function htmlToPlainText(html) {
16
+ return html
17
+ .replace(/<\s*br\s*\/?\s*>/gi, ' ')
18
+ .replace(/<\s*\/?(p|div|li|ul|ol|h[1-6]|tr|table)\b[^>]*>/gi, ' ')
19
+ .replace(/<[^>]*>/g, '')
20
+ .replace(/&nbsp;/g, ' ')
21
+ .replace(/&#039;/g, "'")
22
+ .replace(/&#39;/g, "'")
23
+ .replace(/&amp;/g, '&')
24
+ .replace(/&lt;/g, '<')
25
+ .replace(/&gt;/g, '>')
26
+ .replace(/&quot;/g, '"')
27
+ .replace(/\s+/g, ' ')
28
+ .trim();
29
+ }
30
+ /**
31
+ * Build an FTS5 `MATCH` expression from free-text input. Each token is stripped
32
+ * of embedded quotes and wrapped in double quotes, which neutralizes every FTS5
33
+ * operator (`-`, `*`, `:`, `NEAR`, `AND`, `OR`, `NOT`) so punctuation or a
34
+ * reserved word can't raise a SQLite syntax error. Tokens are AND-combined
35
+ * (implicit-AND across terms). Returns an empty string when the input has no
36
+ * searchable tokens — callers treat that as "no match", not a thrown query.
37
+ */
38
+ export function toFtsMatch(input) {
39
+ return input
40
+ .trim()
41
+ .split(/\s+/)
42
+ .map((t) => t.replace(/"/g, '').trim())
43
+ .filter((t) => t.length > 0)
44
+ .map((t) => `"${t}"`)
45
+ .join(' AND ');
46
+ }
47
+ //# sourceMappingURL=normalize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.js","sourceRoot":"","sources":["../../../src/services/congress-mirror/normalize.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,IAAI;SACR,OAAO,CAAC,oBAAoB,EAAE,GAAG,CAAC;SAClC,OAAO,CAAC,mDAAmD,EAAE,GAAG,CAAC;SACjE,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;SACvB,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;SACvB,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC;SACtB,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC;SACtB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;SACrB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;SACrB,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;SACvB,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE,CAAC;AACZ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,KAAK;SACT,IAAI,EAAE;SACN,KAAK,CAAC,KAAK,CAAC;SACZ,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;SACtC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;SAC3B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC;SACpB,IAAI,CAAC,OAAO,CAAC,CAAC;AACnB,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @fileoverview Mirror store schema for the Congress bill FTS index. A single
3
+ * MirrorService-owned table (`bills`) with an FTS5 index over `title` + `summary`
4
+ * and secondary indexes on the filterable columns. The primary key is the derived
5
+ * `billId` (`{congress}/{billType}/{billNumber}`) — a single column, matching the
6
+ * `congress://bill/{congress}/{billType}/{billNumber}` resource addressing.
7
+ * @module services/congress-mirror/schema
8
+ */
9
+ import type { SqliteMirrorStoreSpec } from '@cyanheads/mcp-ts-core/mirror';
10
+ /** Primary table name (the MirrorService-owned table). */
11
+ export declare const BILLS_TABLE = "bills";
12
+ /**
13
+ * The MirrorService primary-table spec. One row per bill, merged from the bill
14
+ * list (title, chamber, latest action, update date) and the CRS summaries list
15
+ * (summary text). `title` + `summary` are FTS-indexed; `congress`, `billType`,
16
+ * and `originChamber` back the exact-match search filters.
17
+ */
18
+ export declare function billStoreSpec(path: string): SqliteMirrorStoreSpec;
19
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/services/congress-mirror/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAE3E,0DAA0D;AAC1D,eAAO,MAAM,WAAW,UAAU,CAAC;AAEnC;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,qBAAqB,CA0BjE"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @fileoverview Mirror store schema for the Congress bill FTS index. A single
3
+ * MirrorService-owned table (`bills`) with an FTS5 index over `title` + `summary`
4
+ * and secondary indexes on the filterable columns. The primary key is the derived
5
+ * `billId` (`{congress}/{billType}/{billNumber}`) — a single column, matching the
6
+ * `congress://bill/{congress}/{billType}/{billNumber}` resource addressing.
7
+ * @module services/congress-mirror/schema
8
+ */
9
+ /** Primary table name (the MirrorService-owned table). */
10
+ export const BILLS_TABLE = 'bills';
11
+ /**
12
+ * The MirrorService primary-table spec. One row per bill, merged from the bill
13
+ * list (title, chamber, latest action, update date) and the CRS summaries list
14
+ * (summary text). `title` + `summary` are FTS-indexed; `congress`, `billType`,
15
+ * and `originChamber` back the exact-match search filters.
16
+ */
17
+ export function billStoreSpec(path) {
18
+ return {
19
+ path,
20
+ table: BILLS_TABLE,
21
+ primaryKey: 'billId',
22
+ version: 1,
23
+ columns: {
24
+ billId: 'TEXT',
25
+ congress: 'INTEGER',
26
+ billType: 'TEXT',
27
+ billNumber: 'INTEGER',
28
+ title: 'TEXT',
29
+ summary: 'TEXT',
30
+ originChamber: 'TEXT',
31
+ latestActionDate: 'TEXT',
32
+ latestActionText: 'TEXT',
33
+ updateDate: 'TEXT',
34
+ },
35
+ fts: ['title', 'summary'],
36
+ indexes: [
37
+ { columns: ['congress'] },
38
+ { columns: ['billType'] },
39
+ { columns: ['originChamber'] },
40
+ { columns: ['updateDate'] },
41
+ ],
42
+ };
43
+ }
44
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/services/congress-mirror/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,0DAA0D;AAC1D,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC;AAEnC;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO;QACL,IAAI;QACJ,KAAK,EAAE,WAAW;QAClB,UAAU,EAAE,QAAQ;QACpB,OAAO,EAAE,CAAC;QACV,OAAO,EAAE;YACP,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE,SAAS;YACnB,QAAQ,EAAE,MAAM;YAChB,UAAU,EAAE,SAAS;YACrB,KAAK,EAAE,MAAM;YACb,OAAO,EAAE,MAAM;YACf,aAAa,EAAE,MAAM;YACrB,gBAAgB,EAAE,MAAM;YACxB,gBAAgB,EAAE,MAAM;YACxB,UAAU,EAAE,MAAM;SACnB;QACD,GAAG,EAAE,CAAC,OAAO,EAAE,SAAS,CAAC;QACzB,OAAO,EAAE;YACP,EAAE,OAAO,EAAE,CAAC,UAAU,CAAC,EAAE;YACzB,EAAE,OAAO,EAAE,CAAC,UAAU,CAAC,EAAE;YACzB,EAAE,OAAO,EAAE,CAAC,eAAe,CAAC,EAAE;YAC9B,EAAE,OAAO,EAAE,CAAC,YAAY,CAAC,EAAE;SAC5B;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @fileoverview Domain types for the Congress bill FTS mirror — the merged row
3
+ * shape, the search filters, and the search page envelope. The mirror row is the
4
+ * single-table projection of two upstream Congress.gov sources (the bill list and
5
+ * the CRS summaries list) keyed by a derived `billId`.
6
+ * @module services/congress-mirror/types
7
+ */
8
+ import type { BillType } from '../../services/congress-api/types.js';
9
+ /**
10
+ * One mirrored bill — a flat row keyed by the derived `billId`
11
+ * (`{congress}/{billType}/{billNumber}`). `title` comes from the bill list;
12
+ * `summary` from the CRS summaries list (HTML-stripped, latest version), nullable
13
+ * because CRS summarizes only a fraction of bills.
14
+ */
15
+ export interface BillMirrorRow {
16
+ billId: string;
17
+ billNumber: number;
18
+ billType: string;
19
+ congress: number;
20
+ latestActionDate: string | null;
21
+ latestActionText: string | null;
22
+ originChamber: string | null;
23
+ summary: string | null;
24
+ title: string | null;
25
+ updateDate: string | null;
26
+ }
27
+ /** Filters accepted by the keyword search. */
28
+ export interface SearchBillsFilters {
29
+ billType?: BillType | undefined;
30
+ congress?: number | undefined;
31
+ limit: number;
32
+ offset: number;
33
+ originChamber?: string | undefined;
34
+ query: string;
35
+ }
36
+ /** A single search result — a merged mirror row projected for display. */
37
+ export interface SearchBillResult {
38
+ billId: string;
39
+ billNumber: number;
40
+ billType: string;
41
+ congress: number;
42
+ latestActionDate?: string;
43
+ latestActionText?: string;
44
+ originChamber?: string;
45
+ summaryPreview?: string;
46
+ title: string;
47
+ }
48
+ /** Search page returned by the mirror service. */
49
+ export interface SearchBillsPage {
50
+ items: SearchBillResult[];
51
+ limit: number;
52
+ offset: number;
53
+ total: number;
54
+ }
55
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/congress-mirror/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAEjE;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,8CAA8C;AAC9C,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,0EAA0E;AAC1E,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,kDAAkD;AAClD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @fileoverview Domain types for the Congress bill FTS mirror — the merged row
3
+ * shape, the search filters, and the search page envelope. The mirror row is the
4
+ * single-table projection of two upstream Congress.gov sources (the bill list and
5
+ * the CRS summaries list) keyed by a derived `billId`.
6
+ * @module services/congress-mirror/types
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/services/congress-mirror/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}