ソースを参照

Initial commit: 119th Congress voting-dashboard pipeline

Python-stdlib data pipeline (fetch → parse → enrich_roster → build_members
→ build_app) + vanilla-JS frontend (member, compare, ranking views) covering
all 552 members of the 119th Congress. Framework-free SPA designed for
standalone, iframe, or inline embedding into third-party hosts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Max 1 ヶ月 前
コミット
37d7ecc39b
61 ファイル変更22427 行追加0 行削除
  1. 19 0
      .gitignore
  2. 104 0
      CLAUDE.md
  3. 657 0
      DOCUMENTATION.md
  4. 155 0
      Methodology.md
  5. 15 0
      NOTES.md
  6. 257 0
      PROJECT_SCOPE.md
  7. 134 0
      analyze.py
  8. 31 0
      build_all.py
  9. 220 0
      build_app.py
  10. 208 0
      build_members.py
  11. 4948 0
      data/119/house/roster.json
  12. 0 0
      data/119/house/votes.jsonl
  13. 105 0
      data/119/lis_to_bioguide.json
  14. 0 0
      data/119/manifest.json
  15. 7165 0
      data/119/members_directory.json
  16. 1240 0
      data/119/senate/roster.json
  17. 273 0
      data/119/senate/votes.jsonl
  18. 308 0
      enrich_roster.py
  19. 131 0
      fetch.py
  20. 92 0
      legacy/AlexandriaOcasioCortez119.html
  21. 92 0
      legacy/ByronDonalds119.html
  22. 92 0
      legacy/IlhanOmar119.html
  23. 92 0
      legacy/JimJordan119.html
  24. 93 0
      legacy/LindseyGraham119.html
  25. 92 0
      legacy/MarjorieTaylorGreene119.html
  26. 92 0
      legacy/RoKhanna119.html
  27. 122 0
      legacy/ThomasMassie119.html
  28. 469 0
      legacy/build_dashboard.py
  29. 87 0
      legacy/build_index.py
  30. 427 0
      legacy/build_member.py
  31. 433 0
      legacy/build_senator.py
  32. 38 0
      legacy/fetch_senate.py
  33. 145 0
      legacy/fetch_votes.py
  34. 106 0
      legacy/index.html
  35. 0 0
      legacy/votes.json
  36. 322 0
      parse.py
  37. 47 0
      research/PHASE3_PARITY.md
  38. 23 0
      research/PHASE6_CSS_AUDIT.md
  39. 70 0
      research/PHASE6_EMBED_TEST.md
  40. 384 0
      research/PLAN.md
  41. 38 0
      research/TEST-PLAN.md
  42. 472 0
      template/app.css
  43. 135 0
      template/app.html
  44. 622 0
      template/app.js
  45. 110 0
      template/compare.html
  46. 689 0
      template/compare.js
  47. 101 0
      template/ranking.html
  48. 269 0
      template/ranking.js
  49. 12 0
      template/vendor/chart.umd.min.js
  50. 1 0
      template/vendor/sortable.min.js
  51. 0 0
      tests/__init__.py
  52. 3 0
      tests/conftest.py
  53. 50 0
      tests/fixtures/bipartisan_house.xml
  54. 49 0
      tests/fixtures/failed_blocking_house.xml
  55. 31 0
      tests/fixtures/failed_blocking_rep_senate.xml
  56. 32 0
      tests/fixtures/failed_blocking_senate.xml
  57. 54 0
      tests/fixtures/partisan_house.xml
  58. 29 0
      tests/fixtures/partisan_senate.xml
  59. 48 0
      tests/fixtures/split_party_house.xml
  60. 205 0
      tests/parity_check.py
  61. 189 0
      tests/test_analyze.py

+ 19 - 0
.gitignore

@@ -0,0 +1,19 @@
+.env
+__pycache__/
+*.pyc
+.DS_Store
+
+# Upstream/external caches (re-fetched on demand)
+**/cache/
+**/api_cache/
+
+# Build outputs (regenerated by build_members.py / build_app.py)
+results/
+data/*/members/
+data/*/build_report.json
+
+# Dev convenience symlink (template/data -> ../data/119)
+template/data
+
+# Test scratch
+.pytest_cache/

+ 104 - 0
CLAUDE.md

@@ -0,0 +1,104 @@
+# Project Context
+
+## What This File Is
+This is the persistent context file for Claude Code. Keep it concise and useful.
+
+## CLAUDE.md Maintenance Rules
+- Keep this file under 160 lines
+- Only record decisions, not explanations of common knowledge
+- When adding something, check if anything existing is now outdated and remove it
+- Use short bullet points, not paragraphs
+- No boilerplate or filler text
+- If a section grows past 10 items, consolidate or prune the least relevant ones
+- Format: what we chose + why in one line (e.g., "vanilla JS over React — embeddable, no build step")
+
+## Stack
+- Python 3 (stdlib only) — fetch, parse, analyze, build_members, build_app, enrich_roster
+- pytest — classifier unit tests + KPI parity gate against legacy/
+- Vanilla JS (IIFE, no framework, no bundler) — frontend must drop into 3rd-party hosts
+- Chart.js 4.4.0 + SortableJS 1.15.2 — vendored locally under `template/vendor/`, no CDN
+- Data source: clerk.house.gov XML + senate.gov XML (no API key); Congress.gov v3 API for roster enrichment (key in `.env`)
+
+## Architecture Decisions
+- One unified vote-record schema for both chambers (`votes.jsonl`) — analyze.py is chamber-agnostic
+- analyze.py is pure (no I/O); consumed by `build_members.py` workers
+- `build_members.py` uses `multiprocessing.Pool` with initializer-shared records (no per-task serialization)
+- Per-member JSON written via tmp + os.replace (atomic); members/ dir wiped on each build to avoid stale files
+- `manifest.json` is the picker index AND carries per-member KPI dict (`k`) so ranking.html needs only one fetch
+- `build_app.py` inlines the full manifest as `<script type="application/json" id="polisci-manifest">` so file:// works for the picker
+- Per-member JSON still requires HTTP (78 MB total — too large to inline)
+- Chart canvases wrapped in `.chart-canvas-wrap` with fixed-height parent `.chart-frame` — prevents Chart.js infinite-growth feedback loop
+- All CSS scoped under `#polisci-root` — required by inline-embed contract (CSS audit gates this at 0 violations)
+- `data-base="./data/"` on `#polisci-root` lets host pages relocate the data dir
+- Senate vote XMLs use LIS member IDs (e.g. `S270`); Congress.gov uses bioguide — `lis_to_bioguide.json` crosswalk built by name+state+party match (Congress.gov v3 does not expose LIS reliably)
+- Roster merge: vote-derived roster gets enriched with `full_name`, `district`, `served_from/to`, `photo_url`, `served_partial` from `members_directory.json`
+- ID regex hard limit: `^[A-Z]\d{6}$|^S\d{3,4}$` validated on every URL read AND against `manifestById`
+- All upstream strings rendered via `textContent` / `createElement` — zero `innerHTML` (security-audited)
+- localStorage namespaced `polisci:v119:*`; only `lastMember` persisted in MVP
+
+## Pipeline
+```
+fetch.py → parse.py → enrich_roster.py → parse.py → pytest → build_members.py → build_app.py
+```
+- `fetch.py` — idempotent network fetch into `data/<C>/{house,senate}/cache/`
+- `parse.py` — XML → `votes.jsonl` + `roster.json`; rejects upstream strings containing `<`, `>`, control chars; merges `members_directory.json` if present
+- `enrich_roster.py` — Congress.gov API → `members_directory.json` + LIS crosswalk; cached responses in `api_cache/`; 350ms throttle; sends `User-Agent: polisci-pipeline/1.0` (API rejects without one)
+- `build_members.py` — parallel pool; passes full chamber records to `analyze.aggregate` (NOT pre-filtered by member) so absences count as N/A rows
+- `build_app.py` — wipes + recreates `results/<C>/`; injects manifest into HTML heads; writes README with CSP + iframe snippet
+- `build_all.py` — one-command wrapper; runs `parse.py` twice (before and after enrich) so directory data merges in
+
+## Data Layout
+- `data/<C>/{house,senate}/{cache/, votes.jsonl, roster.json}` — raw + parsed per-chamber
+- `data/<C>/members/<id>.json` — per-member metrics (~80 KB each)
+- `data/<C>/manifest.json` — picker index w/ KPI `k` field per member
+- `data/<C>/members_directory.json` — Congress.gov roster (~551 members)
+- `data/<C>/lis_to_bioguide.json` — Senate ID crosswalk
+- `data/<C>/api_cache/` — cached Congress.gov responses
+- `data/<C>/build_report.json` — per-build success/failure log
+- `results/<C>/` — embeddable artifact (the shipping output)
+
+## Frontend Pages
+- `app.html` — single-member dashboard: sidebar filters (chamber/party/state), typeahead, 8 KPI tiles, 5 charts, sortable vote table
+- `compare.html` — overlay up to 6 members across 5 comparison charts; per-member color-coded pills
+- `ranking.html` — rank House or Senate members by any of 14 metrics; row click opens member dashboard in new tab
+- Nav between pages: `Member · Compare · Rankings` in `<header>`
+- Member IDs in URL only (`?id=`, `?ids=`, `?c=&m=&o=&p=`) — deep-linkable, validated against manifest
+
+## Embedding Modes
+- Standalone: open `app.html` directly (over HTTP)
+- Iframe: `sandbox="allow-scripts allow-same-origin" referrerpolicy="no-referrer"`
+- Inline: copy `<link>` + `<script>` + `<div id="polisci-root" data-base="...">` into host page
+- Recommended CSP documented in `results/<C>/README.md`
+
+## Verified Properties
+- KPI parity vs `legacy/`: 8/8 PASS (script: `tests/parity_check.py`; report: `research/PHASE3_PARITY.md`)
+- analyze.py classifier: 22 pytest tests against frozen XML fixtures
+- CSS namespace audit: 0 violations (report: `research/PHASE6_CSS_AUDIT.md`)
+- HTTP smoke (all assets + manifest + sample member + 404 path): 10/10
+- No external network at runtime; vendor scripts loaded locally
+- `build_members.py` for 552 members: ~5 s on local CPU
+
+## Security
+- API key in `.env` (gitignored); rotated via Congress.gov sign-up flow; redacted from DOCUMENTATION.md §2
+- `.gitignore`: `.env`, `__pycache__/`, `*.pyc`, `data/*/cache/`, `data/*/api_cache/`
+- Upstream-string validation in `parse.py` rejects `<`, `>`, control chars at XML parse time
+- All DOM mutations via `textContent` / `createElement` / `replaceChildren` — `grep innerHTML` returns empty across template/ and results/
+- No `eval`, no `Function()`, no inline event handlers
+
+## Conventions
+- One JS file per page (`app.js`, `compare.js`, `ranking.js`); IIFE wrappers
+- ~200 lines of filter/typeahead logic duplicated between `app.js` and `compare.js` — accepted for v1 to avoid coupling
+- Python scripts: CLI via argparse, `--congress N` (default 119), shebang + executable
+- No comments in code unless WHY is non-obvious (e.g. security validation, LIS fallback rationale)
+- Tests live in `tests/` (not the test-engineer sandbox) per project requirement
+
+## Known Issues / Deferred (see NOTES.md for full list)
+- Editorial label wording ("Helped Republicans", "Blocked Dem-Backed") — compliance flagged; user kept current copy for MVP, must revisit before publication
+- compare.html "Voted against own party" uses `monthly['Helped Neither']` as proxy — analyze.py doesn't emit a true monthly own-party-defection series
+- Visible "How to read this" caveats panel deferred to v1.1 (limitations live in DOCUMENTATION.md §10)
+- localStorage only persists `lastMember`; filter state lives in URL only
+- Cross-browser manual smoke not run (no headless browser in build env); `_iframe_test.html` + `_embed_test.html` available in `results/119/`
+- 120th-Congress dry run impossible until 120th data exists
+
+## Active Tasks
+- (none — Phase 7 closed)

+ 657 - 0
DOCUMENTATION.md

@@ -0,0 +1,657 @@
+# 119th Congress House Voting Dashboards — Build & Methodology Documentation
+
+> **Note for AI models reading this file:** This document is organized into discrete numbered sections with a complete table of contents directly below. **Use the table of contents to jump to the section relevant to the user's question rather than reading the whole document.** Each section is self-contained and labeled with its purpose. Skim the TOC, identify the matching section by heading, then read only that section. Avoid re-reading prior sections unless cross-referenced.
+
+---
+
+## Table of Contents
+
+1. [Project Overview](#1-project-overview)
+2. [Data Sources & API Credentials](#2-data-sources--api-credentials)
+3. [Roster — Members Included and Excluded](#3-roster--members-included-and-excluded)
+    - [3.1 Member-level notes — when to surface a banner on a dashboard](#31-member-level-notes--when-to-surface-a-banner-on-a-dashboard)
+4. [Data Procurement Process](#4-data-procurement-process)
+5. [XML Parsing & Schema](#5-xml-parsing--schema)
+6. [Classification & Analysis Methodology](#6-classification--analysis-methodology)
+7. [Dashboard Construction](#7-dashboard-construction)
+8. [File Layout & Artifacts](#8-file-layout--artifacts)
+9. [How to Regenerate or Extend](#9-how-to-regenerate-or-extend)
+10. [Known Limitations & Caveats](#10-known-limitations--caveats)
+11. [Change Log](#11-change-log)
+12. [Senate Dashboard Plan (in progress)](#12-senate-dashboard-plan-in-progress)
+
+---
+
+## 1. Project Overview
+
+This project produces self-contained, interactive HTML dashboards summarizing
+every U.S. House roll-call vote of the **119th Congress** (Jan 3, 2025 – present,
+captured 2026-05-23) and analyzing each target member's voting record against
+their party majority, the opposing party majority, and overall outcomes.
+
+For each member the dashboard reports:
+
+- Participation rate (votes cast / total roll calls).
+- Distribution of the member's individual votes (Yea / Nay / Not Voting / Present).
+- Alignment classification per vote: **Helped Republicans**, **Helped Democrats**,
+  **Helped Both**, or **Helped Neither** (see §6).
+- "Blocking wins" counts — how often the member's Nay vote contributed to defeating
+  a measure backed by either party majority.
+- Lone-wolf defection count — votes against the member's own party majority with
+  ≤5 fellow same-party defectors.
+- Monthly trend of alignment classifications.
+- A sortable, filterable table of every roll call.
+
+The output is a directory of standalone `.html` files (each ~160 KB, no server
+required) using Chart.js + SortableJS via CDN.
+
+---
+
+## 2. Data Sources & API Credentials
+
+### Primary source: clerk.house.gov XML rollcalls
+
+Each House roll-call vote is published as an XML file at a deterministic URL:
+
+```
+https://clerk.house.gov/evs/{YEAR}/roll{NNN}.xml
+```
+
+Where `{NNN}` is a 3-digit, 1-indexed roll number resetting each calendar year.
+This is the **authoritative** source (Clerk of the House) and includes per-member
+vote records, party totals, bill identifier, question, result, date, and time.
+**No API key is required.** No authentication, no rate-limit headers documented.
+Self-imposed throttling of 350 ms between fetches was used (see §4).
+
+### Year ranges & index discovery
+
+The Clerk publishes year-index pages at `https://clerk.house.gov/evs/{YEAR}/ROLL_{XXX}.asp`
+(grouped in blocks of 100). These were scraped once to determine the highest
+roll number per year:
+
+- **2025**: rolls `001`–`362` (362 votes)
+- **2026**: rolls `001`–`191` (191 votes)
+- **Total**: 553 roll-call votes
+
+### Supplemental source: Congress.gov API (used for Phase 0.5 roster enrichment)
+
+A Congress.gov API key is required for the complete-roster enrichment step
+performed by `enrich_roster.py` (Phase 0.5 of the project plan). The key is
+loaded from `.env` at runtime:
+
+```
+CONGRESS_GOV_API_KEY=<see .env (gitignored); rotate via https://api.congress.gov/sign-up/ if exposed>
+```
+
+Endpoint: `https://api.congress.gov/v3/...?api_key=<key>`
+
+The clerk.house.gov XML provides every field required for the core roll-call
+analysis (vote breakdown, member vote, bill ID, question, result, date), so
+the original vote-tally pipeline does not call this API. The key is used by
+the roster enrichment pass to fill in members who never appear in a roll-call
+vote during the analysis window, and remains available for extensions needing
+bill subject codes, cosponsor lists, or roll-call cross-references that the
+Clerk XML omits.
+
+> **Security note:** The API key has been moved out of this document and into
+> `.env` (gitignored). To rotate: (1) sign up for a replacement key at
+> https://api.congress.gov/sign-up/, (2) drop the new key into `.env` as
+> `CONGRESS_GOV_API_KEY=...`, (3) delete the old key from the Congress.gov
+> dashboard. If the previously-exposed key is found in git history or
+> redistributed copies of this doc, rotate immediately.
+
+---
+
+## 3. Roster — Members Included and Excluded
+
+### Included (House members of the 119th Congress)
+
+| Display Name              | Bioguide  | Party | Chamber | Dashboard file                          |
+|---------------------------|-----------|-------|---------|-----------------------------------------|
+| Thomas Massie (KY-4)      | M001184   | R     | House   | `ThomasMassie119.html`                  |
+| Ro Khanna (CA-17)         | K000389   | D     | House   | `RoKhanna119.html`                      |
+| Alexandria Ocasio-Cortez (NY-14) | O000172 | D | House   | `AlexandriaOcasioCortez119.html`        |
+| Ilhan Omar (MN-5)         | O000173   | D     | House   | `IlhanOmar119.html`                     |
+| Marjorie Taylor Greene (GA-14) | G000596 | R | House   | `MarjorieTaylorGreene119.html`          |
+| Jim Jordan (OH-4)         | J000289   | R     | House   | `JimJordan119.html`                     |
+| Byron Donalds (FL-19)     | D000032   | R     | House   | `ByronDonalds119.html`                  |
+
+The three Trump-loyal Republican House members selected (Greene, Jordan, Donalds)
+were chosen as among the most frequently identified by political-press coverage
+as among Donald Trump's most consistent House allies and surrogates during the
+119th Congress.
+
+### Explicitly excluded (with reason)
+
+| Requested Name        | Reason for exclusion                                                                        |
+|-----------------------|---------------------------------------------------------------------------------------------|
+| **Lindsey Graham**    | **U.S. Senator (R-SC), not a House member.** clerk.house.gov data does not cover the Senate. Senate roll-call XML lives at `senate.gov/legislative/LIS/roll_call_votes/...` with a different schema. Would require a separate fetcher; out of scope for this build. |
+| **Ron Paul**          | **Not a member of the 119th Congress.** Last served TX-14 in the 112th Congress (ended Jan 3, 2013). No 119th roll-call record exists. |
+
+### 3.1 Member-level notes — when to surface a banner on a dashboard
+
+Some members have circumstances that materially affect how their voting record
+should be read. To prevent misinterpretation, the builder supports an optional
+**member note** — a short banner rendered directly under the dashboard header,
+above the KPI grid, styled as a yellow-bordered callout. The note is also
+preserved in source so it can be discovered when reading the roster.
+
+#### When you MUST add a note
+
+Add a member note whenever the raw numbers would mislead a reader who does
+not already know the member's circumstances. Trigger conditions:
+
+1. **Mid-Congress entry or exit** — resignation, death, expulsion, appointment
+   to executive branch, or seat-flip via special election. Any of these
+   truncates the member's voting window relative to the full Congress.
+   *Example:* Marjorie Taylor Greene's voluntary resignation effective
+   2026-01-05; her dashboard shows 325 of 553 votes cast and the rest as
+   "Not Voting," which without context looks like extreme absenteeism.
+2. **Extended leave of absence** — publicly reported illness, family leave,
+   campaign for higher office, or military deployment lasting weeks+.
+3. **Party switch or change in caucus affiliation** mid-Congress. The
+   alignment/blocking metrics compute against R/D majorities and the
+   member's own party — a switch changes what "lone wolf" means.
+4. **Speakership or leadership role** that conventionally affects voting
+   patterns (e.g., the Speaker traditionally votes only to break ties).
+5. **Vacancies, contested seatings, swearing-in delays** beyond the first
+   few days of the Congress.
+
+#### When notes are optional but recommended
+
+- Member voluntarily abstains from a class of votes for stated reasons
+  (e.g., conflict of interest, recusal).
+- Member is well below median participation (<80%) for reasons not
+  otherwise documented.
+- Member's classification is heavily skewed by a known confounder
+  (e.g., the chair of a committee voting in lockstep on procedural
+  motions specific to that committee).
+
+#### When you should NOT add a note
+
+- Don't add notes that editorialize policy positions ("a controversial
+  conservative voice"). Notes are factual circumstance only.
+- Don't paraphrase the same caveats documented in §10 — those apply
+  to every member and live in the methodology section, not on the
+  dashboard.
+- Don't restate numbers already visible on the dashboard.
+
+#### Format
+
+Notes are passed as the optional 5th element of a `ROSTER` tuple inside
+`build_member.py` (House) or `build_senator.py` (Senate):
+
+```python
+("G000596", "Marjorie Taylor Greene", "R", "MarjorieTaylorGreene119.html",
+ "Rep. Greene publicly announced her resignation from the House in late 2025, "
+ "effective January 5, 2026. This explains her substantially lower "
+ "participation count (325 of 553) versus other members analyzed. Votes "
+ "after her departure date are necessarily recorded as Not Voting in "
+ "clerk.house.gov data."),
+```
+
+The text appears verbatim inside a `<div class="member-note">` banner.
+Keep it under ~3 sentences. Lead with the fact; follow with the
+implication for the metrics on this dashboard.
+
+#### Single-member rebuild after adding a note
+
+```bash
+python3 build_member.py G000596 "Marjorie Taylor Greene" R MarjorieTaylorGreene119.html
+# (Single-member CLI mode does not accept a note — re-run the full ROSTER
+#  loop with `python3 build_member.py` to pick up note changes.)
+```
+
+---
+
+## 4. Data Procurement Process
+
+The fetcher (`fetch_votes.py`) executed once and cached every XML locally so
+subsequent member-by-member analysis re-parses from disk (no re-fetch).
+
+Steps performed:
+
+1. **Year-index discovery** — fetched `https://clerk.house.gov/evs/{YEAR}/ROLL_{000,100,200,300}.asp`
+   for each calendar year, grep'd `rollnumber=NNN` parameters, took the max.
+2. **Per-vote download** — looped `year ∈ {2025, 2026}` × `roll ∈ [1..max_roll]`:
+   - URL: `https://clerk.house.gov/evs/{year}/roll{roll:03d}.xml`
+   - Cache path: `vote_cache/{year}_{roll:03d}.xml`
+   - Skipped if cached file already exists and exceeds 200 bytes.
+   - Sent a `User-Agent: Mozilla/5.0 (research; polisci-analysis)` header.
+   - **Throttle: `time.sleep(0.35)` between successful network fetches** (≈2.9 req/s),
+     well under any conservative rate-limit threshold.
+   - Failures logged to stderr, loop continues (no votes were lost; all 553 fetched).
+3. **Total payload**: 553 XML files, ~14 MB on disk under `vote_cache/`.
+
+Total wall-clock time for the cold fetch: ≈3.5 minutes (553 × 0.35 s + transfer).
+
+---
+
+## 5. XML Parsing & Schema
+
+Each `roll{NNN}.xml` follows DTD `vote v1.0 20031119`. Key extracted fields:
+
+```
+<rollcall-vote>
+  <vote-metadata>
+    <majority>            R or D
+    <rollcall-num>        e.g. 47
+    <legis-num>           e.g. "H R 1234"
+    <vote-question>       e.g. "On Passage"
+    <vote-result>         e.g. "Passed" / "Failed" / "Agreed to" / "Rejected"
+    <action-date>         e.g. "3-Jan-2025"
+    <vote-desc>           short bill title
+    <vote-totals>
+      <totals-by-party>   { party, yea-total, nay-total, present-total, not-voting-total }  × R/D/I
+  <vote-data>
+    <recorded-vote>
+      <legislator name-id="M001184" party="R" state="KY" ...>Massie</legislator>
+      <vote>Yea | Nay | Aye | No | Present | Not Voting</vote>
+    ... (one per legislator)
+```
+
+`Aye`/`No` are emitted for procedural questions; `Yea`/`Nay` for ordinary
+passage. Both are normalized to `Yea`/`Nay` for analysis purposes.
+
+A member's vote is looked up by exact match on `legislator/@name-id` against
+the target Bioguide ID. If the member did not vote, the field is absent
+entirely and we record `None` (rendered as "absent" in classification).
+
+---
+
+## 6. Classification & Analysis Methodology
+
+For every vote, the analyzer determines each party's **majority position**:
+
+```
+party_position = Yea  if  yea > nay
+               = Nay  if  nay > yea
+               = Split otherwise (tie or zero)
+```
+
+### 6.1 Alignment classification
+
+Per vote, the member's normalized vote (`Yea`/`Nay`) is compared to each party's
+majority position:
+
+| Both R-pos and D-pos match member's vote | → `Helped Both`         |
+| Only R-pos matches                       | → `Helped Republicans`  |
+| Only D-pos matches                       | → `Helped Democrats`    |
+| Neither matches (both opposed member)    | → `Helped Neither`      |
+| Member did not vote / voted Present      | → `N/A: <state>`        |
+
+"Helped Both" arises on bipartisan votes where both party majorities aligned
+(common on naming-a-post-office bills, suspension-calendar items, etc.).
+"Helped Neither" arises when member is on the losing side relative to both
+party leaderships — usually a small protest/defector cluster.
+
+### 6.2 Blocking analysis
+
+A "blocking win" is recorded when:
+
+- The member voted `Nay`, AND
+- The measure failed (`result` matches `fail`, `reject`, `not agreed`, `not passed`), AND
+- The other party's majority was on the **opposite side** (i.e., it was a
+  partisan vote, not a bipartisan defeat).
+
+`blocked = "Democrat"` if Dem majority was Yea and Rep majority was not Yea
+(i.e., Dems backed it, member's Nay helped sink it).
+
+`blocked = "Republican"` if Rep majority was Yea and Dem majority was not Yea
+(i.e., GOP backed it, member's Nay helped sink it).
+
+This metric attributes a single "share" of credit to the member for the
+defeat, regardless of margin. Members with many such tallies are
+disproportionately blocking their own caucus's agenda (notable for Massie).
+
+### 6.3 Voted-with / voted-against by party majority
+
+Across votes where each party's majority took a definite position (not Split),
+count how many times the member's normalized vote matched (`with`) or differed
+(`against`) that party's majority. Reported as KPI cards plus a stacked bar
+chart with raw counts and percentages.
+
+### 6.4 Lone-wolf defection
+
+A vote is a **lone-wolf defection** if all of the following hold:
+
+- The member's own party (R or D, per roster) had a definite majority position.
+- The member's normalized vote opposed that majority.
+- ≤5 fellow same-party members also defected (i.e., the member was part of
+  a very small dissenting bloc within their own caucus).
+
+This identifies the "stubborn outliers" within a caucus.
+
+### 6.5 Monthly trend
+
+Each vote's `action-date` is parsed (`DD-Mon-YYYY`) and bucketed by
+`YYYY-MM`. The four primary alignment classes are summed per month
+and rendered as a multi-series line chart.
+
+---
+
+## 7. Dashboard Construction
+
+### 7.1 Template strategy
+
+`build_member.py` holds a single HTML template string with `__PLACEHOLDER__`
+tokens. The Python builder fills in the data, embeds the full per-vote JSON
+payload (typically ~150 KB) inline, and writes the result to
+`results/<Member>119.html`. The output has **no runtime dependencies on local
+files** — Chart.js and SortableJS load from public CDNs.
+
+### 7.2 Libraries used (CDN)
+
+- **Chart.js v4.4.0** — doughnut, bar, stacked bar, horizontal bar, line charts.
+- **SortableJS v1.15.2** — drag-and-drop reordering of KPI cards. Card order
+  is persisted to `localStorage` under a per-bioguide key, so each member's
+  dashboard remembers its own layout.
+
+### 7.3 Layout
+
+- Header (member name + party pill + bioguide + date range)
+- Drag hint + reset-order button
+- **KPI grid** (6 reorderable cards): Roll-Calls/Participation,
+  Voted-Against-GOP, Voted-Against-Dem, Blocked-Dem-backed,
+  Blocked-GOP-backed, Lone-Wolf Defections — all with raw counts + percentages.
+- **Chart row 1**: Alignment doughnut · Vote-distribution bar (with inline labels).
+- **Chart row 2**: Voted-with-vs-against stacked bar · Blocking horizontal bar.
+- **Chart row 3**: Monthly alignment trend line.
+- **Vote table**: sortable, filterable (free-text search + dropdowns for
+  alignment / blocking / member's vote).
+- Footer with source attribution and methodology pointer.
+
+### 7.4 Defensive fixes applied
+
+- Each `<canvas>` is wrapped in a `position:relative; height:300px` container
+  to prevent Chart.js's responsive-resize loop from infinitely growing the page.
+- `.card { min-width: 0 }` so CSS grid columns can shrink properly with
+  long content.
+
+---
+
+## 8. File Layout & Artifacts
+
+The tree is split into a working `/data/` area (raw caches, intermediate
+JSONL, per-member metric files) that the pipeline reads and writes, and a
+`/results/` area that is the actual embeddable artifact shipped to hosts.
+Everything under `/results/<C>/` is self-contained — no external network
+calls at runtime, all vendored — while `/data/` retains the upstream
+caches and build metadata needed to re-derive results from scratch.
+
+```
+polisci/
+├── DOCUMENTATION.md            # this file
+├── NOTES.md                    # deferred concerns; see file
+├── PROJECT_SCOPE.md            # PM-owned scope record (created/updated by PM agent only)
+├── .env                        # gitignored — CONGRESS_GOV_API_KEY=...
+├── .gitignore
+├── fetch.py                    # idempotent network fetch
+├── parse.py                    # XML → votes.jsonl + roster.json (+ merge w/ Congress.gov directory)
+├── analyze.py                  # pure analytics; classify_vote + aggregate
+├── enrich_roster.py            # Congress.gov roster pull + LIS↔bioguide crosswalk
+├── build_members.py            # parallel per-member JSON build
+├── build_app.py                # template → results/<C>/ embeddable artifact
+├── build_all.py                # one-command orchestration
+├── tests/                      # pytest unit tests for analyze.py + parity_check.py
+│   ├── fixtures/*.xml
+│   ├── test_analyze.py
+│   └── parity_check.py
+├── template/
+│   ├── app.html, app.css, app.js
+│   ├── compare.html, compare.js
+│   └── vendor/
+│       ├── chart.umd.min.js    # Chart.js 4.4.0
+│       └── sortable.min.js     # SortableJS 1.15.2
+├── data/119/
+│   ├── house/{cache/, votes.jsonl, roster.json}
+│   ├── senate/{cache/, votes.jsonl, roster.json}
+│   ├── members/<id>.json       # per-member metrics
+│   ├── manifest.json           # member index for picker
+│   ├── members_directory.json  # Congress.gov roster (~551 members)
+│   ├── lis_to_bioguide.json    # Senate ID crosswalk
+│   ├── api_cache/              # cached Congress.gov responses
+│   └── build_report.json
+├── results/119/                # embeddable artifact — what ships
+│   ├── app.html, compare.html, app.js, compare.js, app.css
+│   ├── vendor/{chart…, sortable…}
+│   ├── data/{manifest.json, members/<id>.json}
+│   └── README.md
+└── legacy/                     # archived pre-pivot single-member dashboards
+```
+
+---
+
+## 9. How to Regenerate or Extend
+
+### Full pipeline (step by step)
+
+```bash
+python3 fetch.py          --congress 119      # idempotent; near-zero work if cache populated
+python3 parse.py          --congress 119      # XML → votes.jsonl + roster.json
+python3 enrich_roster.py  --congress 119      # Congress.gov API → members_directory.json + lis_to_bioguide.json
+python3 parse.py          --congress 119      # re-run to merge directory into roster
+pytest tests/                                 # gate: classifier behavior frozen
+python3 build_members.py  --congress 119      # parallel: writes per-member JSON + manifest + build_report
+python3 build_app.py      --congress 119      # template/ + vendor + data → results/<C>/
+```
+
+### One-command equivalent
+
+```bash
+python3 build_all.py --congress 119
+```
+
+### Script reference
+
+- **`fetch.py`** — downloads House (`clerk.house.gov`) and Senate
+  (`senate.gov/LIS`) roll-call XML into `data/<C>/{house,senate}/cache/`.
+  Idempotent; skips files already on disk.
+- **`parse.py`** — parses cached XML into `votes.jsonl` and `roster.json`
+  per chamber. If `members_directory.json` exists, merges it into the
+  roster so members who never cast a vote are still listed.
+- **`enrich_roster.py`** — pulls the canonical Congress.gov member
+  directory and emits `members_directory.json` plus the
+  `lis_to_bioguide.json` Senate-ID crosswalk. Requires
+  `CONGRESS_GOV_API_KEY` in `.env`.
+- **`analyze.py`** — pure functions (`classify_vote`, `aggregate`) shared
+  by `build_members.py` and the parity tests. No I/O.
+- **`build_members.py`** — parallel per-member metric computation;
+  writes `data/<C>/members/<id>.json`, `manifest.json`, and
+  `build_report.json`.
+- **`build_app.py`** — copies `template/` + vendored libraries + the
+  built data into `results/<C>/` — the self-contained, embeddable
+  artifact.
+- **`build_all.py`** — orchestrates the seven steps above with a single
+  `--congress` argument.
+- **`pytest tests/`** — gate run between parse and build; freezes
+  classifier behavior and includes the legacy 8-member KPI parity check.
+
+### Extending to a new Congress
+
+```bash
+python3 build_all.py --congress 120
+```
+
+No code changes required; the pipeline is parameterized end-to-end on
+`--congress`.
+
+---
+
+## 10. Known Limitations & Caveats
+
+- **House only.** Senate votes are not covered; senators (e.g., Lindsey Graham)
+  would require a parallel fetcher targeting `senate.gov/legislative/LIS/...`.
+- **Roll-call votes only.** Voice votes, unanimous-consent agreements, and
+  motions adopted without a recorded vote are invisible to this analysis.
+  A member's silence on a controversial measure that passed by voice vote
+  cannot be detected here.
+- **Bill-subject classification is not attempted.** Alignment counts treat
+  procedural votes (e.g., motions to recommit), naming bills, and major
+  policy votes equivalently. Heavy weighting of procedural calendar votes
+  can inflate "Helped Republicans" / "Helped Democrats" counts vs the
+  substantive picture.
+- **"Helped Both" interpretation.** A bipartisan vote that passes overwhelmingly
+  is genuinely the member helping both sides; it is not noise — but it can
+  dilute the visual share of the more interesting partisan classes.
+- **Blocking-wins attribution.** Each blocking tally credits the member
+  individually for a defeat that involved hundreds of other Nay votes; the
+  metric is a count, not a marginal causal estimate.
+- **Lone-wolf threshold (≤5)** is a heuristic. Tighter (≤2) would isolate
+  true singletons; looser (≤15) would capture organized defector groups.
+  Adjust in `aggregate()` in `build_member.py` if needed.
+- **Member resignations / mid-term entries** are not flagged explicitly in
+  data. A low participation count may reflect resignation, illness, or running
+  for higher office — check the underlying date pattern in the votes table.
+  (See §3 note on MTG's announced 2026 resignation.)
+- **No correction for "Aye"/"Yea" semantic difference.** Both are normalized
+  to `Yea` for analysis; the distinction (passage vs. procedural) is preserved
+  in the per-vote table column.
+- **Data freshness.** Snapshot taken 2026-05-23. Re-run `fetch_votes.py` and
+  `build_member.py` to refresh.
+
+---
+
+## 11. Change Log
+
+| Date       | Change                                                                                  |
+|------------|-----------------------------------------------------------------------------------------|
+| 2026-05-23 | Initial Massie dashboard built (553 votes, 6 KPIs, 5 charts, filterable table)          |
+| 2026-05-23 | Fixed Chart.js infinite-resize bug by wrapping canvases in fixed-height containers       |
+| 2026-05-23 | Added percentages to doughnut legend + tooltip                                          |
+| 2026-05-23 | Added inline count + % labels above each bar in vote-distribution chart                 |
+| 2026-05-23 | Added "Voted Against GOP/Dem Majority" KPI cards + with/against stacked bar chart      |
+| 2026-05-23 | Added blocking-wins horizontal bar; percentages on all KPI cards                        |
+| 2026-05-23 | Added SortableJS drag-and-drop card reordering with `localStorage` persistence          |
+| 2026-05-23 | Merged "Total Roll Calls" and "Massie Voted" into single Participation card             |
+| 2026-05-23 | Parameterized builder (`build_member.py`); generated dashboards for 6 additional House members |
+| 2026-05-23 | Wrote DOCUMENTATION.md; moved Massie dashboard to `results/ThomasMassie119.html`        |
+| 2026-05-24 | Added §3.1 member-notes guidance; rendered MTG resignation banner on her dashboard       |
+| 2026-05-24 | Wrote Senate fetcher + builder; generated `LindseyGraham119.html` (see §12)              |
+
+## 2026-05-24 — v1.0.0  Interactive SPA rewrite (`build_*`, `template/`, `results/`)
+
+Replaced the 8 standalone dashboards in `legacy/` with a parameterized
+pipeline producing a single interactive SPA covering every 119th-Congress
+member (552 total: 449 House + 103 Senate). Single-page member picker
+with searchable typeahead + sidebar filters; comparison view overlays up
+to 6 members across 5 charts; URL-deep-linkable; framework-free and
+embeddable into third-party hosts via standalone, iframe, or inline
+modes (see `results/119/README.md`). Roster completeness now sourced
+from the Congress.gov API (`enrich_roster.py`); the Congress.gov API
+key moved from §2 of this document into `.env` (gitignored). Phase 3
+KPI-parity gate confirmed 8/8 legacy members reproduce exactly.
+
+Generalizes to future Congresses via `--congress N`.
+
+Known limitations carried forward to v1.1: see `NOTES.md`.
+
+---
+
+## 12. Senate Dashboard Plan (in progress)
+
+This section captures the design for extending the House pipeline to U.S.
+Senators, plus the implementation choices for the first build (Lindsey
+Graham, R-SC).
+
+### 12.1 Data source
+
+Senate roll-call XML lives on senate.gov, not clerk.house.gov:
+
+- **Index per session**:
+  `https://www.senate.gov/legislative/LIS/roll_call_lists/vote_menu_119_{S}.xml`
+  where `{S}` is `1` (2025) or `2` (2026).
+- **Per-vote XML**:
+  `https://www.senate.gov/legislative/LIS/roll_call_votes/vote119{S}/vote_119_{S}_{NNNNN}.xml`
+  (5-digit zero-padded vote number — note: **different padding from House
+  rolls**, which use 3 digits).
+
+No API key, no authentication. Same 350 ms throttle policy applies.
+
+### 12.2 Vote counts (snapshot 2026-05-24)
+
+- Session 1 (2025): 659 votes
+- Session 2 (2026): 130 votes
+- **Total: 789 Senate roll-call votes** (≈43% more than the House count for
+  the same Congress, driven by Senate's heavy nominations calendar).
+
+### 12.3 Schema differences vs. House
+
+| Field                 | House (`clerk.house.gov`)              | Senate (`senate.gov`)                   |
+|-----------------------|----------------------------------------|-----------------------------------------|
+| Root element          | `rollcall-vote`                        | `roll_call_vote`                        |
+| Per-party totals      | `vote-totals/totals-by-party`          | **Not present** — must aggregate from per-member records |
+| Member ID             | Bioguide (`name-id="M001184"`)         | LIS (`lis_member_id`, e.g. `S293`)      |
+| Vote element          | `<vote>Yea</vote>`                     | `<vote_cast>Yea</vote_cast>`            |
+| Member party          | attribute on `<legislator>`            | child `<party>` element                 |
+| Date format           | `3-Jan-2025`                           | `January 9, 2025,  02:54 PM`            |
+| Vote question         | `<vote-question>` + `<vote-desc>`      | `<question>` + `<vote_title>` + `<vote_document_text>` |
+| Result                | `<vote-result>` (e.g. "Failed")        | `<vote_result>` (e.g. "Cloture on the Motion to Proceed Agreed to") |
+
+### 12.4 Architectural plan
+
+Two new scripts, mirroring the House pair:
+
+- **`fetch_senate.py`** — fetches `vote_menu_119_{1,2}.xml`, discovers max
+  vote number per session, then loops to download every per-vote XML to
+  `senate_vote_cache/{S}_{NNNNN}.xml`. Idempotent like the House fetcher.
+- **`build_senator.py`** — parses cached XML, aggregates per-party totals
+  from member records, runs the **same classification logic** as the House
+  builder (§6) so the output is methodologically comparable across chambers.
+  Emits HTML to `results/<Senator>119.html` using the same template
+  (substituting "House" → "Senate" in the header and source attribution).
+
+### 12.5 Key implementation choices
+
+- **Party totals from members.** For each vote, walk `<members>/<member>`
+  and increment `{R,D,I} × {yea,nay,present,not_voting}` based on
+  `<party>` and `<vote_cast>`. Independents are tallied separately but
+  classified by which caucus they conference with (Sanders, King → D
+  for majority-position computation, since they reliably caucus with
+  Democrats).
+- **Vote vocabulary.** Senate uses `Yea`/`Nay`/`Present`/`Not Voting`;
+  no `Aye`/`No` distinction. No normalization needed.
+- **Bill identifier.** Compose from `<document>/<document_type>` +
+  `<document_number>` (e.g. "S. 5"). For nominations, fall back to
+  `<vote_title>` (which contains "Motion to Invoke Cloture: ...").
+- **Blocking analysis for nominations.** The Senate's heavy nominations
+  load means many "blocking" results would be against confirming a
+  presidential nominee. The same classification rules (§6.2) apply; readers
+  should remember a "blocked Dem-backed" Senate measure during a Republican
+  trifecta is rare by construction, while "blocked GOP-backed" is more
+  common with a slim majority and a 60-vote cloture threshold.
+- **Member identification.** Roster entries use LIS IDs, not bioguide.
+  Map of LIS → bioguide can be added later if cross-chamber joining is
+  needed. For Graham: `S293` (LIS), `G000359` (bioguide).
+
+### 12.6 Initial build target
+
+| Display Name       | LIS ID | Party | Chamber | Dashboard file              |
+|--------------------|--------|-------|---------|-----------------------------|
+| Lindsey Graham (SC)| S293   | R     | Senate  | `LindseyGraham119.html`     |
+
+### 12.7 Future Senate roster candidates
+
+Suggested next senators (not built yet):
+
+- Bernie Sanders (I-VT, caucuses D) — independent benchmark
+- John Fetterman (D-PA) — heterodox Democrat, useful comparison to Massie
+- Susan Collins (R-ME) — most-defection-prone Republican
+- Rand Paul (R-KY) — Senate counterpart to Massie's libertarian-leaning record
+- Ted Cruz (R-TX) — Trump-aligned Senate Republican
+
+Add via `ROSTER` in `build_senator.py` once written.
+
+### 12.8 Reuse of House template
+
+The HTML template, CSS, and JS in `build_senator.py` are intentionally
+identical to `build_member.py` so dashboards are directly comparable.
+The only structural changes:
+
+- Header subtitle: "Senate roll-call votes" instead of "House roll-call votes."
+- Source attribution: `senate.gov` instead of `clerk.house.gov`.
+- "Voted Against GOP/Dem Majority" denominators use party tallies derived
+  from member-level aggregation (functionally identical, computed differently).
+
+This is intentional: the comparative value of these dashboards depends on
+consistent visual + methodological treatment across chambers.

+ 155 - 0
Methodology.md

@@ -0,0 +1,155 @@
+# Methodology
+
+How the 119th Congress voting-dashboard metrics are computed, and what they do
+and don't measure.
+
+## 1. Data sources
+
+- **House roll calls** — Clerk of the House XML, one file per roll-call vote
+  (`https://clerk.house.gov/evs/<year>/roll<NNN>.xml`). User-facing index:
+  https://clerk.house.gov/Votes
+- **Senate roll calls** — Senate.gov LIS XML, one file per roll-call vote
+  (`https://www.senate.gov/legislative/LIS/roll_call_votes/...`). User-facing
+  index: https://www.senate.gov/legislative/votes_new.htm
+- **Member roster** — Congress.gov v3 API
+  (`/member/congress/119`) supplements vote-derived rosters so members who
+  served but never cast a recorded vote are still represented.
+
+Coverage for the 119th Congress as of the most recent build: 553 House +
+789 Senate roll-call votes; 552 distinct members (449 House + 103 Senate).
+
+## 2. Vote normalization
+
+Each member's recorded vote on a roll call is one of `Yea`, `Nay`, `Aye`,
+`No`, `Present`, `Not Voting`. For analysis, `Aye`/`Yea` are merged into
+`Yea` and `No`/`Nay` are merged into `Nay` — the procedural/substantive
+distinction is preserved only in the per-vote table column.
+
+For every vote, each party's **majority position** is computed:
+
+```
+party_position = Yea   if yea > nay
+               = Nay   if nay > yea
+               = Split otherwise (tie or zero)
+```
+
+## 3. Alignment classification
+
+Per vote, the member's normalized `Yea`/`Nay` is compared to each party's
+majority position:
+
+| Condition                                | Label                |
+|------------------------------------------|----------------------|
+| Member matches BOTH party majorities     | `Helped Both`        |
+| Member matches only Republican majority  | `Helped Republicans` |
+| Member matches only Democratic majority  | `Helped Democrats`   |
+| Member matches NEITHER party majority    | `Helped Neither`     |
+| Member did not cast a Yea/Nay            | `N/A: <state>`       |
+
+- **Helped Both** arises on bipartisan votes (post-office namings, suspension
+  calendar items, broadly popular measures).
+- **Helped Neither** arises when the member is on the losing side relative to
+  both party leaderships — typically a small protest/defector cluster.
+
+## 4. Blocking analysis
+
+A **blocking win** is recorded when ALL of the following hold:
+
+1. The member voted `Nay`.
+2. The measure failed (`result` matches `fail`, `reject`, `not agreed`,
+   `not passed`, `not invoked`).
+3. The other party's majority was on the opposite side (it was a partisan
+   defeat, not a bipartisan one).
+
+- `blocked = "Democrat"` — Democratic majority was Yea, Republican majority
+  was not Yea, member voted Nay → counted as helping sink a Democrat-backed
+  measure.
+- `blocked = "Republican"` — symmetric.
+
+This is a per-share count, not a marginal causal estimate: the metric
+credits the member individually for a defeat that may have involved hundreds
+of other Nay votes.
+
+## 5. Voted-with / voted-against by party majority
+
+Across votes where each party had a definite majority (not Split), the
+member's normalized vote is counted as matching (`with`) or differing
+(`against`) that party's majority. Reported as KPI tiles and as a
+stacked bar chart of raw counts and percentages.
+
+## 6. Lone-wolf defection
+
+A vote counts as a **lone-wolf defection** when:
+
+1. The member's own party (per roster) had a definite majority position.
+2. The member's normalized vote opposed that majority.
+3. The number of fellow same-party defectors was at most the chamber
+   threshold (**5 in the House, 3 in the Senate**).
+
+Identifies stubborn outliers — members who repeatedly break with their
+own caucus when very few others do.
+
+## 7. Monthly trend
+
+Each vote's date is bucketed by `YYYY-MM`. The four primary alignment
+classes (Helped Republicans / Helped Democrats / Helped Both /
+Helped Neither) are summed per month and rendered as a multi-series
+line chart.
+
+## 8. Ranking metrics
+
+The Rankings page sorts the chosen chamber by any of:
+
+- Raw counts: Total Votes, Yeas, Nays, Voted With/Against GOP, Voted
+  With/Against Dem, Lone Wolf Votes
+- Percentages of votes cast: Participation %, Voted With/Against GOP %,
+  Voted With/Against Dem %, Lone Wolf %
+
+Percentage metrics exclude members with zero votes cast (denominator
+undefined).
+
+## 9. Comparison view
+
+The comparison view overlays up to six members on five charts: alignment
+over time, "voted against own party" rate over time (using
+`Helped Neither` as a proxy — see Limitations §10), grouped KPI bar,
+defection scatter (% against GOP vs % against Dem), and vote distribution.
+
+## 10. Known limitations and caveats
+
+- **Roll-call votes only.** Voice votes, unanimous-consent agreements, and
+  motions adopted without a recorded vote are invisible. A member's
+  silence on a controversial measure that passed by voice cannot be
+  detected here.
+- **Procedural vs. substantive votes are treated equally.** Bill-subject
+  classification is not attempted. Heavy weighting of procedural-calendar
+  votes can inflate "Helped Republicans" / "Helped Democrats" counts vs.
+  the substantive picture.
+- **"Helped Both" interpretation.** A bipartisan vote that passes
+  overwhelmingly is a real instance of the member helping both sides — but
+  it can visually dilute the more interesting partisan classes.
+- **Blocking-wins attribution.** Each blocking tally credits the member
+  individually for a defeat that may have involved hundreds of other Nay
+  votes. The metric is a count of partisan defeats the member's Nay vote
+  belonged to, not a marginal causal estimate.
+- **Lone-wolf threshold** (≤5 House / ≤3 Senate) is a heuristic.
+- **"Voted against own party" overlay is a proxy.** The comparison-page
+  monthly chart uses `Helped Neither` per month as an approximation; the
+  pipeline does not currently emit a true monthly own-party-defection
+  series.
+- **Mid-term resignations / replacements** are not flagged on dashboards
+  beyond the `served_partial` banner for members who cast zero recorded
+  votes. A low participation count may reflect resignation, illness, or
+  campaigning for higher office — check the underlying date pattern in
+  the votes table.
+- **Editorial label wording.** Labels such as "Helped Republicans" and
+  "Blocked Dem-Backed" attribute intent that the math does not measure
+  directly. They describe a counting relationship between the member's
+  vote and party majorities, not motive.
+
+## 11. Reproducibility
+
+Each per-member JSON includes a `_meta` block with `schema_version`,
+`pipeline_version`, `classifier_hash` (SHA-256 of `analyze.py`),
+`data_snapshot_date`, and per-chamber `source_xml_count`. The classifier
+hash changes if and only if the classification logic changes.

+ 15 - 0
NOTES.md

@@ -0,0 +1,15 @@
+# NOTES — Deferred Concerns
+
+This file captures known concerns considered but deliberately deferred from the MVP scope of the 119th Congress voting-dashboard rewrite. Each item names the source of the concern (compliance, PM, programmer) and the reason it was deferred.
+
+1. **Editorial label wording (compliance Finding 1, High).** The compliance review flagged that current labels such as "Helped Republicans" and "Blocked Dem-Backed" attribute intent and agency that the underlying math does not actually measure. Neutral alternatives were proposed — for example, "Aligned with R majority" and "Voted Nay on failed D-backed measures" — which describe the observed voting behavior without imputing motive. The decision is to keep the current wording per explicit user preference for the MVP, and to revisit this language before any third-party publication or wider distribution.
+
+2. **compare.html chart scope (PM + programmer, Medium).** Both the PM and programmer agents observed that the voted-against-own-party overlay and the vote-distribution comparison chart are largely derivative of data already shown in the single-member view, raising a question of whether all five comparison charts add distinct value. The decision is to ship all five charts in MVP per user preference, and to trim the set later only if user-research feedback shows that specific charts are redundant or confusing.
+
+3. **Visible caveats panel (compliance Finding 2).** Section 10 of DOCUMENTATION.md enumerates material limitations of the analysis: procedural and substantive votes are treated equally, blocking is computed per-share rather than at the margin, the lone-wolf threshold of ≤5 is a heuristic, and voice votes are invisible to the pipeline. Compliance recommended surfacing these caveats to end users via a collapsible "How to read this" panel on each dashboard page. This work is deferred to v1.1 rather than blocking MVP launch.
+
+4. **`localStorage` as MVP feature (PM, Could-Have).** Client-side persistence of user preferences via `localStorage` was scoped as a Could-Have rather than a Must-Have for the MVP. The PM agent flagged it as the first candidate to cut if Phase 3 implementation runs long, so it is being tracked here as an explicit deferral rather than a forgotten feature.
+
+5. **120th Congress dry-run (programmer).** The rewrite generalizes the pipeline so that it can ingest any Congress by number, but no live 120th Congress data exists yet to actually exercise that generalization end-to-end. Until real 120th data is available, the programmer's confidence in the parameterized pipeline rests on code review and unit tests alone; a true dry-run validation is deferred until the data lands.
+
+6. **Comparison "Voted against own party" proxy (Phase 5).** `analyze.py` emits monthly alignment buckets keyed by [Helped R / Helped D / Helped Both / Helped Neither], but does NOT emit a per-month "voted against own party" series. The compare.html chart uses `monthly['Helped Neither']` as a proxy, since a vote that helped neither party is the strongest signal of own-party defection available from current aggregates. **Caveat:** "Helped Neither" can also include votes where both parties were Split or where the member voted Yea on a tied vote — it is not a pure measure of own-party defection. Surfaced in the chart's figcaption. A future enhancement to analyze.py could emit a true monthly own-party-defection series.

+ 257 - 0
PROJECT_SCOPE.md

@@ -0,0 +1,257 @@
+# Project: 119th Congress Voting Dashboard
+
+_Written post-hoc by the PM agent (2026-05-24) to codify shipped reality through Phase 6.
+The implementation plan that drove the build lives at `research/PLAN.md`.
+Only the PM agent may edit this file._
+
+---
+
+## 1. Project Overview
+
+A static-file, framework-free interactive dashboard that surfaces member-level
+roll-call voting behavior for every member of the 119th US Congress. Eight
+standalone legacy HTML files (Massie, Khanna, AOC, Omar, MTG, Jordan, Donalds,
+Graham) were replaced with a unified single-page application covering all ~552
+seated members, served from a single build artifact at `results/119/`.
+
+The project is a personal research and analysis tool, built and operated by a
+solo hobbyist analyst. It is designed for the analyst's own use and for sharing
+with third-party hosts who can embed the artifact via iframe or inline `<div>`.
+
+---
+
+## 2. Target Users
+
+| User | Description |
+|------|-------------|
+| Primary | The analyst themselves — personal research, exploration, and editorial work on congressional voting patterns. |
+| Secondary | Third-party hosts who embed the artifact via iframe or inline `<div>` into their own pages. Embedding is fully supported by design. |
+
+---
+
+## 3. Core Value Proposition
+
+Given any member of the 119th Congress, instantly render a reproducible,
+citation-quality breakdown of how their roll-call votes aligned with or
+diverged from each party's majority position — without a page reload, without
+a server, and without any external network dependency after the initial page
+load.
+
+---
+
+## 4. Scope of Work
+
+### In Scope (MVP — as built through Phase 6)
+
+| Feature | Description | Priority |
+|---------|-------------|----------|
+| Single-member SPA dashboard | `app.html` — searchable typeahead + sidebar filters (chamber, party, state); on selection, fetches per-member JSON and mutates Chart.js datasets in place without teardown | Must Have |
+| 5 core charts | Vote distribution, alignment doughnut, blocking bars, alignment-over-time line, with/against stacked bar — created once at page init | Must Have |
+| Sortable/filterable vote table | Per-vote rows; all upstream strings rendered via `textContent` (no `innerHTML`) | Must Have |
+| URL deep-linking | `pushState` on member selection; `replaceState` on filter typing; reload restores state | Must Have |
+| localStorage persistence | Last-selected member persisted as `polisci:v119:lastMember`; validated against manifest on read | Could Have (shipped) |
+| Comparison view | `compare.html` — multi-select pills (up to 6 members); 5 overlay charts; shareable `?ids=` URL | Must Have |
+| All-member coverage | `enrich_roster.py` pulls the complete 119th roster from Congress.gov API; members with zero roll-call votes receive a `served_partial` banner | Must Have |
+| Framework-free embedding | Three modes: standalone, iframe (`sandbox="allow-scripts allow-same-origin"`), inline `<div id="polisci-root" data-base="…">` | Must Have |
+| Zero CDN dependencies | Chart.js 4.4.0 + SortableJS 1.15.2 vendored locally in `template/vendor/`; no runtime external requests | Must Have |
+| Parameterized pipeline | All build scripts accept `--congress N`; generalizes to future Congresses without code changes | Must Have |
+| Reproducibility metadata | Every per-member JSON includes a `_meta` block: schema version, pipeline version, classifier hash, data snapshot date, source XML counts | Must Have |
+| Test suite | `pytest tests/test_analyze.py` with frozen XML fixtures covering partisan, bipartisan, absent-member, and failed-blocking cases | Must Have |
+
+### Out of Scope (Future / Deferred — see `NOTES.md` for full rationale)
+
+- **Editorial label rewording** — "Helped Republicans / Blocked Dem-Backed" language kept per user preference; neutral alternatives deferred to v1.1 before any third-party publication (NOTES item 1, compliance Finding 1, High risk)
+- **Comparison chart trimming** — all 5 overlay charts shipped; reducing the set deferred pending user-research feedback (NOTES item 2)
+- **Visible caveats panel** — collapsible "How to read this" panel describing methodology limitations deferred to v1.1 (NOTES item 3, compliance Finding 2)
+- **Full localStorage persistence** — only `lastMember` is persisted; filter state persistence deferred (NOTES item 4)
+- **120th Congress dry-run** — pipeline is parameterized but cannot be validated end-to-end until 120th data exists (NOTES item 5)
+- **True own-party-defection series** — `compare.html` uses `Helped Neither` as a proxy; a precise monthly series requires a future `analyze.py` enhancement (NOTES item 6)
+
+---
+
+## 5. Feature Flow
+
+**Single-member view (`app.html`):**
+
+```
+1. Page loads → fetches manifest.json (552 members, version-stamped)
+2. Sidebar populated (chamber, party, state checkboxes)
+3. User types in typeahead or applies sidebar filters → list narrows live
+4. User selects a member → pushState updates URL to ?id=<bioguide>
+5. App fetches data/members/<id>.json (cache-busted by manifest version)
+6. Chart.js datasets mutated in place; chart.update('none') called — no teardown
+7. KPI cards, sortable/filterable vote table, and member-note banner update
+8. Reloading the URL restores the same member
+```
+
+**Comparison view (`compare.html`):**
+
+```
+1. Page loads → same manifest fetch and sidebar setup
+2. User selects members via typeahead → color-coded pills appear
+3. App fetches each selected member's JSON; 5 overlay charts update
+4. URL updates to ?ids=<id1>,<id2>,... (shareable, capped at 6)
+5. Pill click opens member's app.html?id=<id> in a new tab
+6. Reloading restores all selected members from URL
+```
+
+---
+
+## 6. Usability Concerns
+
+- **Mobile**: sidebar collapses on small viewports; responsive `@media (max-width: 768px)` block in `app.css`
+- **Accessibility**: all upstream strings via `textContent`; bill links built via `createElement` with validated href
+- **Embedding safety**: all CSS namespaced under `#polisci-root` (0 unscoped rules per Phase 6 audit); `data-base` attribute makes data path host-configurable
+- **Performance**: 552-member manifest loads once; per-member JSON is ~80 KB; switching members costs one fetch, not a page load; full build completes in under 5 seconds
+- **No external requests after page load**: verified by Phase 6 grep audit and HTTP smoke test
+
+---
+
+## 7. Technical Considerations
+
+**Tech stack:**
+- Python 3, stdlib only (no third-party packages at runtime); `pytest` for tests
+- Vanilla JS (ES2020, no framework, no transpiler)
+- HTML/CSS (no preprocessor)
+- Chart.js 4.4.0 + SortableJS 1.15.2, vendored locally
+
+**Data pipeline:**
+```
+clerk.house.gov XML  ──┐
+                       ├─→ fetch.py → parse.py → enrich_roster.py
+senate.gov XML     ────┘                              │
+Congress.gov API ─────────────────────────────────────┘
+                                     ↓
+                             build_members.py (parallel pool)
+                             → data/119/members/<id>.json × 552
+                             → data/119/manifest.json
+                                     ↓
+                               build_app.py
+                             → results/119/  (embeddable artifact)
+```
+
+**Data sources:**
+- `clerk.house.gov` — House roll-call XML (553 votes cached)
+- `senate.gov` — Senate roll-call XML (789 votes cached)
+- `congress.gov/v3` API — complete 119th roster + Senate LIS-to-bioguide crosswalk; API key in `.env` (gitignored)
+
+**Security posture:**
+- All upstream strings rendered via `textContent`, never `innerHTML`
+- `parse.py` rejects strings containing `<`, `>`, or control characters
+- Query string `id` matched against `^[A-Z]\d{6}$|^S\d{3}$` and verified against manifest before any fetch or DOM use
+- `ids` capped at 6; each validated against manifest
+- localStorage values regex-validated against manifest allowlist on read
+- No `postMessage` API (frame-boundary attack surface closed in v1)
+- No CDN; no SRI question
+
+**Deployment:** fully static; serve `results/119/` from any file host or web server. Recommended CSP and iframe sandbox snippet documented in `results/119/README.md`.
+
+---
+
+## 8. Implementation Plan
+
+### Phase 0 — Reset and scaffolding
+- [x] Task 0.1 — Create `.env` with API key; add `.gitignore` (`.env`, `__pycache__/`, `*.pyc`, `data/*/cache/`)
+- [x] Task 0.2 — Redact API key from `DOCUMENTATION.md` §2; add rotation guidance
+- [x] Task 0.3 — Create `NOTES.md` with 6 deferred concerns
+- [x] Task 0.4 — Download Chart.js 4.4.0 + SortableJS 1.15.2 into `template/vendor/`
+
+### Phase 0.5 — Complete-roster enrichment
+- [x] Task 0.5.1 — Write `enrich_roster.py`: paginate Congress.gov `/member/congress/119`; write `members_directory.json`; build `lis_to_bioguide.json` via second-pass senator detail fetch
+- [x] Task 0.5.2 — Modify `parse.py`: merge `members_directory.json` into `roster.json`; apply `served_partial` flag to zero-vote members; validate `len(roster) >= 535`
+
+### Phase 1 — Foundation
+- [x] Task 1.1 — Write `build_members.py`: `multiprocessing.Pool`, atomic writes, `_meta` block, `build_report.json`, manifest array + version field
+- [x] Task 1.2 — Write `tests/fixtures/*.xml` (partisan, bipartisan, absent, failed-blocking) + `tests/test_analyze.py`
+- [x] Task 1.3 — Write skeleton `template/app.html` + `template/app.css` (namespaced under `#polisci-root`)
+- [x] Task 1.4 — Gate: `enrich_roster.py` runs clean; `parse.py` produces merged roster; `pytest` passes; `build_members.py` emits ≥535 JSONs + manifest
+
+### Phase 2 — Single-member view
+- [x] Task 2.1 — Write `template/app.js`: manifest loader, sidebar filters, typeahead, member-fetch + in-place Chart.js updates for all 5 charts
+- [x] Task 2.2 — Sortable/filterable vote table (ported from legacy; `textContent` substitution)
+- [x] Task 2.3 — URL deep-linking (`pushState` on selection, `replaceState` on filter typing, `popstate` handler)
+- [x] Task 2.4 — Member-note banner for `served_partial` members
+- [x] Task 2.5 — localStorage persistence for `lastMember` (namespaced + validated)
+
+### Phase 3 — KPI parity gate (hard gate)
+- [x] Task 3.1 — Regenerate 8 legacy members through new pipeline; diff all KPIs against `legacy/*.html`
+- [x] Task 3.2 — Confirm MTG banner, deep-links, and no CDN traffic
+- [x] Task 3.3 — Gate result: 8/8 PASS — safe to proceed to Phase 4
+
+### Phase 4 — Full member build
+- [x] Task 4.1 — `build_members.py --congress 119` for all members; confirm 552 JSONs, 0 failures in `build_report.json`
+- [x] Task 4.2 — Smoke-test 10 randomly chosen members across both chambers and all parties
+
+### Phase 5 — Comparison view
+- [x] Task 5.1 — Write `compare.html` + multi-select pills, shareable `?ids=` URL (scaffolding)
+- [x] Task 5.2 — Overlay chart 1: alignment-over-time line (per member, with alignment-class switcher)
+- [x] Task 5.3 — Overlay chart 2: voted-against-own-party rate over time (`Helped Neither` proxy)
+- [x] Task 5.4 — Overlay chart 3: side-by-side KPI grouped bar
+- [x] Task 5.5 — Overlay chart 4: defection scatter (X: % against GOP, Y: % against Dem)
+- [x] Task 5.6 — Overlay chart 5: vote-distribution grouped bar (Yea/Nay/Present/Not Voting)
+
+### Phase 6 — Embedding, security hardening, polish
+- [x] Task 6.1 — Write `build_app.py`: copy template → results; stamp manifest version into HTML; copy data; write `results/119/README.md` with CSP + sandbox snippet
+- [x] Task 6.2 — CSS namespace audit: 0 unscoped rules confirmed; inline-div embed smoke test in `results/119/_embed_test.html`
+- [x] Task 6.3 — `data-base` attribute support; iframe embed test (`_iframe_test.html`, `_iframe_compare_test.html`); HTTP smoke via `python3 -m http.server 8765`; external-URL grep audit (zero runtime external calls)
+
+### Phase 7 — Documentation and close-out (in progress)
+- [ ] Task 7.1 — Update `DOCUMENTATION.md` §8 (new file layout), §9 (new regeneration commands), §11 (change-log entries)
+- [x] Task 7.2 — Write `PROJECT_SCOPE.md` reflecting shipped reality (this file)
+- [ ] Task 7.3 — Delete `legacy/` after user confirmation
+
+---
+
+## 9. Success Criteria
+
+- [x] `pytest tests/` — all green
+- [x] `build_members.py --congress 119` — 552 member JSONs, 0 failures, completes in under 5 seconds
+- [x] Phase 3 KPI parity gate — 8/8 members PASS against legacy output
+- [x] Phase 6 CSS audit — 0 unscoped rules
+- [x] Phase 6 external-URL audit — 0 runtime external network calls
+- [x] Switching members in `app.html` re-renders without a page load
+- [x] Comparison view accepts up to 6 members and renders all 5 overlay charts
+- [x] Iframe and inline-div embedding verified
+- [ ] Manual cross-browser smoke (Chrome, Firefox, Safari) — deferred to user (no headless browser available on build host)
+
+---
+
+## 10. Known Risks and Open Items
+
+See `NOTES.md` for the full 6-item list with rationale. Summary:
+
+1. **Editorial label wording** (High, compliance Finding 1) — biggest risk before any third-party publication. Labels impute intent; neutral alternatives exist. Must resolve before v1.1 public release.
+2. **Comparison chart redundancy** (Medium) — 5 charts shipped; trim later if user research shows overlap.
+3. **Visible caveats panel** (Medium, compliance Finding 2) — methodology limitations not surfaced to end users yet; deferred to v1.1.
+4. **localStorage scope** (Low) — only `lastMember` persisted; full filter persistence not implemented.
+5. **120th Congress validation** (Low) — pipeline parameterization untested against real data; validate when 120th data lands.
+6. **Own-party-defection proxy** (Low) — `Helped Neither` used as proxy in compare chart; a true series requires a future `analyze.py` change.
+
+---
+
+## 11. Future Congresses
+
+The pipeline is fully parameterized. To build the 120th Congress dashboard:
+
+```bash
+python3 fetch.py          --congress 120   # once roll-call data is available
+python3 parse.py          --congress 120
+python3 enrich_roster.py  --congress 120
+pytest tests/
+python3 build_members.py  --congress 120
+python3 build_app.py      --congress 120
+# artifact at results/120/
+```
+
+No code changes are required. The only prerequisite is live data from
+`clerk.house.gov` and `senate.gov` for the 120th Congress.
+
+---
+
+## 12. Extra Features
+
+_Features added after initial scope. Complete current Implementation Plan progress before starting these._
+
+| Feature | Description | Added On | Rationale |
+|---------|-------------|----------|-----------|
+| (none yet) | | | |

+ 134 - 0
analyze.py

@@ -0,0 +1,134 @@
+#!/usr/bin/env python3
+"""Pure analytical functions over the unified votes schema.
+
+No I/O. Given a list of parsed vote dicts (see parse.py) and a target
+member id + party, produce a metrics dict ready for rendering.
+
+Classification rules — see DOCUMENTATION.md §6.
+"""
+from collections import Counter, defaultdict
+
+# Lone-wolf defection threshold (max fellow same-party defectors).
+# Smaller chamber => tighter threshold.
+LONE_WOLF_THRESHOLD = {"house": 5, "senate": 3}
+
+def _norm_vote(v):
+    """Normalize Aye/No (procedural) to Yea/Nay."""
+    if v in ("Yea", "Aye"): return "Yea"
+    if v in ("Nay", "No"):  return "Nay"
+    return v  # Present, Not Voting, None, ""
+
+def _majority_position(party_totals):
+    y, n = party_totals["yea"], party_totals["nay"]
+    if y > n: return "Yea"
+    if n > y: return "Nay"
+    return "Split"
+
+def classify_vote(record, member_id):
+    """Return dict with: member_vote, member_vote_norm, r_pos, d_pos,
+    alignment, blocked. Single-vote analysis, no aggregation."""
+    raw = record["votes"].get(member_id)
+    norm = _norm_vote(raw)
+    r_pos = _majority_position(record["totals"]["R"])
+    d_pos = _majority_position(record["totals"]["D"])
+    if norm not in ("Yea", "Nay"):
+        return {"member_vote": raw, "member_vote_norm": norm,
+                "r_pos": r_pos, "d_pos": d_pos,
+                "alignment": "N/A: " + (raw or "absent"),
+                "blocked": None}
+    helped_r = (r_pos != "Split" and norm == r_pos)
+    helped_d = (d_pos != "Split" and norm == d_pos)
+    if helped_r and helped_d: align = "Helped Both"
+    elif helped_r:            align = "Helped Republicans"
+    elif helped_d:            align = "Helped Democrats"
+    else:                     align = "Helped Neither"
+    result = (record.get("result") or "").lower()
+    failed = any(s in result for s in ("fail", "reject", "not agreed", "not passed", "not invoked"))
+    blocked = None
+    if d_pos == "Yea" and failed and norm == "Nay" and r_pos != "Yea":
+        blocked = "Democrat"
+    if r_pos == "Yea" and failed and norm == "Nay" and d_pos != "Yea":
+        blocked = "Republican"
+    return {"member_vote": raw, "member_vote_norm": norm,
+            "r_pos": r_pos, "d_pos": d_pos,
+            "alignment": align, "blocked": blocked}
+
+def aggregate(records, member_id, member_party, chamber):
+    """records: iterable of parsed vote dicts. Returns the metrics dict
+    consumed by render.py."""
+    align_counts = Counter()
+    raw_vote_counts = Counter()
+    blocked_counts = Counter()
+    month_align = defaultdict(Counter)
+    rows = []
+    va_gop = vw_gop = va_dem = vw_dem = 0
+    lone_wolf = 0
+    threshold = LONE_WOLF_THRESHOLD.get(chamber, 5)
+    yeas = nays = nv = present = voting = 0
+    for rec in records:
+        cls = classify_vote(rec, member_id)
+        raw = cls["member_vote"]
+        norm = cls["member_vote_norm"]
+        align_counts[cls["alignment"]] += 1
+        raw_vote_counts[raw or "Absent"] += 1
+        if cls["blocked"]:
+            blocked_counts[cls["blocked"]] += 1
+        if rec.get("date"):
+            month_align[rec["date"][:7]][cls["alignment"]] += 1
+        # vote-distribution counters
+        if norm == "Yea": yeas += 1; voting += 1
+        elif norm == "Nay": nays += 1; voting += 1
+        elif raw == "Not Voting": nv += 1
+        elif raw == "Present": present += 1
+        # voted with/against each party majority
+        if norm in ("Yea","Nay"):
+            r_pos, d_pos = cls["r_pos"], cls["d_pos"]
+            if r_pos != "Split":
+                if norm == r_pos: vw_gop += 1
+                else: va_gop += 1
+            if d_pos != "Split":
+                if norm == d_pos: vw_dem += 1
+                else: va_dem += 1
+            # lone wolf vs own party
+            own_pos = r_pos if member_party == "R" else d_pos if member_party == "D" else None
+            if own_pos and own_pos != "Split" and norm != own_pos:
+                own_totals = rec["totals"]["R"] if member_party == "R" else rec["totals"]["D"]
+                defectors = own_totals["nay"] if own_pos == "Yea" else own_totals["yea"]
+                if defectors <= threshold:
+                    lone_wolf += 1
+        # row for the table
+        rows.append({
+            "y": rec.get("year") or "",
+            "r": rec.get("num") or 0,
+            "d": rec.get("date_raw") or rec.get("date") or "",
+            "ln": rec.get("bill") or "",
+            "q": rec.get("question") or "",
+            "ds": (rec.get("desc") or "")[:90],
+            "rs": rec.get("result") or "",
+            "m": raw or "",
+            "ry": rec["totals"]["R"]["yea"], "rn": rec["totals"]["R"]["nay"],
+            "dy": rec["totals"]["D"]["yea"], "dn": rec["totals"]["D"]["nay"],
+            "a": cls["alignment"],
+            "b": cls["blocked"] or "",
+        })
+    months_sorted = sorted(month_align.keys())
+    align_labels = ["Helped Republicans","Helped Democrats","Helped Both","Helped Neither"]
+    monthly = {lab: [month_align[m].get(lab, 0) for m in months_sorted]
+               for lab in align_labels}
+    total = len(rows)
+    return {
+        "chamber": chamber,
+        "total": total, "voting": voting,
+        "yeas": yeas, "nays": nays, "nv": nv, "present": present,
+        "alignment": dict(align_counts),
+        "member": dict(raw_vote_counts),
+        "blocked": dict(blocked_counts),
+        "months": months_sorted, "monthly": monthly,
+        "rows": rows,
+        "blocked_dem_count": blocked_counts.get("Democrat", 0),
+        "blocked_rep_count": blocked_counts.get("Republican", 0),
+        "lone_wolf": lone_wolf,
+        "voted_with_gop": vw_gop, "voted_against_gop": va_gop,
+        "voted_with_dem": vw_dem, "voted_against_dem": va_dem,
+        "lone_wolf_threshold": threshold,
+    }

+ 31 - 0
build_all.py

@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+"""Run the full pipeline end-to-end."""
+import argparse
+import subprocess
+import sys
+
+ap = argparse.ArgumentParser()
+ap.add_argument("--congress", type=int, default=119)
+ap.add_argument("--skip-fetch", action="store_true")
+ap.add_argument("--skip-enrich", action="store_true")
+args = ap.parse_args()
+
+C = str(args.congress)
+steps = []
+if not args.skip_fetch:
+    steps.append(["python3", "fetch.py", "--congress", C])
+steps.append(["python3", "parse.py", "--congress", C])
+if not args.skip_enrich:
+    steps.append(["python3", "enrich_roster.py", "--congress", C])
+    steps.append(["python3", "parse.py", "--congress", C])  # re-parse to merge enriched roster
+steps.append(["python3", "-m", "pytest", "tests/", "-q"])
+steps.append(["python3", "build_members.py", "--congress", C])
+steps.append(["python3", "build_app.py", "--congress", C])
+
+for s in steps:
+    print("==>", " ".join(s))
+    r = subprocess.run(s)
+    if r.returncode != 0:
+        sys.exit(r.returncode)
+
+print("build_all: done")

+ 220 - 0
build_app.py

@@ -0,0 +1,220 @@
+#!/usr/bin/env python3
+"""Build the embeddable dashboard artifact under results/<C>/.
+
+Wipes and recreates results/<C>/, copies templates + vendor + per-member
+data, stamps the manifest version into the HTML pages, and writes a
+README with embedding instructions.
+
+Usage: python3 build_app.py --congress 119
+"""
+import argparse
+import json
+import shutil
+import sys
+from datetime import datetime, timezone
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent
+
+TEMPLATE_FILES = [
+    "app.html",
+    "compare.html",
+    "ranking.html",
+    "app.js",
+    "compare.js",
+    "ranking.js",
+    "app.css",
+]
+VENDOR_FILES = [
+    "vendor/chart.umd.min.js",
+    "vendor/sortable.min.js",
+]
+
+
+def dir_size_mb(path: Path) -> float:
+    total = 0
+    for p in path.rglob("*"):
+        if p.is_file():
+            total += p.stat().st_size
+    return total / (1024 * 1024)
+
+
+def stamp_manifest_version(html_path: Path, version: str, generated_at: str, manifest: dict) -> None:
+    text = html_path.read_text(encoding="utf-8")
+    ver_tag = (
+        '<script type="application/json" id="polisci-manifest-version">'
+        + json.dumps({"version": version, "generated_at": generated_at}, separators=(",", ":"))
+        + "</script>"
+    )
+    # Inline the full manifest so the picker loads under file:// (per-member JSON
+    # still requires HTTP — but the app shell is interactive without a server).
+    manifest_tag = (
+        '<script type="application/json" id="polisci-manifest">'
+        + json.dumps(manifest, separators=(",", ":"))
+        + "</script>"
+    )
+    if "</head>" not in text:
+        raise SystemExit(f"build_app: no </head> in {html_path}")
+    text = text.replace("</head>", ver_tag + "\n" + manifest_tag + "\n</head>", 1)
+    html_path.write_text(text, encoding="utf-8")
+
+
+README_TEMPLATE = """# {congress_ord} Congress Voting Dashboard — Embeddable Artifact
+
+This directory is a self-contained dashboard for the {congress_ord} Congress.
+No external network requests at runtime; all data, charts, and vendor
+scripts ship in this directory.
+
+## Files
+
+- `app.html` — single-member dashboard
+- `compare.html` — multi-member comparison view
+- `app.js`, `compare.js`, `app.css` — application code
+- `vendor/chart.umd.min.js` — Chart.js 4.4.0
+- `vendor/sortable.min.js` — SortableJS 1.15.2
+- `data/manifest.json` — member index (~{member_count} entries)
+- `data/members/<id>.json` — per-member metrics (~80 KB each)
+
+## Embed modes
+
+### 1. Standalone
+Open `app.html` (or `compare.html`) directly.
+
+### 2. Iframe
+```html
+<iframe
+  src="https://your.host/path/to/app.html"
+  sandbox="allow-scripts allow-same-origin"
+  referrerpolicy="no-referrer"
+  style="width:100%;min-height:1200px;border:0"></iframe>
+```
+
+### 3. Inline (single host page)
+```html
+<link rel="stylesheet" href="https://your.host/path/to/app.css">
+<div id="polisci-root" data-base="https://your.host/path/to/data/"></div>
+<script src="https://your.host/path/to/vendor/chart.umd.min.js" defer></script>
+<script src="https://your.host/path/to/vendor/sortable.min.js" defer></script>
+<script src="https://your.host/path/to/app.js" defer></script>
+```
+
+All CSS is scoped under `#polisci-root` to avoid collisions with host styles.
+Override `data-base` to point at the data directory served from your host.
+
+## Recommended Content Security Policy
+
+```
+Content-Security-Policy: default-src 'self'; script-src 'self';
+  style-src 'self'; img-src 'self' data:; connect-src 'self';
+  frame-ancestors <your-domain>; base-uri 'none'; form-action 'none'
+```
+
+## Regenerating
+
+From the project root:
+```
+python3 fetch.py          --congress {congress}
+python3 parse.py          --congress {congress}
+python3 enrich_roster.py  --congress {congress}
+pytest tests/
+python3 build_members.py  --congress {congress}
+python3 build_app.py      --congress {congress}
+```
+
+Or the all-in-one:
+```
+python3 build_all.py --congress {congress}
+```
+
+## Provenance
+
+Each per-member JSON includes a `_meta` block with `schema_version`,
+`pipeline_version`, `classifier_hash` (SHA-256 of analyze.py),
+`data_snapshot_date`, and `source_xml_count`. See `DOCUMENTATION.md`
+in the source repository for full methodology.
+"""
+
+
+def ordinal(n: int) -> str:
+    if 11 <= (n % 100) <= 13:
+        suf = "th"
+    else:
+        suf = {1: "st", 2: "nd", 3: "rd"}.get(n % 10, "th")
+    return f"{n}{suf}"
+
+
+def main() -> int:
+    ap = argparse.ArgumentParser(description="Build embeddable dashboard artifact.")
+    ap.add_argument("--congress", type=int, default=119)
+    args = ap.parse_args()
+    congress = args.congress
+
+    template_dir = ROOT / "template"
+    data_dir = ROOT / "data" / str(congress)
+    manifest_path = data_dir / "manifest.json"
+    members_dir = data_dir / "members"
+    out_dir = ROOT / "results" / str(congress)
+
+    if not manifest_path.is_file():
+        print(
+            f"build_app: missing {manifest_path}; run build_members.py --congress {congress} first",
+            file=sys.stderr,
+        )
+        return 2
+
+    with manifest_path.open("r", encoding="utf-8") as f:
+        manifest = json.load(f)
+    version = manifest.get("version", "unknown")
+    member_count = len(manifest.get("members", []))
+
+    # Wipe + recreate
+    if out_dir.exists():
+        shutil.rmtree(out_dir)
+    out_dir.mkdir(parents=True, exist_ok=True)
+    print(f"build_app: cleaned and recreated {out_dir}/")
+
+    # Copy template files
+    for rel in TEMPLATE_FILES:
+        src = template_dir / rel
+        dst = out_dir / rel
+        dst.parent.mkdir(parents=True, exist_ok=True)
+        shutil.copy2(src, dst)
+
+    # Copy vendor files
+    for rel in VENDOR_FILES:
+        src = template_dir / rel
+        dst = out_dir / rel
+        dst.parent.mkdir(parents=True, exist_ok=True)
+        shutil.copy2(src, dst)
+
+    # Stamp manifest version + full inline manifest into HTML heads
+    generated_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
+    for html_name in ("app.html", "compare.html", "ranking.html"):
+        stamp_manifest_version(out_dir / html_name, version, generated_at, manifest)
+
+    # Copy manifest + members data
+    out_data = out_dir / "data"
+    out_data.mkdir(parents=True, exist_ok=True)
+    shutil.copy2(manifest_path, out_data / "manifest.json")
+    shutil.copytree(members_dir, out_data / "members", dirs_exist_ok=True)
+
+    # README
+    readme = README_TEMPLATE.format(
+        congress=congress,
+        congress_ord=ordinal(congress),
+        member_count=member_count,
+    )
+    (out_dir / "README.md").write_text(readme, encoding="utf-8")
+
+    # Methodology — copied verbatim from project root; linked from page footers
+    methodology_src = Path("Methodology.md")
+    if methodology_src.exists():
+        shutil.copy2(methodology_src, out_dir / "Methodology.md")
+
+    size_mb = dir_size_mb(out_dir)
+    print(f"build_app: results/{congress}/ ready ({member_count} members, {size_mb:.1f} MB)")
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())

+ 208 - 0
build_members.py

@@ -0,0 +1,208 @@
+#!/usr/bin/env python3
+"""Build per-member dashboard JSON + manifest + build_report for a Congress.
+
+Reads votes.jsonl + roster.json for both chambers, fans out analyze.aggregate
+across a multiprocessing pool, writes atomic per-member JSON files, then a
+manifest.json (picker payload) and build_report.json (run log).
+"""
+import argparse
+import datetime as _dt
+import hashlib
+import json
+import multiprocessing as mp
+import os
+import sys
+import time
+import traceback
+from pathlib import Path
+
+import analyze
+
+PIPELINE_VERSION = "1.0.0"
+SCHEMA_VERSION = 1
+
+_WORKER_RECORDS = None
+_WORKER_META = None
+
+
+def _init_worker(records, meta):
+    global _WORKER_RECORDS, _WORKER_META
+    _WORKER_RECORDS = records
+    _WORKER_META = meta
+
+
+def _worker(task):
+    mid, m = task
+    try:
+        chamber = (m.get("chamber") or "").lower()
+        recs = [r for r in _WORKER_RECORDS if r.get("chamber") == chamber]
+        metrics = analyze.aggregate(recs, mid, m.get("party"), chamber)
+        payload = {
+            "id": mid,
+            "name": m.get("full_name") or m.get("name"),
+            "party": m.get("party"),
+            "state": m.get("state"),
+            "chamber": m.get("chamber"),
+            "served_from": m.get("served_from"),
+            "served_to": m.get("served_to"),
+            "served_partial": bool(m.get("served_partial", False)),
+            "metrics": metrics,
+            "_meta": _WORKER_META,
+        }
+        if (m.get("chamber") or "").lower() == "house":
+            payload["district"] = m.get("district")
+        return payload
+    except Exception as e:
+        return {"_error": f"{type(e).__name__}: {e}\n{traceback.format_exc()}", "id": mid}
+
+
+def _load_jsonl(path):
+    out = []
+    with open(path, "r", encoding="utf-8") as f:
+        for line in f:
+            line = line.strip()
+            if line:
+                out.append(json.loads(line))
+    return out
+
+
+def _atomic_write_json(path: Path, obj):
+    tmp = path.with_suffix(path.suffix + ".tmp")
+    with open(tmp, "w", encoding="utf-8") as f:
+        json.dump(obj, f, separators=(",", ":"))
+    os.replace(tmp, path)
+
+
+def _chamber_letter(chamber):
+    return "H" if (chamber or "").lower() == "house" else "S"
+
+
+def main(argv=None):
+    ap = argparse.ArgumentParser(description="Build per-member dashboard JSON.")
+    ap.add_argument("--congress", type=int, default=119)
+    ap.add_argument("-v", "--verbose", action="store_true")
+    args = ap.parse_args(argv)
+
+    started = _dt.datetime.now(_dt.timezone.utc)
+    t0 = time.monotonic()
+
+    data_root = Path("data") / str(args.congress)
+    house_votes_p = data_root / "house" / "votes.jsonl"
+    senate_votes_p = data_root / "senate" / "votes.jsonl"
+    house_roster_p = data_root / "house" / "roster.json"
+    senate_roster_p = data_root / "senate" / "roster.json"
+
+    for p in (house_votes_p, senate_votes_p, house_roster_p, senate_roster_p):
+        if not p.exists():
+            print(f"build_members: missing input {p}", file=sys.stderr)
+            return 2
+
+    house_recs = _load_jsonl(house_votes_p)
+    senate_recs = _load_jsonl(senate_votes_p)
+    records = house_recs + senate_recs
+
+    with open(house_roster_p, "r", encoding="utf-8") as f:
+        house_roster = json.load(f)
+    with open(senate_roster_p, "r", encoding="utf-8") as f:
+        senate_roster = json.load(f)
+    roster = {}
+    roster.update(house_roster)
+    roster.update(senate_roster)
+
+    analyze_path = Path(analyze.__file__)
+    classifier_hash = hashlib.sha256(analyze_path.read_bytes()).hexdigest()
+    snapshot_date = started.date().isoformat()
+    meta = {
+        "schema_version": SCHEMA_VERSION,
+        "pipeline_version": PIPELINE_VERSION,
+        "classifier_hash": classifier_hash,
+        "data_snapshot_date": snapshot_date,
+        "source_xml_count": {"house": len(house_recs), "senate": len(senate_recs)},
+    }
+
+    out_dir = data_root / "members"
+    if out_dir.exists():
+        for f in out_dir.glob("*.json"):
+            f.unlink()
+    out_dir.mkdir(parents=True, exist_ok=True)
+
+    tasks = list(roster.items())
+    n_procs = min(8, os.cpu_count() or 4)
+
+    succeeded = 0
+    failures = []
+    members_meta = []
+
+    with mp.Pool(processes=n_procs, initializer=_init_worker, initargs=(records, meta)) as pool:
+        for result in pool.imap_unordered(_worker, tasks, chunksize=8):
+            if "_error" in result:
+                failures.append({"id": result.get("id"), "error": result["_error"]})
+                if args.verbose:
+                    print(f"FAIL {result.get('id')}: {result['_error'].splitlines()[0]}", file=sys.stderr)
+                continue
+            mid = result["id"]
+            _atomic_write_json(out_dir / f"{mid}.json", result)
+            entry = {
+                "id": mid,
+                "n": result.get("name") or mid,
+                "p": result.get("party"),
+                "s": result.get("state"),
+                "c": _chamber_letter(result.get("chamber")),
+            }
+            if entry["c"] == "H" and result.get("district") is not None:
+                entry["d"] = result["district"]
+            if result.get("served_partial"):
+                entry["sp"] = True
+            mx = result.get("metrics") or {}
+            entry["k"] = {
+                "total": mx.get("total", 0),
+                "voting": mx.get("voting", 0),
+                "yeas": mx.get("yeas", 0),
+                "nays": mx.get("nays", 0),
+                "voted_with_gop": mx.get("voted_with_gop", 0),
+                "voted_with_dem": mx.get("voted_with_dem", 0),
+                "voted_against_gop": mx.get("voted_against_gop", 0),
+                "voted_against_dem": mx.get("voted_against_dem", 0),
+                "lone_wolf": mx.get("lone_wolf", 0),
+            }
+            members_meta.append(entry)
+            succeeded += 1
+            if args.verbose:
+                print(f"OK {mid} ({entry['n']})")
+
+    members_meta.sort(key=lambda e: (e["n"] or "").lower())
+
+    manifest = {
+        "version": f"{PIPELINE_VERSION}+{snapshot_date}",
+        "generated_at": started.isoformat().replace("+00:00", "Z"),
+        "congress": args.congress,
+        "members": members_meta,
+    }
+    _atomic_write_json(data_root / "manifest.json", manifest)
+
+    finished = _dt.datetime.now(_dt.timezone.utc)
+    duration = round(time.monotonic() - t0, 2)
+    total = len(tasks)
+    report = {
+        "started_at": started.isoformat().replace("+00:00", "Z"),
+        "finished_at": finished.isoformat().replace("+00:00", "Z"),
+        "duration_seconds": duration,
+        "members_total": total,
+        "members_succeeded": succeeded,
+        "members_failed": len(failures),
+        "failures": failures,
+        "warnings": [],
+        "source": {
+            "house_votes": len(house_recs),
+            "senate_votes": len(senate_recs),
+            "roster_entries": total,
+        },
+    }
+    _atomic_write_json(data_root / "build_report.json", report)
+
+    print(f"build_members: {succeeded}/{total} OK in {duration}s — manifest at {data_root / 'manifest.json'}")
+    return 0 if not failures else 1
+
+
+if __name__ == "__main__":
+    sys.exit(main())

+ 4948 - 0
data/119/house/roster.json

@@ -0,0 +1,4948 @@
+{
+  "A000055": {
+    "bioguide": "A000055",
+    "chamber": "house",
+    "full_name": "Robert B. Aderholt",
+    "name": "Aderholt",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/a000055_200.jpg",
+    "served_from": "1997-01-03",
+    "served_partial": false,
+    "state": "AL"
+  },
+  "A000148": {
+    "bioguide": "A000148",
+    "chamber": "house",
+    "full_name": "Jake Auchincloss",
+    "name": "Auchincloss",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67817e391f9ad6ea6fb1ebda_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "MA"
+  },
+  "A000369": {
+    "bioguide": "A000369",
+    "chamber": "house",
+    "full_name": "Mark E. Amodei",
+    "name": "Amodei (NV)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/a000369_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "NV"
+  },
+  "A000370": {
+    "bioguide": "A000370",
+    "chamber": "house",
+    "full_name": "Alma S. Adams",
+    "name": "Adams",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/a000370_200.jpg",
+    "served_from": "2014-01-03",
+    "served_partial": false,
+    "state": "NC"
+  },
+  "A000371": {
+    "bioguide": "A000371",
+    "chamber": "house",
+    "full_name": "Pete Aguilar",
+    "name": "Aguilar",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/a000371_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "A000372": {
+    "bioguide": "A000372",
+    "chamber": "house",
+    "full_name": "Rick W. Allen",
+    "name": "Allen",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/a000372_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "GA"
+  },
+  "A000375": {
+    "bioguide": "A000375",
+    "chamber": "house",
+    "full_name": "Jodey C. Arrington",
+    "name": "Arrington",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_tx_19_arrington_jodey_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "A000379": {
+    "bioguide": "A000379",
+    "chamber": "house",
+    "full_name": "Mark Alford",
+    "name": "Alford",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/a000379_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "MO"
+  },
+  "A000380": {
+    "bioguide": "A000380",
+    "chamber": "house",
+    "full_name": "Gabe Amo",
+    "name": "Amo",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/669ace45fa2fb0d731226768_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "RI"
+  },
+  "A000381": {
+    "bioguide": "A000381",
+    "chamber": "house",
+    "full_name": "Yassamin Ansari",
+    "name": "Ansari",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67741fc30b34857ecc90902e_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "AZ"
+  },
+  "B000490": {
+    "bioguide": "B000490",
+    "chamber": "house",
+    "full_name": "Sanford D. Bishop",
+    "name": "Bishop",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b000490_200.jpg",
+    "served_from": "1993-01-03",
+    "served_partial": false,
+    "state": "GA"
+  },
+  "B000668": {
+    "bioguide": "B000668",
+    "chamber": "house",
+    "full_name": "Cliff Bentz",
+    "name": "Bentz",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b000668_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "OR"
+  },
+  "B000740": {
+    "bioguide": "B000740",
+    "chamber": "house",
+    "full_name": "Stephanie I. Bice",
+    "name": "Bice",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b000740_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "OK"
+  },
+  "B000825": {
+    "bioguide": "B000825",
+    "chamber": "house",
+    "full_name": "Lauren Boebert",
+    "name": "Boebert",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b000825_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "CO"
+  },
+  "B001257": {
+    "bioguide": "B001257",
+    "chamber": "house",
+    "full_name": "Gus M. Bilirakis",
+    "name": "Bilirakis",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/117_rp_fl_12_bilirakis_gus_200.jpg",
+    "served_from": "2007-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "B001260": {
+    "bioguide": "B001260",
+    "chamber": "house",
+    "full_name": "Vern Buchanan",
+    "name": "Buchanan",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001260_200.jpg",
+    "served_from": "2007-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "B001278": {
+    "bioguide": "B001278",
+    "chamber": "house",
+    "full_name": "Suzanne Bonamici",
+    "name": "Bonamici",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001278_200.jpg",
+    "served_from": "2012-01-03",
+    "served_partial": false,
+    "state": "OR"
+  },
+  "B001281": {
+    "bioguide": "B001281",
+    "chamber": "house",
+    "full_name": "Joyce Beatty",
+    "name": "Beatty",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001281_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "OH"
+  },
+  "B001282": {
+    "bioguide": "B001282",
+    "chamber": "house",
+    "full_name": "Andy Barr",
+    "name": "Barr",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001282_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "KY"
+  },
+  "B001285": {
+    "bioguide": "B001285",
+    "chamber": "house",
+    "full_name": "Julia Brownley",
+    "name": "Brownley",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/68000188f22eaf56065817e8_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "B001287": {
+    "bioguide": "B001287",
+    "chamber": "house",
+    "full_name": "Ami Bera",
+    "name": "Bera",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001287_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "B001291": {
+    "bioguide": "B001291",
+    "chamber": "house",
+    "full_name": "Brian Babin",
+    "name": "Babin",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001291_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "B001292": {
+    "bioguide": "B001292",
+    "chamber": "house",
+    "full_name": "Donald S. Beyer",
+    "name": "Beyer",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001292_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "VA"
+  },
+  "B001295": {
+    "bioguide": "B001295",
+    "chamber": "house",
+    "full_name": "Mike Bost",
+    "name": "Bost",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001295_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "IL"
+  },
+  "B001296": {
+    "bioguide": "B001296",
+    "chamber": "house",
+    "full_name": "Brendan F. Boyle",
+    "name": "Boyle (PA)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/69b30d648b7b84825b02d362_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "PA"
+  },
+  "B001298": {
+    "bioguide": "B001298",
+    "chamber": "house",
+    "full_name": "Don Bacon",
+    "name": "Bacon",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_ne_2_bacon_don_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "NE"
+  },
+  "B001300": {
+    "bioguide": "B001300",
+    "chamber": "house",
+    "full_name": "Nanette Diaz Barrag\u00e1n",
+    "name": "Barragan",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001300_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "B001301": {
+    "bioguide": "B001301",
+    "chamber": "house",
+    "full_name": "Jack Bergman",
+    "name": "Bergman",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001301_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "MI"
+  },
+  "B001302": {
+    "bioguide": "B001302",
+    "chamber": "house",
+    "full_name": "Andy Biggs",
+    "name": "Biggs (AZ)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001302_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "AZ"
+  },
+  "B001306": {
+    "bioguide": "B001306",
+    "chamber": "house",
+    "full_name": "Troy Balderson",
+    "name": "Balderson",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_oh_12_balderson_troy_200.jpg",
+    "served_from": "2018-01-03",
+    "served_partial": false,
+    "state": "OH"
+  },
+  "B001307": {
+    "bioguide": "B001307",
+    "chamber": "house",
+    "full_name": "James R. Baird",
+    "name": "Baird",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001307_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "IN"
+  },
+  "B001309": {
+    "bioguide": "B001309",
+    "chamber": "house",
+    "full_name": "Tim Burchett",
+    "name": "Burchett",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001309_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "TN"
+  },
+  "B001313": {
+    "bioguide": "B001313",
+    "chamber": "house",
+    "full_name": "Shontel M. Brown",
+    "name": "Brown",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001313_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "OH"
+  },
+  "B001314": {
+    "bioguide": "B001314",
+    "chamber": "house",
+    "full_name": "Aaron Bean",
+    "name": "Bean (FL)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001314_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "B001315": {
+    "bioguide": "B001315",
+    "chamber": "house",
+    "full_name": "Nikki Budzinski",
+    "name": "Budzinski",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001315_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "IL"
+  },
+  "B001316": {
+    "bioguide": "B001316",
+    "chamber": "house",
+    "full_name": "Eric Burlison",
+    "name": "Burlison",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001316_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "MO"
+  },
+  "B001317": {
+    "bioguide": "B001317",
+    "chamber": "house",
+    "full_name": "Josh Brecheen",
+    "name": "Brecheen",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001317_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "OK"
+  },
+  "B001318": {
+    "bioguide": "B001318",
+    "chamber": "house",
+    "full_name": "Becca Balint",
+    "name": "Balint",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001318_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "VT"
+  },
+  "B001321": {
+    "bioguide": "B001321",
+    "chamber": "house",
+    "full_name": "Tom Barrett",
+    "name": "Barrett",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/6774207d0b34857ecc909034_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "MI"
+  },
+  "B001322": {
+    "bioguide": "B001322",
+    "chamber": "house",
+    "full_name": "Michael Baumgartner",
+    "name": "Baumgartner",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/6774212e0b34857ecc90903a_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "WA"
+  },
+  "B001323": {
+    "bioguide": "B001323",
+    "chamber": "house",
+    "full_name": "Nicholas J. Begich",
+    "name": "Begich",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/6774217e0b34857ecc909040_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "AK"
+  },
+  "B001324": {
+    "bioguide": "B001324",
+    "chamber": "house",
+    "full_name": "Wesley Bell",
+    "name": "Bell",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677422240b34857ecc90904a_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "MO"
+  },
+  "B001325": {
+    "bioguide": "B001325",
+    "chamber": "house",
+    "full_name": "Sheri Biggs",
+    "name": "Biggs (SC)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677422990b34857ecc909052_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "SC"
+  },
+  "B001326": {
+    "bioguide": "B001326",
+    "chamber": "house",
+    "full_name": "Janelle S. Bynum",
+    "name": "Bynum",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677423150b34857ecc909059_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "OR"
+  },
+  "B001327": {
+    "bioguide": "B001327",
+    "chamber": "house",
+    "full_name": "Robert P. Bresnahan",
+    "name": "Bresnahan",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/6774236f0b34857ecc90905f_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "PA"
+  },
+  "C000059": {
+    "bioguide": "C000059",
+    "chamber": "house",
+    "full_name": "Ken Calvert",
+    "name": "Calvert",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c000059_200.jpg",
+    "served_from": "1993-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "C000537": {
+    "bioguide": "C000537",
+    "chamber": "house",
+    "full_name": "James E. Clyburn",
+    "name": "Clyburn",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c000537_200.jpg",
+    "served_from": "1993-01-03",
+    "served_partial": false,
+    "state": "SC"
+  },
+  "C001039": {
+    "bioguide": "C001039",
+    "chamber": "house",
+    "full_name": "Kat Cammack",
+    "name": "Cammack",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001039_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "C001051": {
+    "bioguide": "C001051",
+    "chamber": "house",
+    "full_name": "John R. Carter",
+    "name": "Carter (TX)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001051_200.jpg",
+    "served_from": "2003-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "C001053": {
+    "bioguide": "C001053",
+    "chamber": "house",
+    "full_name": "Tom Cole",
+    "name": "Cole",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001053_200.jpg",
+    "served_from": "2003-01-03",
+    "served_partial": false,
+    "state": "OK"
+  },
+  "C001055": {
+    "bioguide": "C001055",
+    "chamber": "house",
+    "full_name": "Ed Case",
+    "name": "Case",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001055_200.jpg",
+    "served_from": "2002-01-03",
+    "served_partial": false,
+    "state": "HI"
+  },
+  "C001059": {
+    "bioguide": "C001059",
+    "chamber": "house",
+    "full_name": "Jim Costa",
+    "name": "Costa",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6973c909ded7781a35cc1fd7_200.jpg",
+    "served_from": "2005-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "C001061": {
+    "bioguide": "C001061",
+    "chamber": "house",
+    "full_name": "Emanuel Cleaver",
+    "name": "Cleaver",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_mo_5_cleaver_emanuel_200.jpg",
+    "served_from": "2005-01-03",
+    "served_partial": false,
+    "state": "MO"
+  },
+  "C001063": {
+    "bioguide": "C001063",
+    "chamber": "house",
+    "full_name": "Henry Cuellar",
+    "name": "Cuellar",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_tx_28_cuellar_henry_200.jpg",
+    "served_from": "2005-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "C001066": {
+    "bioguide": "C001066",
+    "chamber": "house",
+    "full_name": "Kathy Castor",
+    "name": "Castor (FL)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001066_200.jpg",
+    "served_from": "2007-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "C001067": {
+    "bioguide": "C001067",
+    "chamber": "house",
+    "full_name": "Yvette D. Clarke",
+    "name": "Clarke (NY)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/674dfc6b5c48ff736e6e1762_200.jpg",
+    "served_from": "2007-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "C001068": {
+    "bioguide": "C001068",
+    "chamber": "house",
+    "full_name": "Steve Cohen",
+    "name": "Cohen",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001068_200.jpg",
+    "served_from": "2007-01-03",
+    "served_partial": false,
+    "state": "TN"
+  },
+  "C001069": {
+    "bioguide": "C001069",
+    "chamber": "house",
+    "full_name": "Joe Courtney",
+    "name": "Courtney",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001069_200.jpg",
+    "served_from": "2007-01-03",
+    "served_partial": false,
+    "state": "CT"
+  },
+  "C001072": {
+    "bioguide": "C001072",
+    "chamber": "house",
+    "full_name": "Andr\u00e9 Carson",
+    "name": "Carson",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001072_200.jpg",
+    "served_from": "2008-01-03",
+    "served_partial": false,
+    "state": "IN"
+  },
+  "C001078": {
+    "bioguide": "C001078",
+    "chamber": "house",
+    "full_name": "Gerald E. Connolly",
+    "name": "Connolly",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001078_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "served_to": "2025-01-03",
+    "state": "VA"
+  },
+  "C001080": {
+    "bioguide": "C001080",
+    "chamber": "house",
+    "full_name": "Judy Chu",
+    "name": "Chu",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001080_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "C001087": {
+    "bioguide": "C001087",
+    "chamber": "house",
+    "full_name": "Eric A. \"Rick\" Crawford",
+    "name": "Crawford",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/6916065185b49de40f52cad4_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "AR"
+  },
+  "C001091": {
+    "bioguide": "C001091",
+    "chamber": "house",
+    "full_name": "Joaquin Castro",
+    "name": "Castro (TX)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/69d7f5b07304cef7a0c977b1_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "C001101": {
+    "bioguide": "C001101",
+    "chamber": "house",
+    "full_name": "Katherine M. Clark",
+    "name": "Clark (MA)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001101_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "MA"
+  },
+  "C001103": {
+    "bioguide": "C001103",
+    "chamber": "house",
+    "full_name": "Earl L. \"Buddy\" Carter",
+    "name": "Carter (GA)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001103_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "GA"
+  },
+  "C001108": {
+    "bioguide": "C001108",
+    "chamber": "house",
+    "full_name": "James Comer",
+    "name": "Comer",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001108_200.jpg",
+    "served_from": "2016-01-03",
+    "served_partial": false,
+    "state": "KY"
+  },
+  "C001110": {
+    "bioguide": "C001110",
+    "chamber": "house",
+    "full_name": "J. Luis Correa",
+    "name": "Correa",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_ca_46_correa_j_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "C001112": {
+    "bioguide": "C001112",
+    "chamber": "house",
+    "full_name": "Salud O. Carbajal",
+    "name": "Carbajal",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_ca_24_carbajal_salud_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "C001115": {
+    "bioguide": "C001115",
+    "chamber": "house",
+    "full_name": "Michael Cloud",
+    "name": "Cloud",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_tx_27_cloud_michael_200.jpg",
+    "served_from": "2018-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "C001116": {
+    "bioguide": "C001116",
+    "chamber": "house",
+    "full_name": "Andrew S. Clyde",
+    "name": "Clyde",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001116_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "GA"
+  },
+  "C001117": {
+    "bioguide": "C001117",
+    "chamber": "house",
+    "full_name": "Sean Casten",
+    "name": "Casten",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001117_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "IL"
+  },
+  "C001118": {
+    "bioguide": "C001118",
+    "chamber": "house",
+    "full_name": "Ben Cline",
+    "name": "Cline",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/69825a92eb30d32718993747_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "VA"
+  },
+  "C001119": {
+    "bioguide": "C001119",
+    "chamber": "house",
+    "full_name": "Angie Craig",
+    "name": "Craig",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001119_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "MN"
+  },
+  "C001120": {
+    "bioguide": "C001120",
+    "chamber": "house",
+    "full_name": "Dan Crenshaw",
+    "name": "Crenshaw",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/698a13d0a7af728d4e6c2038_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "C001121": {
+    "bioguide": "C001121",
+    "chamber": "house",
+    "full_name": "Jason Crow",
+    "name": "Crow",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/69d7f4167304cef7a0c9779f_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "CO"
+  },
+  "C001123": {
+    "bioguide": "C001123",
+    "chamber": "house",
+    "full_name": "Gilbert Ray Cisneros",
+    "name": "Cisneros",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6807d8d63e52ea7df920ef05_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "C001125": {
+    "bioguide": "C001125",
+    "chamber": "house",
+    "full_name": "Troy A. Carter",
+    "name": "Carter (LA)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001125_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "LA"
+  },
+  "C001126": {
+    "bioguide": "C001126",
+    "chamber": "house",
+    "full_name": "Mike Carey",
+    "name": "Carey",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001126_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "OH"
+  },
+  "C001127": {
+    "bioguide": "C001127",
+    "chamber": "house",
+    "full_name": "Sheila Cherfilus-McCormick",
+    "name": "Cherfilus-McCormick",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001127_200.jpg",
+    "served_from": "2022-01-03",
+    "served_partial": false,
+    "served_to": "2026-01-03",
+    "state": "FL"
+  },
+  "C001129": {
+    "bioguide": "C001129",
+    "chamber": "house",
+    "full_name": "Mike Collins",
+    "name": "Collins",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001129_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "GA"
+  },
+  "C001130": {
+    "bioguide": "C001130",
+    "chamber": "house",
+    "full_name": "Jasmine Crockett",
+    "name": "Crockett",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001130_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "C001131": {
+    "bioguide": "C001131",
+    "chamber": "house",
+    "full_name": "Greg Casar",
+    "name": "Casar",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001131_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "C001132": {
+    "bioguide": "C001132",
+    "chamber": "house",
+    "full_name": "Elijah Crane",
+    "name": "Crane",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001132_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "AZ"
+  },
+  "C001133": {
+    "bioguide": "C001133",
+    "chamber": "house",
+    "full_name": "Juan Ciscomani",
+    "name": "Ciscomani",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001133_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "AZ"
+  },
+  "C001136": {
+    "bioguide": "C001136",
+    "chamber": "house",
+    "full_name": "Herbert C. Conaway",
+    "name": "Conaway",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6774243a0b34857ecc90906b_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "NJ"
+  },
+  "C001137": {
+    "bioguide": "C001137",
+    "chamber": "house",
+    "full_name": "Jeff Crank",
+    "name": "Crank",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677424810b34857ecc909071_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "CO"
+  },
+  "D000032": {
+    "bioguide": "D000032",
+    "chamber": "house",
+    "full_name": "Byron Donalds",
+    "name": "Donalds",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/d000032_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "D000096": {
+    "bioguide": "D000096",
+    "chamber": "house",
+    "full_name": "Danny K. Davis",
+    "name": "Davis (IL)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_il_7_davis_danny_200.jpg",
+    "served_from": "1997-01-03",
+    "served_partial": false,
+    "state": "IL"
+  },
+  "D000197": {
+    "bioguide": "D000197",
+    "chamber": "house",
+    "full_name": "Diana DeGette",
+    "name": "DeGette",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_co_1_degette_diana_200.jpg",
+    "served_from": "1997-01-03",
+    "served_partial": false,
+    "state": "CO"
+  },
+  "D000216": {
+    "bioguide": "D000216",
+    "chamber": "house",
+    "full_name": "Rosa L. DeLauro",
+    "name": "DeLauro",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_ct_3_delauro_rosa_200.jpg",
+    "served_from": "1991-01-03",
+    "served_partial": false,
+    "state": "CT"
+  },
+  "D000230": {
+    "bioguide": "D000230",
+    "chamber": "house",
+    "full_name": "Donald G. Davis",
+    "name": "Davis (NC)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/d000230_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "NC"
+  },
+  "D000399": {
+    "bioguide": "D000399",
+    "chamber": "house",
+    "full_name": "Lloyd Doggett",
+    "name": "Doggett",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/d000399_200.jpg",
+    "served_from": "1995-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "D000530": {
+    "bioguide": "D000530",
+    "chamber": "house",
+    "full_name": "Christopher R. Deluzio",
+    "name": "Deluzio",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/d000530_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "PA"
+  },
+  "D000594": {
+    "bioguide": "D000594",
+    "chamber": "house",
+    "full_name": "Monica De La Cruz",
+    "name": "De La Cruz",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/66d9fd54f440bf1dc174fbf6_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "D000600": {
+    "bioguide": "D000600",
+    "chamber": "house",
+    "full_name": "Mario Diaz-Balart",
+    "name": "Diaz-Balart",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_fl_25_diazbalart_mario_200.jpg",
+    "served_from": "2003-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "D000616": {
+    "bioguide": "D000616",
+    "chamber": "house",
+    "full_name": "Scott DesJarlais",
+    "name": "DesJarlais",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/d000616_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "TN"
+  },
+  "D000617": {
+    "bioguide": "D000617",
+    "chamber": "house",
+    "full_name": "Suzan K. DelBene",
+    "name": "DelBene",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/d000617_200.jpg",
+    "served_from": "2012-01-03",
+    "served_partial": false,
+    "state": "WA"
+  },
+  "D000623": {
+    "bioguide": "D000623",
+    "chamber": "house",
+    "full_name": "Mark DeSaulnier",
+    "name": "DeSaulnier",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_ca_11_desaulnier_mark_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "D000624": {
+    "bioguide": "D000624",
+    "chamber": "house",
+    "full_name": "Debbie Dingell",
+    "name": "Dingell",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/d000624_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "MI"
+  },
+  "D000626": {
+    "bioguide": "D000626",
+    "chamber": "house",
+    "full_name": "Warren Davidson",
+    "name": "Davidson",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_oh_8_davidson_warren_200.jpg",
+    "served_from": "2016-01-03",
+    "served_partial": false,
+    "state": "OH"
+  },
+  "D000628": {
+    "bioguide": "D000628",
+    "chamber": "house",
+    "full_name": "Neal P. Dunn",
+    "name": "Dunn (FL)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_fl_2_dunn_neal_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "D000629": {
+    "bioguide": "D000629",
+    "chamber": "house",
+    "full_name": "Sharice Davids",
+    "name": "Davids (KS)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/d000629_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "KS"
+  },
+  "D000631": {
+    "bioguide": "D000631",
+    "chamber": "house",
+    "full_name": "Madeleine Dean",
+    "name": "Dean (PA)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/d000631_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "PA"
+  },
+  "D000634": {
+    "bioguide": "D000634",
+    "chamber": "house",
+    "full_name": "Troy Downing",
+    "name": "Downing",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677424da0b34857ecc909077_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "MT"
+  },
+  "D000635": {
+    "bioguide": "D000635",
+    "chamber": "house",
+    "full_name": "Maxine Dexter",
+    "name": "Dexter",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677425260b34857ecc90907d_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "OR"
+  },
+  "E000071": {
+    "bioguide": "E000071",
+    "chamber": "house",
+    "full_name": "Jake Ellzey",
+    "name": "Ellzey",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/e000071_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "E000235": {
+    "bioguide": "E000235",
+    "chamber": "house",
+    "full_name": "Mike Ezell",
+    "name": "Ezell",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/e000235_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "MS"
+  },
+  "E000246": {
+    "bioguide": "E000246",
+    "chamber": "house",
+    "full_name": "Chuck Edwards",
+    "name": "Edwards",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/e000246_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "NC"
+  },
+  "E000294": {
+    "bioguide": "E000294",
+    "chamber": "house",
+    "full_name": "Tom Emmer",
+    "name": "Emmer",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/e000294_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "MN"
+  },
+  "E000296": {
+    "bioguide": "E000296",
+    "chamber": "house",
+    "full_name": "Dwight Evans",
+    "name": "Evans (PA)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_pa_2_evans_dwight_200.jpg",
+    "served_from": "2016-01-03",
+    "served_partial": false,
+    "state": "PA"
+  },
+  "E000297": {
+    "bioguide": "E000297",
+    "chamber": "house",
+    "full_name": "Adriano Espaillat",
+    "name": "Espaillat",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/e000297_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "E000298": {
+    "bioguide": "E000298",
+    "chamber": "house",
+    "full_name": "Ron Estes",
+    "name": "Estes",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/e000298_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "KS"
+  },
+  "E000299": {
+    "bioguide": "E000299",
+    "chamber": "house",
+    "full_name": "Veronica Escobar",
+    "name": "Escobar",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/e000299_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "E000300": {
+    "bioguide": "E000300",
+    "chamber": "house",
+    "full_name": "Gabe Evans",
+    "name": "Evans (CO)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677425730b34857ecc909083_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "CO"
+  },
+  "E000301": {
+    "bioguide": "E000301",
+    "chamber": "house",
+    "full_name": "Sarah Elfreth",
+    "name": "Elfreth",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677425dc0b34857ecc909089_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "MD"
+  },
+  "F000110": {
+    "bioguide": "F000110",
+    "chamber": "house",
+    "full_name": "Cleo Fields",
+    "name": "Fields",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677426250b34857ecc90908f_200.jpg",
+    "served_from": "1993-01-03",
+    "served_partial": false,
+    "state": "LA"
+  },
+  "F000246": {
+    "bioguide": "F000246",
+    "chamber": "house",
+    "full_name": "Pat Fallon",
+    "name": "Fallon",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/f000246_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "F000446": {
+    "bioguide": "F000446",
+    "chamber": "house",
+    "full_name": "Randy Feenstra",
+    "name": "Feenstra",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/f000446_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "IA"
+  },
+  "F000450": {
+    "bioguide": "F000450",
+    "chamber": "house",
+    "full_name": "Virginia Foxx",
+    "name": "Foxx",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_nc_5_foxx_virginia_200.jpg",
+    "served_from": "2005-01-03",
+    "served_partial": false,
+    "state": "NC"
+  },
+  "F000454": {
+    "bioguide": "F000454",
+    "chamber": "house",
+    "full_name": "Bill Foster",
+    "name": "Foster",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/f000454_200.jpg",
+    "served_from": "2008-01-03",
+    "served_partial": false,
+    "state": "IL"
+  },
+  "F000459": {
+    "bioguide": "F000459",
+    "chamber": "house",
+    "full_name": "Charles J. \"Chuck\" Fleischmann",
+    "name": "Fleischmann",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/f000459_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "TN"
+  },
+  "F000462": {
+    "bioguide": "F000462",
+    "chamber": "house",
+    "full_name": "Lois Frankel",
+    "name": "Frankel, Lois",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/f000462_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "F000466": {
+    "bioguide": "F000466",
+    "chamber": "house",
+    "full_name": "Brian K. Fitzpatrick",
+    "name": "Fitzpatrick",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_pa_1_fitzpatrick_brian_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "PA"
+  },
+  "F000468": {
+    "bioguide": "F000468",
+    "chamber": "house",
+    "full_name": "Lizzie Fletcher",
+    "name": "Fletcher",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67813f931f9ad6ea6fb1eb6f_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "F000469": {
+    "bioguide": "F000469",
+    "chamber": "house",
+    "full_name": "Russ Fulcher",
+    "name": "Fulcher",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/f000469_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "ID"
+  },
+  "F000470": {
+    "bioguide": "F000470",
+    "chamber": "house",
+    "full_name": "Michelle Fischbach",
+    "name": "Fischbach",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/f000470_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "MN"
+  },
+  "F000471": {
+    "bioguide": "F000471",
+    "chamber": "house",
+    "full_name": "Scott Fitzgerald",
+    "name": "Fitzgerald",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/f000471_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "WI"
+  },
+  "F000472": {
+    "bioguide": "F000472",
+    "chamber": "house",
+    "full_name": "Scott Franklin",
+    "name": "Franklin, Scott",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/f000472_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "F000474": {
+    "bioguide": "F000474",
+    "chamber": "house",
+    "full_name": "Mike Flood",
+    "name": "Flood",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67b4de4a61bd80d04553b0a5_200.jpg",
+    "served_from": "2022-01-03",
+    "served_partial": false,
+    "state": "NE"
+  },
+  "F000475": {
+    "bioguide": "F000475",
+    "chamber": "house",
+    "full_name": "Brad Finstad",
+    "name": "Finstad",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/f000475_200.jpg",
+    "served_from": "2022-01-03",
+    "served_partial": false,
+    "state": "MN"
+  },
+  "F000476": {
+    "bioguide": "F000476",
+    "chamber": "house",
+    "full_name": "Maxwell Frost",
+    "name": "Frost",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/69a71e9a55f11d8282dde7a0_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "F000477": {
+    "bioguide": "F000477",
+    "chamber": "house",
+    "full_name": "Valerie P. Foushee",
+    "name": "Foushee",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/68122e57246d1b6bd8d9f6ab_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "NC"
+  },
+  "F000478": {
+    "bioguide": "F000478",
+    "chamber": "house",
+    "full_name": "Russell Fry",
+    "name": "Fry",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/f000478_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "SC"
+  },
+  "F000480": {
+    "bioguide": "F000480",
+    "chamber": "house",
+    "full_name": "Vince Fong",
+    "name": "Fong",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/669ff04f5d19788d1f2034aa_200.jpg",
+    "served_from": "2024-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "F000481": {
+    "bioguide": "F000481",
+    "chamber": "house",
+    "full_name": "Shomari Figures",
+    "name": "Figures",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/68013eaa4e51529406f18e42_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "AL"
+  },
+  "F000482": {
+    "bioguide": "F000482",
+    "chamber": "house",
+    "full_name": "Julie Fedorchak",
+    "name": "Fedorchak",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677426c20b34857ecc90909b_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "ND"
+  },
+  "F000483": {
+    "bioguide": "F000483",
+    "chamber": "house",
+    "full_name": "Laura Friedman",
+    "name": "Friedman",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6a0f0dcb69b12040cf77e337_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "F000484": {
+    "bioguide": "F000484",
+    "chamber": "house",
+    "full_name": "Randy Fine",
+    "name": "Fine",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67efda8c1b05a5a598f7fde0_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "F000485": {
+    "bioguide": "F000485",
+    "chamber": "house",
+    "full_name": "Clay Fuller",
+    "name": "Fuller",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/69eb95e5277642362f6112c5_200.jpg",
+    "served_from": "2026-01-03",
+    "served_partial": false,
+    "state": "GA"
+  },
+  "G000546": {
+    "bioguide": "G000546",
+    "chamber": "house",
+    "full_name": "Sam Graves",
+    "name": "Graves",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000546_200.jpg",
+    "served_from": "2001-01-03",
+    "served_partial": false,
+    "state": "MO"
+  },
+  "G000551": {
+    "bioguide": "G000551",
+    "chamber": "house",
+    "full_name": "Ra\u00fal M. Grijalva",
+    "name": "Grijalva",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/g000551_200.jpg",
+    "served_from": "2003-01-03",
+    "served_partial": false,
+    "served_to": "2025-01-03",
+    "state": "AZ"
+  },
+  "G000553": {
+    "bioguide": "G000553",
+    "chamber": "house",
+    "full_name": "Al Green",
+    "name": "Green, Al (TX)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_tx_9_green_al_200.jpg",
+    "served_from": "2005-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "G000558": {
+    "bioguide": "G000558",
+    "chamber": "house",
+    "full_name": "Brett Guthrie",
+    "name": "Guthrie",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000558_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "state": "KY"
+  },
+  "G000559": {
+    "bioguide": "G000559",
+    "chamber": "house",
+    "full_name": "John Garamendi",
+    "name": "Garamendi",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/g000559_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "G000565": {
+    "bioguide": "G000565",
+    "chamber": "house",
+    "full_name": "Paul A. Gosar",
+    "name": "Gosar",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000565_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "AZ"
+  },
+  "G000568": {
+    "bioguide": "G000568",
+    "chamber": "house",
+    "full_name": "H. Morgan Griffith",
+    "name": "Griffith",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/68094df86c2e6631263de737_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "VA"
+  },
+  "G000576": {
+    "bioguide": "G000576",
+    "chamber": "house",
+    "full_name": "Glenn Grothman",
+    "name": "Grothman",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_wi_6_grothman_glenn_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "WI"
+  },
+  "G000578": {
+    "chamber": "house",
+    "name": "Gaetz",
+    "party": "R",
+    "state": "FL"
+  },
+  "G000581": {
+    "bioguide": "G000581",
+    "chamber": "house",
+    "full_name": "Vicente Gonzalez",
+    "name": "Gonzalez, V.",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_tx_15_gonzalez_vicente_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "G000583": {
+    "bioguide": "G000583",
+    "chamber": "house",
+    "full_name": "Josh Gottheimer",
+    "name": "Gottheimer",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_nj_5_gottheimer_josh_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "NJ"
+  },
+  "G000585": {
+    "bioguide": "G000585",
+    "chamber": "house",
+    "full_name": "Jimmy Gomez",
+    "name": "Gomez",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_ca_34_gomez_jimmy_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "G000586": {
+    "bioguide": "G000586",
+    "chamber": "house",
+    "full_name": "Jes\u00fas G. \"Chuy\" Garc\u00eda",
+    "name": "Garcia (IL)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/g000586_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "IL"
+  },
+  "G000587": {
+    "bioguide": "G000587",
+    "chamber": "house",
+    "full_name": "Sylvia R. Garcia",
+    "name": "Garcia (TX)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/g000587_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "G000589": {
+    "bioguide": "G000589",
+    "chamber": "house",
+    "full_name": "Lance Gooden",
+    "name": "Gooden",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000589_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "G000590": {
+    "bioguide": "G000590",
+    "chamber": "house",
+    "full_name": "Mark E. Green",
+    "name": "Green (TN)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000590_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "served_to": "2025-01-03",
+    "state": "TN"
+  },
+  "G000591": {
+    "bioguide": "G000591",
+    "chamber": "house",
+    "full_name": "Michael Guest",
+    "name": "Guest",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000591_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "MS"
+  },
+  "G000592": {
+    "bioguide": "G000592",
+    "chamber": "house",
+    "full_name": "Jared F. Golden",
+    "name": "Golden (ME)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/g000592_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "ME"
+  },
+  "G000593": {
+    "bioguide": "G000593",
+    "chamber": "house",
+    "full_name": "Carlos A. Gimenez",
+    "name": "Gimenez",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000593_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "G000594": {
+    "bioguide": "G000594",
+    "chamber": "house",
+    "full_name": "Tony Gonzales",
+    "name": "Gonzales, Tony",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000594_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "served_to": "2026-01-03",
+    "state": "TX"
+  },
+  "G000596": {
+    "bioguide": "G000596",
+    "chamber": "house",
+    "full_name": "Marjorie Taylor Greene",
+    "name": "Greene (GA)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000596_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "served_to": "2026-01-03",
+    "state": "GA"
+  },
+  "G000597": {
+    "bioguide": "G000597",
+    "chamber": "house",
+    "full_name": "Andrew R. Garbarino",
+    "name": "Garbarino",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000597_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "G000598": {
+    "bioguide": "G000598",
+    "chamber": "house",
+    "full_name": "Robert Garcia",
+    "name": "Garcia (CA)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/g000598_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "G000599": {
+    "bioguide": "G000599",
+    "chamber": "house",
+    "full_name": "Daniel S. Goldman",
+    "name": "Goldman (NY)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/g000599_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "G000600": {
+    "bioguide": "G000600",
+    "chamber": "house",
+    "full_name": "Marie Gluesenkamp Perez",
+    "name": "Perez",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/g000600_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "WA"
+  },
+  "G000601": {
+    "bioguide": "G000601",
+    "chamber": "house",
+    "full_name": "Craig A. Goldman",
+    "name": "Goldman (TX)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/6774277d0b34857ecc9090a7_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "G000602": {
+    "bioguide": "G000602",
+    "chamber": "house",
+    "full_name": "Laura Gillen",
+    "name": "Gillen",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677427de0b34857ecc9090ad_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "G000603": {
+    "bioguide": "G000603",
+    "chamber": "house",
+    "full_name": "Brandon Gill",
+    "name": "Gill (TX)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677428ac0b34857ecc9090b3_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "G000604": {
+    "bioguide": "G000604",
+    "chamber": "house",
+    "full_name": "Maggie Goodlander",
+    "name": "Goodlander",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/678ff62d66bf616cf1a7ce13_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "NH"
+  },
+  "G000605": {
+    "bioguide": "G000605",
+    "chamber": "house",
+    "full_name": "Adam Gray",
+    "name": "Gray",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67742a160b34857ecc9090bf_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "G000606": {
+    "bioguide": "G000606",
+    "chamber": "house",
+    "full_name": "Adelita S. Grijalva",
+    "name": "Grijalva",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6915fa2585b49de40f52cacb_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "AZ"
+  },
+  "H000874": {
+    "bioguide": "H000874",
+    "chamber": "house",
+    "full_name": "Steny H. Hoyer",
+    "name": "Hoyer",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_md_5_hoyer_steny_200.jpg",
+    "served_from": "1981-01-03",
+    "served_partial": false,
+    "state": "MD"
+  },
+  "H001047": {
+    "bioguide": "H001047",
+    "chamber": "house",
+    "full_name": "James A. Himes",
+    "name": "Himes",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/h001047_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "state": "CT"
+  },
+  "H001052": {
+    "bioguide": "H001052",
+    "chamber": "house",
+    "full_name": "Andy Harris",
+    "name": "Harris (MD)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/117_rp_md_1_harris_andy_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "MD"
+  },
+  "H001058": {
+    "bioguide": "H001058",
+    "chamber": "house",
+    "full_name": "Bill Huizenga",
+    "name": "Huizenga",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001058_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "MI"
+  },
+  "H001066": {
+    "bioguide": "H001066",
+    "chamber": "house",
+    "full_name": "Steven Horsford",
+    "name": "Horsford",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/h001066_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "NV"
+  },
+  "H001067": {
+    "bioguide": "H001067",
+    "chamber": "house",
+    "full_name": "Richard Hudson",
+    "name": "Hudson",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001067_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "NC"
+  },
+  "H001068": {
+    "bioguide": "H001068",
+    "chamber": "house",
+    "full_name": "Jared Huffman",
+    "name": "Huffman",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/h001068_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "H001072": {
+    "bioguide": "H001072",
+    "chamber": "house",
+    "full_name": "J. French Hill",
+    "name": "Hill (AR)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001072_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "AR"
+  },
+  "H001077": {
+    "bioguide": "H001077",
+    "chamber": "house",
+    "full_name": "Clay Higgins",
+    "name": "Higgins (LA)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001077_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "LA"
+  },
+  "H001081": {
+    "bioguide": "H001081",
+    "chamber": "house",
+    "full_name": "Jahana Hayes",
+    "name": "Hayes",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/h001081_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "CT"
+  },
+  "H001082": {
+    "bioguide": "H001082",
+    "chamber": "house",
+    "full_name": "Kevin Hern",
+    "name": "Hern (OK)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001082_200.jpg",
+    "served_from": "2018-01-03",
+    "served_partial": false,
+    "state": "OK"
+  },
+  "H001085": {
+    "bioguide": "H001085",
+    "chamber": "house",
+    "full_name": "Chrissy Houlahan",
+    "name": "Houlahan",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/681bc0e6b763f94d6e471f50_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "PA"
+  },
+  "H001086": {
+    "bioguide": "H001086",
+    "chamber": "house",
+    "full_name": "Diana Harshbarger",
+    "name": "Harshbarger",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001086_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "TN"
+  },
+  "H001090": {
+    "bioguide": "H001090",
+    "chamber": "house",
+    "full_name": "Josh Harder",
+    "name": "Harder (CA)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/h001090_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "H001091": {
+    "bioguide": "H001091",
+    "chamber": "house",
+    "full_name": "Ashley Hinson",
+    "name": "Hinson",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677ed0c7514c773869b6b920_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "IA"
+  },
+  "H001093": {
+    "bioguide": "H001093",
+    "chamber": "house",
+    "full_name": "Erin Houchin",
+    "name": "Houchin",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001093_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "IN"
+  },
+  "H001094": {
+    "bioguide": "H001094",
+    "chamber": "house",
+    "full_name": "Val T. Hoyle",
+    "name": "Hoyle (OR)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/h001094_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "OR"
+  },
+  "H001095": {
+    "bioguide": "H001095",
+    "chamber": "house",
+    "full_name": "Wesley Hunt",
+    "name": "Hunt",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001095_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "H001096": {
+    "bioguide": "H001096",
+    "chamber": "house",
+    "full_name": "Harriet M. Hageman",
+    "name": "Hageman",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001096_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "WY"
+  },
+  "H001098": {
+    "bioguide": "H001098",
+    "chamber": "house",
+    "full_name": "Abraham J. Hamadeh",
+    "name": "Hamadeh (AZ)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67742a6f0b34857ecc9090c5_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "AZ"
+  },
+  "H001099": {
+    "bioguide": "H001099",
+    "chamber": "house",
+    "full_name": "Mike Haridopolos",
+    "name": "Haridopolos",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67742ab50b34857ecc9090cb_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "H001100": {
+    "bioguide": "H001100",
+    "chamber": "house",
+    "full_name": "Jeff Hurd",
+    "name": "Hurd (CO)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67742c5e0b34857ecc9090d1_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "CO"
+  },
+  "H001101": {
+    "bioguide": "H001101",
+    "chamber": "house",
+    "full_name": "Pat Harrigan",
+    "name": "Harrigan",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67742ca40b34857ecc9090d7_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "NC"
+  },
+  "H001102": {
+    "bioguide": "H001102",
+    "chamber": "house",
+    "full_name": "Mark Harris",
+    "name": "Harris (NC)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67742d1b0b34857ecc9090dd_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "NC"
+  },
+  "H001103": {
+    "bioguide": "H001103",
+    "chamber": "house",
+    "full_name": "Pablo Jose Hern\u00e1ndez",
+    "name": "Hernandez",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67742d980b34857ecc9090e3_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "XX"
+  },
+  "I000056": {
+    "bioguide": "I000056",
+    "chamber": "house",
+    "full_name": "Darrell Issa",
+    "name": "Issa",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/i000056_200.jpg",
+    "served_from": "2001-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "I000058": {
+    "bioguide": "I000058",
+    "chamber": "house",
+    "full_name": "Glenn Ivey",
+    "name": "Ivey",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/i000058_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "MD"
+  },
+  "J000288": {
+    "bioguide": "J000288",
+    "chamber": "house",
+    "full_name": "Henry C. \"Hank\" Johnson",
+    "name": "Johnson (GA)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/j000288_200.jpg",
+    "served_from": "2007-01-03",
+    "served_partial": false,
+    "state": "GA"
+  },
+  "J000289": {
+    "bioguide": "J000289",
+    "chamber": "house",
+    "full_name": "Jim Jordan",
+    "name": "Jordan",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/j000289_200.jpg",
+    "served_from": "2007-01-03",
+    "served_partial": false,
+    "state": "OH"
+  },
+  "J000294": {
+    "bioguide": "J000294",
+    "chamber": "house",
+    "full_name": "Hakeem S. Jeffries",
+    "name": "Jeffries",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/j000294_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "J000295": {
+    "bioguide": "J000295",
+    "chamber": "house",
+    "full_name": "David P. Joyce",
+    "name": "Joyce (OH)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/j000295_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "OH"
+  },
+  "J000298": {
+    "bioguide": "J000298",
+    "chamber": "house",
+    "full_name": "Pramila Jayapal",
+    "name": "Jayapal",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_wa_7_jayapal_pramila_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "WA"
+  },
+  "J000299": {
+    "bioguide": "J000299",
+    "chamber": "house",
+    "full_name": "Mike Johnson",
+    "name": "Johnson (LA)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67ffcb2af22eaf56065817c4_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "LA"
+  },
+  "J000301": {
+    "bioguide": "J000301",
+    "chamber": "house",
+    "full_name": "Dusty Johnson",
+    "name": "Johnson (SD)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/j000301_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "SD"
+  },
+  "J000302": {
+    "bioguide": "J000302",
+    "chamber": "house",
+    "full_name": "John Joyce",
+    "name": "Joyce (PA)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/j000302_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "PA"
+  },
+  "J000304": {
+    "bioguide": "J000304",
+    "chamber": "house",
+    "full_name": "Ronny Jackson",
+    "name": "Jackson (TX)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/j000304_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "J000305": {
+    "bioguide": "J000305",
+    "chamber": "house",
+    "full_name": "Sara Jacobs",
+    "name": "Jacobs",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/j000305_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "J000307": {
+    "bioguide": "J000307",
+    "chamber": "house",
+    "full_name": "John James",
+    "name": "James",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/j000307_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "MI"
+  },
+  "J000309": {
+    "bioguide": "J000309",
+    "chamber": "house",
+    "full_name": "Jonathan L. Jackson",
+    "name": "Jackson (IL)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/j000309_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "IL"
+  },
+  "J000310": {
+    "bioguide": "J000310",
+    "chamber": "house",
+    "full_name": "Julie Johnson",
+    "name": "Johnson (TX)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67742df60b34857ecc9090e9_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "J000311": {
+    "bioguide": "J000311",
+    "chamber": "house",
+    "full_name": "Brian Jack",
+    "name": "Jack",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67742e330b34857ecc9090ef_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "GA"
+  },
+  "K000009": {
+    "bioguide": "K000009",
+    "chamber": "house",
+    "full_name": "Marcy Kaptur",
+    "name": "Kaptur",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/k000009_200.jpg",
+    "served_from": "1983-01-03",
+    "served_partial": false,
+    "state": "OH"
+  },
+  "K000375": {
+    "bioguide": "K000375",
+    "chamber": "house",
+    "full_name": "William R. Keating",
+    "name": "Keating",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/k000375_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "MA"
+  },
+  "K000376": {
+    "bioguide": "K000376",
+    "chamber": "house",
+    "full_name": "Mike Kelly",
+    "name": "Kelly (PA)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/k000376_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "PA"
+  },
+  "K000385": {
+    "bioguide": "K000385",
+    "chamber": "house",
+    "full_name": "Robin L. Kelly",
+    "name": "Kelly (IL)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_il_2_kelly_robin_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "IL"
+  },
+  "K000388": {
+    "bioguide": "K000388",
+    "chamber": "house",
+    "full_name": "Trent Kelly",
+    "name": "Kelly (MS)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_ms_1_kelly_trent_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "MS"
+  },
+  "K000389": {
+    "bioguide": "K000389",
+    "chamber": "house",
+    "full_name": "Ro Khanna",
+    "name": "Khanna",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/k000389_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "K000391": {
+    "bioguide": "K000391",
+    "chamber": "house",
+    "full_name": "Raja Krishnamoorthi",
+    "name": "Krishnamoorthi",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/k000391_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "IL"
+  },
+  "K000392": {
+    "bioguide": "K000392",
+    "chamber": "house",
+    "full_name": "David Kustoff",
+    "name": "Kustoff",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_tn_8_kustoff_david_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "TN"
+  },
+  "K000397": {
+    "bioguide": "K000397",
+    "chamber": "house",
+    "full_name": "Young Kim",
+    "name": "Kim",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/k000397_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "K000398": {
+    "bioguide": "K000398",
+    "chamber": "house",
+    "full_name": "Thomas H. Kean",
+    "name": "Kean",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/k000398_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "NJ"
+  },
+  "K000399": {
+    "bioguide": "K000399",
+    "chamber": "house",
+    "full_name": "Jennifer A. Kiggans",
+    "name": "Kiggans (VA)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/66b0ce45b0288a917d98f619_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "VA"
+  },
+  "K000400": {
+    "bioguide": "K000400",
+    "chamber": "house",
+    "full_name": "Sydney Kamlager-Dove",
+    "name": "Kamlager-Dove",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/k000400_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "K000401": {
+    "bioguide": "K000401",
+    "chamber": "house",
+    "full_name": "Kevin Kiley",
+    "name": "Kiley (CA)",
+    "party": "I",
+    "photo_url": "https://www.congress.gov/img/member/k000401_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "K000402": {
+    "bioguide": "K000402",
+    "chamber": "house",
+    "full_name": "Timothy M. Kennedy",
+    "name": "Kennedy (NY)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/668e9306658443009a697c8c_200.jpg",
+    "served_from": "2024-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "K000403": {
+    "bioguide": "K000403",
+    "chamber": "house",
+    "full_name": "Mike Kennedy",
+    "name": "Kennedy (UT)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67742e7c0b34857ecc9090f5_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "UT"
+  },
+  "K000404": {
+    "bioguide": "K000404",
+    "chamber": "house",
+    "full_name": "Kimberlyn King-Hinds",
+    "name": "King-Hinds",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67742f0a0b34857ecc9090fb_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "XX"
+  },
+  "K000405": {
+    "bioguide": "K000405",
+    "chamber": "house",
+    "full_name": "Brad Knott",
+    "name": "Knott",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67742fc60b34857ecc909101_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "NC"
+  },
+  "L000273": {
+    "bioguide": "L000273",
+    "chamber": "house",
+    "full_name": "Teresa Leger Fernandez",
+    "name": "Leger Fernandez",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/l000273_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "NM"
+  },
+  "L000397": {
+    "bioguide": "L000397",
+    "chamber": "house",
+    "full_name": "Zoe Lofgren",
+    "name": "Lofgren",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/671024d7ec807bca66057fcb_200.jpg",
+    "served_from": "1995-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "L000491": {
+    "bioguide": "L000491",
+    "chamber": "house",
+    "full_name": "Frank D. Lucas",
+    "name": "Lucas",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_ok_3_lucas_frank_200.jpg",
+    "served_from": "1993-01-03",
+    "served_partial": false,
+    "state": "OK"
+  },
+  "L000557": {
+    "bioguide": "L000557",
+    "chamber": "house",
+    "full_name": "John B. Larson",
+    "name": "Larson (CT)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/l000557_200.jpg",
+    "served_from": "1999-01-03",
+    "served_partial": false,
+    "state": "CT"
+  },
+  "L000560": {
+    "bioguide": "L000560",
+    "chamber": "house",
+    "full_name": "Rick Larsen",
+    "name": "Larsen (WA)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_wa_2_larsen_rick_200.jpg",
+    "served_from": "2001-01-03",
+    "served_partial": false,
+    "state": "WA"
+  },
+  "L000562": {
+    "bioguide": "L000562",
+    "chamber": "house",
+    "full_name": "Stephen F. Lynch",
+    "name": "Lynch",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/l000562_200.jpg",
+    "served_from": "2001-01-03",
+    "served_partial": false,
+    "state": "MA"
+  },
+  "L000566": {
+    "bioguide": "L000566",
+    "chamber": "house",
+    "full_name": "Robert E. Latta",
+    "name": "Latta",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/6984c938b1dfe04d989a1e21_200.jpg",
+    "served_from": "2007-01-03",
+    "served_partial": false,
+    "state": "OH"
+  },
+  "L000578": {
+    "bioguide": "L000578",
+    "chamber": "house",
+    "full_name": "Doug LaMalfa",
+    "name": "LaMalfa",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/l000578_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "served_to": "2026-01-03",
+    "state": "CA"
+  },
+  "L000582": {
+    "bioguide": "L000582",
+    "chamber": "house",
+    "full_name": "Ted Lieu",
+    "name": "Lieu",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/l000582_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "L000583": {
+    "bioguide": "L000583",
+    "chamber": "house",
+    "full_name": "Barry Loudermilk",
+    "name": "Loudermilk",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_ga_11_loudermilk_barry_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "GA"
+  },
+  "L000585": {
+    "bioguide": "L000585",
+    "chamber": "house",
+    "full_name": "Darin LaHood",
+    "name": "LaHood",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_il_18_lahood_darin_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "IL"
+  },
+  "L000590": {
+    "bioguide": "L000590",
+    "chamber": "house",
+    "full_name": "Susie Lee",
+    "name": "Lee (NV)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/l000590_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "NV"
+  },
+  "L000593": {
+    "bioguide": "L000593",
+    "chamber": "house",
+    "full_name": "Mike Levin",
+    "name": "Levin",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/l000593_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "L000595": {
+    "bioguide": "L000595",
+    "chamber": "house",
+    "full_name": "Julia Letlow",
+    "name": "Letlow",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/l000595_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "LA"
+  },
+  "L000596": {
+    "bioguide": "L000596",
+    "chamber": "house",
+    "full_name": "Anna Paulina Luna",
+    "name": "Luna",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/l000596_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "L000597": {
+    "bioguide": "L000597",
+    "chamber": "house",
+    "full_name": "Laurel M. Lee",
+    "name": "Lee (FL)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/l000597_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "L000598": {
+    "bioguide": "L000598",
+    "chamber": "house",
+    "full_name": "Nick LaLota",
+    "name": "LaLota",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/l000598_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "L000599": {
+    "bioguide": "L000599",
+    "chamber": "house",
+    "full_name": "Michael Lawler",
+    "name": "Lawler",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/l000599_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "L000600": {
+    "bioguide": "L000600",
+    "chamber": "house",
+    "full_name": "Nicholas A. Langworthy",
+    "name": "Langworthy",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/680901e76c2e6631263de716_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "L000601": {
+    "bioguide": "L000601",
+    "chamber": "house",
+    "full_name": "Greg Landsman",
+    "name": "Landsman",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/l000601_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "OH"
+  },
+  "L000602": {
+    "bioguide": "L000602",
+    "chamber": "house",
+    "full_name": "Summer L. Lee",
+    "name": "Lee (PA)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/l000602_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "PA"
+  },
+  "L000603": {
+    "bioguide": "L000603",
+    "chamber": "house",
+    "full_name": "Morgan Luttrell",
+    "name": "Luttrell",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/l000603_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "L000606": {
+    "bioguide": "L000606",
+    "chamber": "house",
+    "full_name": "George Latimer",
+    "name": "Latimer",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6774301b0b34857ecc909107_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "L000607": {
+    "bioguide": "L000607",
+    "chamber": "house",
+    "full_name": "Sam T. Liccardo",
+    "name": "Liccardo",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6774305d0b34857ecc90910d_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "M000194": {
+    "bioguide": "M000194",
+    "chamber": "house",
+    "full_name": "Nancy Mace",
+    "name": "Mace",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m000194_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "SC"
+  },
+  "M000312": {
+    "bioguide": "M000312",
+    "chamber": "house",
+    "full_name": "James P. McGovern",
+    "name": "McGovern",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/117_rp_ma_2_mcgovern_james_200.jpg",
+    "served_from": "1997-01-03",
+    "served_partial": false,
+    "state": "MA"
+  },
+  "M000317": {
+    "bioguide": "M000317",
+    "chamber": "house",
+    "full_name": "Nicole Malliotakis",
+    "name": "Malliotakis",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m000317_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "M000687": {
+    "bioguide": "M000687",
+    "chamber": "house",
+    "full_name": "Kweisi Mfume",
+    "name": "Mfume",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m000687_200.jpg",
+    "served_from": "1987-01-03",
+    "served_partial": false,
+    "state": "MD"
+  },
+  "M000871": {
+    "bioguide": "M000871",
+    "chamber": "house",
+    "full_name": "Tracey Mann",
+    "name": "Mann",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m000871_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "KS"
+  },
+  "M001136": {
+    "bioguide": "M001136",
+    "chamber": "house",
+    "full_name": "Lisa C. McClain",
+    "name": "McClain",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677836d41658e791a384976d_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "MI"
+  },
+  "M001137": {
+    "bioguide": "M001137",
+    "chamber": "house",
+    "full_name": "Gregory W. Meeks",
+    "name": "Meeks",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001137_200.jpg",
+    "served_from": "1997-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "M001143": {
+    "bioguide": "M001143",
+    "chamber": "house",
+    "full_name": "Betty McCollum",
+    "name": "McCollum",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_mn_4_mccollum_betty_200.jpg",
+    "served_from": "2001-01-03",
+    "served_partial": false,
+    "state": "MN"
+  },
+  "M001157": {
+    "bioguide": "M001157",
+    "chamber": "house",
+    "full_name": "Michael T. McCaul",
+    "name": "McCaul",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_tx_10_mccaul_michael_200.jpg",
+    "served_from": "2005-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "M001160": {
+    "bioguide": "M001160",
+    "chamber": "house",
+    "full_name": "Gwen Moore",
+    "name": "Moore (WI)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_wi_4_moore_gwen_200.jpg",
+    "served_from": "2005-01-03",
+    "served_partial": false,
+    "state": "WI"
+  },
+  "M001163": {
+    "bioguide": "M001163",
+    "chamber": "house",
+    "full_name": "Doris O. Matsui",
+    "name": "Matsui",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001163_200.jpg",
+    "served_from": "2005-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "M001177": {
+    "bioguide": "M001177",
+    "chamber": "house",
+    "full_name": "Tom McClintock",
+    "name": "McClintock",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001177_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "M001184": {
+    "bioguide": "M001184",
+    "chamber": "house",
+    "full_name": "Thomas Massie",
+    "name": "Massie",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001184_200.jpg",
+    "served_from": "2012-01-03",
+    "served_partial": false,
+    "state": "KY"
+  },
+  "M001188": {
+    "bioguide": "M001188",
+    "chamber": "house",
+    "full_name": "Grace Meng",
+    "name": "Meng",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001188_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "M001194": {
+    "bioguide": "M001194",
+    "chamber": "house",
+    "full_name": "John R. Moolenaar",
+    "name": "Moolenaar",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001194_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "MI"
+  },
+  "M001196": {
+    "bioguide": "M001196",
+    "chamber": "house",
+    "full_name": "Seth Moulton",
+    "name": "Moulton",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001196_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "MA"
+  },
+  "M001199": {
+    "bioguide": "M001199",
+    "chamber": "house",
+    "full_name": "Brian J. Mast",
+    "name": "Mast",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_fl_18_mast_brian_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "M001204": {
+    "bioguide": "M001204",
+    "chamber": "house",
+    "full_name": "Daniel Meuser",
+    "name": "Meuser",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/6776ebbf4f8a93753830ca7d_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "PA"
+  },
+  "M001205": {
+    "bioguide": "M001205",
+    "chamber": "house",
+    "full_name": "Carol D. Miller",
+    "name": "Miller (WV)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001205_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "WV"
+  },
+  "M001206": {
+    "bioguide": "M001206",
+    "chamber": "house",
+    "full_name": "Joseph D. Morelle",
+    "name": "Morelle",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67ffc962f22eaf56065817bb_200.jpg",
+    "served_from": "2018-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "M001208": {
+    "bioguide": "M001208",
+    "chamber": "house",
+    "full_name": "Lucy McBath",
+    "name": "McBath",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001208_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "GA"
+  },
+  "M001210": {
+    "bioguide": "M001210",
+    "chamber": "house",
+    "full_name": "Gregory F. Murphy",
+    "name": "Murphy",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001210_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "NC"
+  },
+  "M001211": {
+    "bioguide": "M001211",
+    "chamber": "house",
+    "full_name": "Mary E. Miller",
+    "name": "Miller (IL)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001211_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "IL"
+  },
+  "M001212": {
+    "bioguide": "M001212",
+    "chamber": "house",
+    "full_name": "Barry Moore",
+    "name": "Moore (AL)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001212_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "AL"
+  },
+  "M001213": {
+    "bioguide": "M001213",
+    "chamber": "house",
+    "full_name": "Blake D. Moore",
+    "name": "Moore (UT)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001213_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "UT"
+  },
+  "M001214": {
+    "bioguide": "M001214",
+    "chamber": "house",
+    "full_name": "Frank J. Mrvan",
+    "name": "Mrvan",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001214_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "IN"
+  },
+  "M001215": {
+    "bioguide": "M001215",
+    "chamber": "house",
+    "full_name": "Mariannette Miller-Meeks",
+    "name": "Miller-Meeks",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001215_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "IA"
+  },
+  "M001216": {
+    "bioguide": "M001216",
+    "chamber": "house",
+    "full_name": "Cory Mills",
+    "name": "Mills",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001216_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "M001217": {
+    "bioguide": "M001217",
+    "chamber": "house",
+    "full_name": "Jared Moskowitz",
+    "name": "Moskowitz",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001217_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "M001218": {
+    "bioguide": "M001218",
+    "chamber": "house",
+    "full_name": "Richard McCormick",
+    "name": "McCormick",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001218_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "GA"
+  },
+  "M001219": {
+    "bioguide": "M001219",
+    "chamber": "house",
+    "full_name": "James C. Moylan",
+    "name": "Moylan",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001219_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "XX"
+  },
+  "M001220": {
+    "bioguide": "M001220",
+    "chamber": "house",
+    "full_name": "Morgan McGarvey",
+    "name": "McGarvey",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001220_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "KY"
+  },
+  "M001222": {
+    "bioguide": "M001222",
+    "chamber": "house",
+    "full_name": "Max L. Miller",
+    "name": "Miller (OH)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/680908fb6c2e6631263de71f_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "OH"
+  },
+  "M001223": {
+    "bioguide": "M001223",
+    "chamber": "house",
+    "full_name": "Seth Magaziner",
+    "name": "Magaziner",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001223_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "RI"
+  },
+  "M001224": {
+    "bioguide": "M001224",
+    "chamber": "house",
+    "full_name": "Nathaniel Moran",
+    "name": "Moran",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/680008c5f22eaf56065817f4_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "M001225": {
+    "bioguide": "M001225",
+    "chamber": "house",
+    "full_name": "Kevin Mullin",
+    "name": "Mullin",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001225_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "M001226": {
+    "bioguide": "M001226",
+    "chamber": "house",
+    "full_name": "Robert Menendez",
+    "name": "Menendez",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/681231f6246d1b6bd8d9f6b4_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "NJ"
+  },
+  "M001227": {
+    "bioguide": "M001227",
+    "chamber": "house",
+    "full_name": "Jennifer L. McClellan",
+    "name": "McClellan",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001227_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "VA"
+  },
+  "M001228": {
+    "bioguide": "M001228",
+    "chamber": "house",
+    "full_name": "Celeste Maloy",
+    "name": "Maloy",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001228_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "UT"
+  },
+  "M001229": {
+    "bioguide": "M001229",
+    "chamber": "house",
+    "full_name": "LaMonica McIver",
+    "name": "McIver",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/681dfed94fc893ce843e24b8_200.jpg",
+    "served_from": "2024-01-03",
+    "served_partial": false,
+    "state": "NJ"
+  },
+  "M001230": {
+    "bioguide": "M001230",
+    "chamber": "house",
+    "full_name": "Ryan Mackenzie",
+    "name": "Mackenzie",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677430a40b34857ecc909113_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "PA"
+  },
+  "M001231": {
+    "bioguide": "M001231",
+    "chamber": "house",
+    "full_name": "John W. Mannion",
+    "name": "Mannion",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677446860b34857ecc909119_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "M001232": {
+    "bioguide": "M001232",
+    "chamber": "house",
+    "full_name": "April McClain Delaney",
+    "name": "McClain Delaney",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677446d80b34857ecc90911f_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "MD"
+  },
+  "M001233": {
+    "bioguide": "M001233",
+    "chamber": "house",
+    "full_name": "Mark B. Messmer",
+    "name": "Messmer",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677448630b34857ecc909125_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "IN"
+  },
+  "M001234": {
+    "bioguide": "M001234",
+    "chamber": "house",
+    "full_name": "Kelly Morrison",
+    "name": "Morrison",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677449a70b34857ecc90912b_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "MN"
+  },
+  "M001235": {
+    "bioguide": "M001235",
+    "chamber": "house",
+    "full_name": "Riley M. Moore",
+    "name": "Moore (WV)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677449fd0b34857ecc909131_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "WV"
+  },
+  "M001236": {
+    "bioguide": "M001236",
+    "chamber": "house",
+    "full_name": "Tim Moore",
+    "name": "Moore (NC)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67744a540b34857ecc909137_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "NC"
+  },
+  "M001237": {
+    "bioguide": "M001237",
+    "chamber": "house",
+    "full_name": "Kristen McDonald Rivet",
+    "name": "McDonald Rivet",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/69d7f4ee7304cef7a0c977a8_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "MI"
+  },
+  "M001238": {
+    "bioguide": "M001238",
+    "chamber": "house",
+    "full_name": "Sarah McBride",
+    "name": "McBride",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67744b460b34857ecc909143_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "DE"
+  },
+  "M001239": {
+    "bioguide": "M001239",
+    "chamber": "house",
+    "full_name": "John J. McGuire",
+    "name": "McGuire",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67744ba20b34857ecc909149_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "VA"
+  },
+  "M001240": {
+    "bioguide": "M001240",
+    "chamber": "house",
+    "full_name": "Addison P. McDowell",
+    "name": "McDowell",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67744e930b34857ecc90914f_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "NC"
+  },
+  "M001241": {
+    "bioguide": "M001241",
+    "chamber": "house",
+    "full_name": "Dave Min",
+    "name": "Min",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67744ed90b34857ecc909155_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "M001245": {
+    "bioguide": "M001245",
+    "chamber": "house",
+    "full_name": "Christian D. Menefee",
+    "name": "Menefee",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/698206b2eb30d3271899373e_200.jpg",
+    "served_from": "2026-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "M001246": {
+    "bioguide": "M001246",
+    "chamber": "house",
+    "full_name": "Analilia Mejia",
+    "name": "Mejia",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/69eb9743277642362f6112ce_200.jpg",
+    "served_from": "2026-01-03",
+    "served_partial": false,
+    "state": "NJ"
+  },
+  "N000002": {
+    "bioguide": "N000002",
+    "chamber": "house",
+    "full_name": "Jerrold Nadler",
+    "name": "Nadler",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/n000002_200.jpg",
+    "served_from": "1991-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "N000015": {
+    "bioguide": "N000015",
+    "chamber": "house",
+    "full_name": "Richard E. Neal",
+    "name": "Neal",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/n000015_200.jpg",
+    "served_from": "1989-01-03",
+    "served_partial": false,
+    "state": "MA"
+  },
+  "N000026": {
+    "bioguide": "N000026",
+    "chamber": "house",
+    "full_name": "Troy E. Nehls",
+    "name": "Nehls",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/n000026_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "N000147": {
+    "bioguide": "N000147",
+    "chamber": "house",
+    "full_name": "Eleanor Holmes Norton",
+    "name": "Norton",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_dg_dc_norton_eleanor_200.jpg",
+    "served_from": "1991-01-03",
+    "served_partial": false,
+    "state": "XX"
+  },
+  "N000188": {
+    "bioguide": "N000188",
+    "chamber": "house",
+    "full_name": "Donald Norcross",
+    "name": "Norcross",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/n000188_200.jpg",
+    "served_from": "2014-01-03",
+    "served_partial": false,
+    "state": "NJ"
+  },
+  "N000189": {
+    "bioguide": "N000189",
+    "chamber": "house",
+    "full_name": "Dan Newhouse",
+    "name": "Newhouse",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_wa_4_newhouse_dan_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "WA"
+  },
+  "N000190": {
+    "bioguide": "N000190",
+    "chamber": "house",
+    "full_name": "Ralph Norman",
+    "name": "Norman",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/n000190_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "SC"
+  },
+  "N000191": {
+    "bioguide": "N000191",
+    "chamber": "house",
+    "full_name": "Joe Neguse",
+    "name": "Neguse",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/n000191_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "CO"
+  },
+  "N000193": {
+    "bioguide": "N000193",
+    "chamber": "house",
+    "full_name": "Zachary Nunn",
+    "name": "Nunn (IA)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/n000193_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "IA"
+  },
+  "O000019": {
+    "bioguide": "O000019",
+    "chamber": "house",
+    "full_name": "Jay Obernolte",
+    "name": "Obernolte",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/o000019_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "O000086": {
+    "bioguide": "O000086",
+    "chamber": "house",
+    "full_name": "Burgess Owens",
+    "name": "Owens",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/o000086_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "UT"
+  },
+  "O000172": {
+    "bioguide": "O000172",
+    "chamber": "house",
+    "full_name": "Alexandria Ocasio-Cortez",
+    "name": "Ocasio-Cortez",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/o000172_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "O000173": {
+    "bioguide": "O000173",
+    "chamber": "house",
+    "full_name": "Ilhan Omar",
+    "name": "Omar",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/o000173_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "MN"
+  },
+  "O000175": {
+    "bioguide": "O000175",
+    "chamber": "house",
+    "full_name": "Andrew Ogles",
+    "name": "Ogles",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/o000175_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "TN"
+  },
+  "O000176": {
+    "bioguide": "O000176",
+    "chamber": "house",
+    "full_name": "Johnny Olszewski",
+    "name": "Olszewski",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67744f4e0b34857ecc90915b_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "MD"
+  },
+  "O000177": {
+    "bioguide": "O000177",
+    "chamber": "house",
+    "full_name": "Robert F. Onder",
+    "name": "Onder",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67744f970b34857ecc909161_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "MO"
+  },
+  "P000034": {
+    "bioguide": "P000034",
+    "chamber": "house",
+    "full_name": "Frank Pallone",
+    "name": "Pallone",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/p000034_200.jpg",
+    "served_from": "1987-01-03",
+    "served_partial": false,
+    "state": "NJ"
+  },
+  "P000048": {
+    "bioguide": "P000048",
+    "chamber": "house",
+    "full_name": "August Pfluger",
+    "name": "Pfluger",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/p000048_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "P000197": {
+    "bioguide": "P000197",
+    "chamber": "house",
+    "full_name": "Nancy Pelosi",
+    "name": "Pelosi",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/p000197_200.jpg",
+    "served_from": "1987-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "P000597": {
+    "bioguide": "P000597",
+    "chamber": "house",
+    "full_name": "Chellie Pingree",
+    "name": "Pingree",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/p000597_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "state": "ME"
+  },
+  "P000605": {
+    "bioguide": "P000605",
+    "chamber": "house",
+    "full_name": "Scott Perry",
+    "name": "Perry",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677ec7d3514c773869b6b915_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "PA"
+  },
+  "P000607": {
+    "bioguide": "P000607",
+    "chamber": "house",
+    "full_name": "Mark Pocan",
+    "name": "Pocan",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/p000607_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "WI"
+  },
+  "P000608": {
+    "bioguide": "P000608",
+    "chamber": "house",
+    "full_name": "Scott H. Peters",
+    "name": "Peters",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/p000608_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "P000609": {
+    "bioguide": "P000609",
+    "chamber": "house",
+    "full_name": "Gary J. Palmer",
+    "name": "Palmer",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/p000609_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "AL"
+  },
+  "P000610": {
+    "bioguide": "P000610",
+    "chamber": "house",
+    "full_name": "Stacey E. Plaskett",
+    "name": "Plaskett",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_dg_vi_plaskett_stacey_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "XX"
+  },
+  "P000613": {
+    "bioguide": "P000613",
+    "chamber": "house",
+    "full_name": "Jimmy Panetta",
+    "name": "Panetta",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_ca_20_panetta_jimmy_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "P000614": {
+    "bioguide": "P000614",
+    "chamber": "house",
+    "full_name": "Chris Pappas",
+    "name": "Pappas",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/p000614_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "NH"
+  },
+  "P000617": {
+    "bioguide": "P000617",
+    "chamber": "house",
+    "full_name": "Ayanna Pressley",
+    "name": "Pressley",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/p000617_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "MA"
+  },
+  "P000620": {
+    "bioguide": "P000620",
+    "chamber": "house",
+    "full_name": "Brittany Pettersen",
+    "name": "Pettersen",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/p000620_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "CO"
+  },
+  "P000621": {
+    "bioguide": "P000621",
+    "chamber": "house",
+    "full_name": "Nellie Pou",
+    "name": "Pou",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67745d3b0b34857ecc909167_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "NJ"
+  },
+  "P000622": {
+    "bioguide": "P000622",
+    "chamber": "house",
+    "full_name": "Jimmy Patronis",
+    "name": "Patronis",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67efdb991b05a5a598f7fde9_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "Q000023": {
+    "bioguide": "Q000023",
+    "chamber": "house",
+    "full_name": "Mike Quigley",
+    "name": "Quigley",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/q000023_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "state": "IL"
+  },
+  "R000305": {
+    "bioguide": "R000305",
+    "chamber": "house",
+    "full_name": "Deborah K. Ross",
+    "name": "Ross",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/r000305_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "NC"
+  },
+  "R000395": {
+    "bioguide": "R000395",
+    "chamber": "house",
+    "full_name": "Harold Rogers",
+    "name": "Rogers (KY)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000395_200.jpg",
+    "served_from": "1981-01-03",
+    "served_partial": false,
+    "state": "KY"
+  },
+  "R000575": {
+    "bioguide": "R000575",
+    "chamber": "house",
+    "full_name": "Mike D. Rogers",
+    "name": "Rogers (AL)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_al_3_rogers_mike_200.jpg",
+    "served_from": "2003-01-03",
+    "served_partial": false,
+    "state": "AL"
+  },
+  "R000579": {
+    "bioguide": "R000579",
+    "chamber": "house",
+    "full_name": "Patrick Ryan",
+    "name": "Ryan",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/r000579_200.jpg",
+    "served_from": "2022-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "R000599": {
+    "bioguide": "R000599",
+    "chamber": "house",
+    "full_name": "Raul Ruiz",
+    "name": "Ruiz",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/66e1aec832c796cea99fe06f_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "R000600": {
+    "bioguide": "R000600",
+    "chamber": "house",
+    "full_name": "Aumua Amata Coleman Radewagen",
+    "name": "Radewagen",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000600_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "XX"
+  },
+  "R000603": {
+    "bioguide": "R000603",
+    "chamber": "house",
+    "full_name": "David Rouzer",
+    "name": "Rouzer",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000603_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "NC"
+  },
+  "R000606": {
+    "bioguide": "R000606",
+    "chamber": "house",
+    "full_name": "Jamie Raskin",
+    "name": "Raskin",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/r000606_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "MD"
+  },
+  "R000609": {
+    "bioguide": "R000609",
+    "chamber": "house",
+    "full_name": "John H. Rutherford",
+    "name": "Rutherford",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000609_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "R000610": {
+    "bioguide": "R000610",
+    "chamber": "house",
+    "full_name": "Guy Reschenthaler",
+    "name": "Reschenthaler",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000610_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "PA"
+  },
+  "R000612": {
+    "bioguide": "R000612",
+    "chamber": "house",
+    "full_name": "John W. Rose",
+    "name": "Rose",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000612_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "TN"
+  },
+  "R000614": {
+    "bioguide": "R000614",
+    "chamber": "house",
+    "full_name": "Chip Roy",
+    "name": "Roy",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000614_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "R000617": {
+    "bioguide": "R000617",
+    "chamber": "house",
+    "full_name": "Delia C. Ramirez",
+    "name": "Ramirez",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/684c2356333714e4aee2e1fd_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "IL"
+  },
+  "R000619": {
+    "bioguide": "R000619",
+    "chamber": "house",
+    "full_name": "Michael A. Rulli",
+    "name": "Rulli",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/69401dcc8cd6e06e9e3b36de_200.jpg",
+    "served_from": "2024-01-03",
+    "served_partial": false,
+    "state": "OH"
+  },
+  "R000620": {
+    "bioguide": "R000620",
+    "chamber": "house",
+    "full_name": "Luz M. Rivas",
+    "name": "Rivas",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67745d860b34857ecc90916d_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "R000621": {
+    "bioguide": "R000621",
+    "chamber": "house",
+    "full_name": "Emily Randall",
+    "name": "Randall",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67745dcf0b34857ecc909173_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "WA"
+  },
+  "R000622": {
+    "bioguide": "R000622",
+    "chamber": "house",
+    "full_name": "Josh Riley",
+    "name": "Riley (NY)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67745e360b34857ecc909179_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "S000168": {
+    "bioguide": "S000168",
+    "chamber": "house",
+    "full_name": "Maria Elvira Salazar",
+    "name": "Salazar",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s000168_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "S000185": {
+    "bioguide": "S000185",
+    "chamber": "house",
+    "full_name": "Robert C. \"Bobby\" Scott",
+    "name": "Scott (VA)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s000185_200.jpg",
+    "served_from": "1993-01-03",
+    "served_partial": false,
+    "state": "VA"
+  },
+  "S000250": {
+    "bioguide": "S000250",
+    "chamber": "house",
+    "full_name": "Pete Sessions",
+    "name": "Sessions",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_tx_32_sessions_pete_200.jpg",
+    "served_from": "1997-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "S000344": {
+    "bioguide": "S000344",
+    "chamber": "house",
+    "full_name": "Brad Sherman",
+    "name": "Sherman",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s000344_200.jpg",
+    "served_from": "1997-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "S000510": {
+    "bioguide": "S000510",
+    "chamber": "house",
+    "full_name": "Adam Smith",
+    "name": "Smith (WA)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_wa_9_smith_adam_200.jpg",
+    "served_from": "1997-01-03",
+    "served_partial": false,
+    "state": "WA"
+  },
+  "S000522": {
+    "bioguide": "S000522",
+    "chamber": "house",
+    "full_name": "Christopher H. Smith",
+    "name": "Smith (NJ)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s000522_200.jpg",
+    "served_from": "1981-01-03",
+    "served_partial": false,
+    "state": "NJ"
+  },
+  "S000929": {
+    "bioguide": "S000929",
+    "chamber": "house",
+    "full_name": "Victoria Spartz",
+    "name": "Spartz",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s000929_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "IN"
+  },
+  "S001145": {
+    "bioguide": "S001145",
+    "chamber": "house",
+    "full_name": "Janice D. Schakowsky",
+    "name": "Schakowsky",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_il_9_schakowsky_janice_200.jpg",
+    "served_from": "1999-01-03",
+    "served_partial": false,
+    "state": "IL"
+  },
+  "S001148": {
+    "bioguide": "S001148",
+    "chamber": "house",
+    "full_name": "Michael K. Simpson",
+    "name": "Simpson",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/678152c81f9ad6ea6fb1eb7f_200.jpg",
+    "served_from": "1999-01-03",
+    "served_partial": false,
+    "state": "ID"
+  },
+  "S001156": {
+    "bioguide": "S001156",
+    "chamber": "house",
+    "full_name": "Linda T. S\u00e1nchez",
+    "name": "Sanchez",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_ca_38_snchez_linda_200.jpg",
+    "served_from": "2003-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "S001157": {
+    "bioguide": "S001157",
+    "chamber": "house",
+    "full_name": "David Scott",
+    "name": "Scott, David",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_ga_13_scott_david_200.jpg",
+    "served_from": "2003-01-03",
+    "served_partial": false,
+    "served_to": "2026-01-03",
+    "state": "GA"
+  },
+  "S001159": {
+    "bioguide": "S001159",
+    "chamber": "house",
+    "full_name": "Marilyn Strickland",
+    "name": "Strickland",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001159_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "WA"
+  },
+  "S001172": {
+    "bioguide": "S001172",
+    "chamber": "house",
+    "full_name": "Adrian Smith",
+    "name": "Smith (NE)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001172_200.jpg",
+    "served_from": "2007-01-03",
+    "served_partial": false,
+    "state": "NE"
+  },
+  "S001176": {
+    "bioguide": "S001176",
+    "chamber": "house",
+    "full_name": "Steve Scalise",
+    "name": "Scalise",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001176_200.jpg",
+    "served_from": "2008-01-03",
+    "served_partial": false,
+    "state": "LA"
+  },
+  "S001183": {
+    "bioguide": "S001183",
+    "chamber": "house",
+    "full_name": "David Schweikert",
+    "name": "Schweikert",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001183_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "AZ"
+  },
+  "S001185": {
+    "bioguide": "S001185",
+    "chamber": "house",
+    "full_name": "Terri A. Sewell",
+    "name": "Sewell",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001185_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "AL"
+  },
+  "S001188": {
+    "bioguide": "S001188",
+    "chamber": "house",
+    "full_name": "Marlin A. Stutzman",
+    "name": "Stutzman",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67745e7e0b34857ecc90917f_200.jpg",
+    "served_from": "2010-01-03",
+    "served_partial": false,
+    "state": "IN"
+  },
+  "S001189": {
+    "bioguide": "S001189",
+    "chamber": "house",
+    "full_name": "Austin Scott",
+    "name": "Scott, Austin",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001189_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "GA"
+  },
+  "S001190": {
+    "bioguide": "S001190",
+    "chamber": "house",
+    "full_name": "Bradley Scott Schneider",
+    "name": "Schneider",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001190_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "IL"
+  },
+  "S001193": {
+    "bioguide": "S001193",
+    "chamber": "house",
+    "full_name": "Eric Swalwell",
+    "name": "Swalwell",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001193_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "served_to": "2026-01-03",
+    "state": "CA"
+  },
+  "S001195": {
+    "bioguide": "S001195",
+    "chamber": "house",
+    "full_name": "Jason Smith",
+    "name": "Smith (MO)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001195_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "MO"
+  },
+  "S001196": {
+    "bioguide": "S001196",
+    "chamber": "house",
+    "full_name": "Elise M. Stefanik",
+    "name": "Stefanik",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001196_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "S001199": {
+    "bioguide": "S001199",
+    "chamber": "house",
+    "full_name": "Lloyd Smucker",
+    "name": "Smucker",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001199_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "PA"
+  },
+  "S001200": {
+    "bioguide": "S001200",
+    "chamber": "house",
+    "full_name": "Darren Soto",
+    "name": "Soto",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_fl_9_soto_darren_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "S001201": {
+    "bioguide": "S001201",
+    "chamber": "house",
+    "full_name": "Thomas R. Suozzi",
+    "name": "Suozzi",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_ny_3_suozzi_thomas_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "S001205": {
+    "bioguide": "S001205",
+    "chamber": "house",
+    "full_name": "Mary Gay Scanlon",
+    "name": "Scanlon",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_pa_5_scanlon_mary_200.jpg",
+    "served_from": "2018-01-03",
+    "served_partial": false,
+    "state": "PA"
+  },
+  "S001207": {
+    "bioguide": "S001207",
+    "chamber": "house",
+    "full_name": "Mikie Sherrill",
+    "name": "Sherrill",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001207_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "served_to": "2025-01-03",
+    "state": "NJ"
+  },
+  "S001211": {
+    "bioguide": "S001211",
+    "chamber": "house",
+    "full_name": "Greg Stanton",
+    "name": "Stanton",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001211_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "AZ"
+  },
+  "S001212": {
+    "bioguide": "S001212",
+    "chamber": "house",
+    "full_name": "Pete Stauber",
+    "name": "Stauber",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001212_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "MN"
+  },
+  "S001213": {
+    "bioguide": "S001213",
+    "chamber": "house",
+    "full_name": "Bryan Steil",
+    "name": "Steil",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001213_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "WI"
+  },
+  "S001214": {
+    "bioguide": "S001214",
+    "chamber": "house",
+    "full_name": "W. Gregory Steube",
+    "name": "Steube",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001214_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "S001215": {
+    "bioguide": "S001215",
+    "chamber": "house",
+    "full_name": "Haley M. Stevens",
+    "name": "Stevens",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001215_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "MI"
+  },
+  "S001216": {
+    "bioguide": "S001216",
+    "chamber": "house",
+    "full_name": "Kim Schrier",
+    "name": "Schrier",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001216_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "WA"
+  },
+  "S001218": {
+    "bioguide": "S001218",
+    "chamber": "house",
+    "full_name": "Melanie A. Stansbury",
+    "name": "Stansbury",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001218_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "NM"
+  },
+  "S001220": {
+    "bioguide": "S001220",
+    "chamber": "house",
+    "full_name": "Dale W. Strong",
+    "name": "Strong",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001220_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "AL"
+  },
+  "S001221": {
+    "bioguide": "S001221",
+    "chamber": "house",
+    "full_name": "Hillary J. Scholten",
+    "name": "Scholten",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001221_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "MI"
+  },
+  "S001223": {
+    "bioguide": "S001223",
+    "chamber": "house",
+    "full_name": "Emilia Strong Sykes",
+    "name": "Sykes",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001223_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "OH"
+  },
+  "S001224": {
+    "bioguide": "S001224",
+    "chamber": "house",
+    "full_name": "Keith Self",
+    "name": "Self",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001224_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "S001225": {
+    "bioguide": "S001225",
+    "chamber": "house",
+    "full_name": "Eric Sorensen",
+    "name": "Sorensen",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001225_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "IL"
+  },
+  "S001226": {
+    "bioguide": "S001226",
+    "chamber": "house",
+    "full_name": "Andrea Salinas",
+    "name": "Salinas",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001226_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "OR"
+  },
+  "S001228": {
+    "bioguide": "S001228",
+    "chamber": "house",
+    "full_name": "Derek Schmidt",
+    "name": "Schmidt",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67745ec50b34857ecc909185_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "KS"
+  },
+  "S001229": {
+    "bioguide": "S001229",
+    "chamber": "house",
+    "full_name": "Jefferson Shreve",
+    "name": "Shreve",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67745f0e0b34857ecc90918b_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "IN"
+  },
+  "S001230": {
+    "bioguide": "S001230",
+    "chamber": "house",
+    "full_name": "Suhas Subramanyam",
+    "name": "Subramanyam",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6797be8bc75fbc6f720e476a_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "VA"
+  },
+  "S001231": {
+    "bioguide": "S001231",
+    "chamber": "house",
+    "full_name": "Lateefah Simon",
+    "name": "Simon",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67745f940b34857ecc909197_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "T000165": {
+    "bioguide": "T000165",
+    "chamber": "house",
+    "full_name": "Thomas P. Tiffany",
+    "name": "Tiffany",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/t000165_200.jpg",
+    "served_from": "2020-01-03",
+    "served_partial": false,
+    "state": "WI"
+  },
+  "T000193": {
+    "bioguide": "T000193",
+    "chamber": "house",
+    "full_name": "Bennie G. Thompson",
+    "name": "Thompson (MS)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/t000193_200.jpg",
+    "served_from": "1993-01-03",
+    "served_partial": false,
+    "state": "MS"
+  },
+  "T000460": {
+    "bioguide": "T000460",
+    "chamber": "house",
+    "full_name": "Mike Thompson",
+    "name": "Thompson (CA)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_ca_5_thompson_mike_200.jpg",
+    "served_from": "1999-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "T000463": {
+    "bioguide": "T000463",
+    "chamber": "house",
+    "full_name": "Michael R. Turner",
+    "name": "Turner (OH)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/68014c674e51529406f18e56_200.jpg",
+    "served_from": "2003-01-03",
+    "served_partial": false,
+    "state": "OH"
+  },
+  "T000467": {
+    "bioguide": "T000467",
+    "chamber": "house",
+    "full_name": "Glenn Thompson",
+    "name": "Thompson (PA)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_pa_5_thompson_glenn_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "state": "PA"
+  },
+  "T000468": {
+    "bioguide": "T000468",
+    "chamber": "house",
+    "full_name": "Dina Titus",
+    "name": "Titus",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6809296b6c2e6631263de728_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "state": "NV"
+  },
+  "T000469": {
+    "bioguide": "T000469",
+    "chamber": "house",
+    "full_name": "Paul Tonko",
+    "name": "Tonko",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/t000469_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "T000472": {
+    "bioguide": "T000472",
+    "chamber": "house",
+    "full_name": "Mark Takano",
+    "name": "Takano",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/t000472_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "T000474": {
+    "bioguide": "T000474",
+    "chamber": "house",
+    "full_name": "Norma J. Torres",
+    "name": "Torres (CA)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/t000474_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "T000478": {
+    "bioguide": "T000478",
+    "chamber": "house",
+    "full_name": "Claudia Tenney",
+    "name": "Tenney",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/t000478_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "T000480": {
+    "bioguide": "T000480",
+    "chamber": "house",
+    "full_name": "William R. Timmons",
+    "name": "Timmons",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/t000480_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "SC"
+  },
+  "T000481": {
+    "bioguide": "T000481",
+    "chamber": "house",
+    "full_name": "Rashida Tlaib",
+    "name": "Tlaib",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/t000481_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "MI"
+  },
+  "T000482": {
+    "bioguide": "T000482",
+    "chamber": "house",
+    "full_name": "Lori Trahan",
+    "name": "Trahan",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/t000482_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "MA"
+  },
+  "T000486": {
+    "bioguide": "T000486",
+    "chamber": "house",
+    "full_name": "Ritchie Torres",
+    "name": "Torres (NY)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/t000486_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "T000487": {
+    "bioguide": "T000487",
+    "chamber": "house",
+    "full_name": "Jill N. Tokuda",
+    "name": "Tokuda",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/t000487_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "HI"
+  },
+  "T000488": {
+    "bioguide": "T000488",
+    "chamber": "house",
+    "full_name": "Shri Thanedar",
+    "name": "Thanedar",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/68122bf2246d1b6bd8d9f6a2_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "MI"
+  },
+  "T000489": {
+    "bioguide": "T000489",
+    "chamber": "house",
+    "full_name": "Sylvester Turner",
+    "name": "Turner (TX)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67745fdc0b34857ecc90919d_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "served_to": "2025-01-03",
+    "state": "TX"
+  },
+  "T000490": {
+    "bioguide": "T000490",
+    "chamber": "house",
+    "full_name": "David J. Taylor",
+    "name": "Taylor",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677460190b34857ecc9091a3_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "OH"
+  },
+  "T000491": {
+    "bioguide": "T000491",
+    "chamber": "house",
+    "full_name": "Derek Tran",
+    "name": "Tran",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6774606d0b34857ecc9091a9_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "U000040": {
+    "bioguide": "U000040",
+    "chamber": "house",
+    "full_name": "Lauren Underwood",
+    "name": "Underwood",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/u000040_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "IL"
+  },
+  "V000081": {
+    "bioguide": "V000081",
+    "chamber": "house",
+    "full_name": "Nydia M. Vel\u00e1zquez",
+    "name": "Velazquez",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/v000081_200.jpg",
+    "served_from": "1993-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "V000129": {
+    "bioguide": "V000129",
+    "chamber": "house",
+    "full_name": "David G. Valadao",
+    "name": "Valadao",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/v000129_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "V000130": {
+    "bioguide": "V000130",
+    "chamber": "house",
+    "full_name": "Juan Vargas",
+    "name": "Vargas",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/v000130_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "V000131": {
+    "bioguide": "V000131",
+    "chamber": "house",
+    "full_name": "Marc A. Veasey",
+    "name": "Veasey",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/v000131_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "V000133": {
+    "bioguide": "V000133",
+    "chamber": "house",
+    "full_name": "Jefferson Van Drew",
+    "name": "Van Drew",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67c0c39d53fe81a4b3c0cac1_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "NJ"
+  },
+  "V000134": {
+    "bioguide": "V000134",
+    "chamber": "house",
+    "full_name": "Beth Van Duyne",
+    "name": "Van Duyne",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677461060b34857ecc9091af_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "V000135": {
+    "bioguide": "V000135",
+    "chamber": "house",
+    "full_name": "Derrick Van Orden",
+    "name": "Van Orden",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/v000135_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "WI"
+  },
+  "V000136": {
+    "bioguide": "V000136",
+    "chamber": "house",
+    "full_name": "Gabe Vasquez",
+    "name": "Vasquez",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/v000136_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "NM"
+  },
+  "V000138": {
+    "bioguide": "V000138",
+    "chamber": "house",
+    "full_name": "Eugene Simon Vindman",
+    "name": "Vindman",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6774617a0b34857ecc9091b5_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "VA"
+  },
+  "V000139": {
+    "bioguide": "V000139",
+    "chamber": "house",
+    "full_name": "Matt Van Epps",
+    "name": "Van Epps",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/6931b7d5f478c0f1228e9abe_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "TN"
+  },
+  "W000187": {
+    "bioguide": "W000187",
+    "chamber": "house",
+    "full_name": "Maxine Waters",
+    "name": "Waters",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/w000187_200.jpg",
+    "served_from": "1991-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "W000788": {
+    "bioguide": "W000788",
+    "chamber": "house",
+    "full_name": "Nikema Williams",
+    "name": "Williams (GA)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/w000788_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "GA"
+  },
+  "W000795": {
+    "bioguide": "W000795",
+    "chamber": "house",
+    "full_name": "Joe Wilson",
+    "name": "Wilson (SC)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_sc_2_wilson_joe_200.jpg",
+    "served_from": "2001-01-03",
+    "served_partial": false,
+    "state": "SC"
+  },
+  "W000797": {
+    "bioguide": "W000797",
+    "chamber": "house",
+    "full_name": "Debbie Wasserman Schultz",
+    "name": "Wasserman Schultz",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_fl_23_wassermanschultz_debbie_200.jpg",
+    "served_from": "2005-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "W000798": {
+    "bioguide": "W000798",
+    "chamber": "house",
+    "full_name": "Tim Walberg",
+    "name": "Walberg",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/w000798_200.jpg",
+    "served_from": "2007-01-03",
+    "served_partial": false,
+    "state": "MI"
+  },
+  "W000804": {
+    "bioguide": "W000804",
+    "chamber": "house",
+    "full_name": "Robert J. Wittman",
+    "name": "Wittman",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/w000804_200.jpg",
+    "served_from": "2007-01-03",
+    "served_partial": false,
+    "state": "VA"
+  },
+  "W000806": {
+    "bioguide": "W000806",
+    "chamber": "house",
+    "full_name": "Daniel Webster",
+    "name": "Webster (FL)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/w000806_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "W000808": {
+    "bioguide": "W000808",
+    "chamber": "house",
+    "full_name": "Frederica S. Wilson",
+    "name": "Wilson (FL)",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/w000808_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "W000809": {
+    "bioguide": "W000809",
+    "chamber": "house",
+    "full_name": "Steve Womack",
+    "name": "Womack",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/117_rp_ar_3_womack_steve_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "AR"
+  },
+  "W000812": {
+    "bioguide": "W000812",
+    "chamber": "house",
+    "full_name": "Ann Wagner",
+    "name": "Wagner",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/695fc654ddd5f76cbbad2f67_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "MO"
+  },
+  "W000814": {
+    "bioguide": "W000814",
+    "chamber": "house",
+    "full_name": "Randy K. Sr. Weber",
+    "name": "Weber (TX)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/w000814_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "W000816": {
+    "bioguide": "W000816",
+    "chamber": "house",
+    "full_name": "Roger Williams",
+    "name": "Williams (TX)",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/w000816_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "W000821": {
+    "bioguide": "W000821",
+    "chamber": "house",
+    "full_name": "Bruce Westerman",
+    "name": "Westerman",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/w000821_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "AR"
+  },
+  "W000822": {
+    "bioguide": "W000822",
+    "chamber": "house",
+    "full_name": "Bonnie Watson Coleman",
+    "name": "Watson Coleman",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/w000822_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "NJ"
+  },
+  "W000823": {
+    "bioguide": "W000823",
+    "chamber": "house",
+    "full_name": "Michael Waltz",
+    "name": "Waltz",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/w000823_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "served_to": "2025-01-03",
+    "state": "FL"
+  },
+  "W000829": {
+    "bioguide": "W000829",
+    "chamber": "house",
+    "full_name": "Tony Wied",
+    "name": "Wied",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/6734b6724c72e343a6aff9e6_200.jpg",
+    "served_from": "2024-01-03",
+    "served_partial": false,
+    "state": "WI"
+  },
+  "W000830": {
+    "bioguide": "W000830",
+    "chamber": "house",
+    "full_name": "George Whitesides",
+    "name": "Whitesides",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/68dc43db199559bad714973d_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "W000831": {
+    "bioguide": "W000831",
+    "chamber": "house",
+    "full_name": "James R. Walkinshaw",
+    "name": "Walkinshaw",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/68c1bd4ca929c5b98220e069_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "VA"
+  },
+  "Y000067": {
+    "bioguide": "Y000067",
+    "chamber": "house",
+    "full_name": "Rudy Yakym",
+    "name": "Yakym",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/y000067_200.jpg",
+    "served_from": "2022-01-03",
+    "served_partial": false,
+    "state": "IN"
+  },
+  "Z000018": {
+    "bioguide": "Z000018",
+    "chamber": "house",
+    "full_name": "Ryan K. Zinke",
+    "name": "Zinke",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/117_rp_mt_1_zinke_ryan_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "MT"
+  }
+}

ファイルの差分が大きいため隠しています
+ 0 - 0
data/119/house/votes.jsonl


+ 105 - 0
data/119/lis_to_bioguide.json

@@ -0,0 +1,105 @@
+{
+  "S153": "G000386",
+  "S174": "M000355",
+  "S229": "M001111",
+  "S247": "W000779",
+  "S252": "C001035",
+  "S253": "D000563",
+  "S259": "R000122",
+  "S266": "C000880",
+  "S270": "S000148",
+  "S275": "C000127",
+  "S287": "C001056",
+  "S288": "M001153",
+  "S293": "G000359",
+  "S303": "T000250",
+  "S311": "K000367",
+  "S313": "S000033",
+  "S316": "W000802",
+  "S317": "B001261",
+  "S318": "W000437",
+  "S322": "M001176",
+  "S323": "R000584",
+  "S324": "S001181",
+  "S327": "W000805",
+  "S330": "B001267",
+  "S331": "G000555",
+  "S337": "C001088",
+  "S341": "B001277",
+  "S343": "B001236",
+  "S344": "H001061",
+  "S345": "J000293",
+  "S346": "L000577",
+  "S347": "M000934",
+  "S348": "P000603",
+  "S350": "R000595",
+  "S353": "S001194",
+  "S354": "B001230",
+  "S355": "C001098",
+  "S357": "F000463",
+  "S359": "H001046",
+  "S361": "H001042",
+  "S362": "K000384",
+  "S363": "K000383",
+  "S364": "M001169",
+  "S365": "S001184",
+  "S366": "W000817",
+  "S369": "M000133",
+  "S370": "B001288",
+  "S372": "C001047",
+  "S373": "C001075",
+  "S374": "C001095",
+  "S375": "D000618",
+  "S376": "E000295",
+  "S378": "L000575",
+  "S380": "P000595",
+  "S381": "R000605",
+  "S383": "S001198",
+  "S384": "T000476",
+  "S385": "C001113",
+  "S386": "D000622",
+  "S388": "H001076",
+  "S389": "K000393",
+  "S390": "V000128",
+  "S391": "Y000064",
+  "S394": "S001203",
+  "S395": "H001079",
+  "S396": "B001243",
+  "S398": "C001096",
+  "S399": "H001089",
+  "S402": "R000608",
+  "S404": "S001217",
+  "S406": "K000377",
+  "S407": "H000601",
+  "S408": "H000273",
+  "S409": "L000570",
+  "S410": "L000571",
+  "S411": "M001198",
+  "S412": "T000278",
+  "S413": "P000145",
+  "S414": "O000174",
+  "S415": "W000790",
+  "S416": "B001319",
+  "S417": "B001305",
+  "S418": "F000479",
+  "S419": "M001190",
+  "S420": "S001227",
+  "S421": "V000137",
+  "S422": "W000800",
+  "S423": "R000618",
+  "S426": "K000394",
+  "S427": "S001150",
+  "S428": "A000382",
+  "S429": "B001299",
+  "S430": "B001303",
+  "S431": "C001114",
+  "S432": "G000574",
+  "S433": "M001243",
+  "S434": "M001242",
+  "S435": "S001232",
+  "S436": "S001208",
+  "S437": "J000312",
+  "S438": "H001104",
+  "S439": "M001244",
+  "S440": "A000383"
+}

ファイルの差分が大きいため隠しています
+ 0 - 0
data/119/manifest.json


+ 7165 - 0
data/119/members_directory.json

@@ -0,0 +1,7165 @@
+{
+  "A000055": {
+    "bioguide": "A000055",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Robert B. Aderholt",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/a000055_200.jpg",
+    "served_from": "1997-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AL"
+  },
+  "A000148": {
+    "bioguide": "A000148",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jake Auchincloss",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67817e391f9ad6ea6fb1ebda_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MA"
+  },
+  "A000369": {
+    "bioguide": "A000369",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mark E. Amodei",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/a000369_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NV"
+  },
+  "A000370": {
+    "bioguide": "A000370",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Alma S. Adams",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/a000370_200.jpg",
+    "served_from": "2014-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NC"
+  },
+  "A000371": {
+    "bioguide": "A000371",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Pete Aguilar",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/a000371_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "A000372": {
+    "bioguide": "A000372",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Rick W. Allen",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/a000372_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "GA"
+  },
+  "A000375": {
+    "bioguide": "A000375",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jodey C. Arrington",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_tx_19_arrington_jodey_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "A000379": {
+    "bioguide": "A000379",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mark Alford",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/a000379_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MO"
+  },
+  "A000380": {
+    "bioguide": "A000380",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Gabe Amo",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/669ace45fa2fb0d731226768_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "RI"
+  },
+  "A000381": {
+    "bioguide": "A000381",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Yassamin Ansari",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67741fc30b34857ecc90902e_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AZ"
+  },
+  "A000382": {
+    "bioguide": "A000382",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Angela D. Alsobrooks",
+    "lis": "S428",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67acdbbf044eb506e25958f2_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MD"
+  },
+  "A000383": {
+    "bioguide": "A000383",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Alan Armstrong",
+    "lis": "S440",
+    "party": "R",
+    "photo_url": null,
+    "served_from": "2026-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OK"
+  },
+  "B000490": {
+    "bioguide": "B000490",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Sanford D. Bishop",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b000490_200.jpg",
+    "served_from": "1993-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "GA"
+  },
+  "B000668": {
+    "bioguide": "B000668",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Cliff Bentz",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b000668_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OR"
+  },
+  "B000740": {
+    "bioguide": "B000740",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Stephanie I. Bice",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b000740_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OK"
+  },
+  "B000825": {
+    "bioguide": "B000825",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Lauren Boebert",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b000825_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CO"
+  },
+  "B001230": {
+    "bioguide": "B001230",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Tammy Baldwin",
+    "lis": "S354",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001230_200.jpg",
+    "served_from": "1999-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WI"
+  },
+  "B001236": {
+    "bioguide": "B001236",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "John Boozman",
+    "lis": "S343",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001236_200.jpg",
+    "served_from": "2001-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AR"
+  },
+  "B001243": {
+    "bioguide": "B001243",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Marsha Blackburn",
+    "lis": "S396",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001243_200.jpg",
+    "served_from": "2003-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TN"
+  },
+  "B001257": {
+    "bioguide": "B001257",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Gus M. Bilirakis",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/117_rp_fl_12_bilirakis_gus_200.jpg",
+    "served_from": "2007-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "B001260": {
+    "bioguide": "B001260",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Vern Buchanan",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001260_200.jpg",
+    "served_from": "2007-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "B001261": {
+    "bioguide": "B001261",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "John Barrasso",
+    "lis": "S317",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001261_200.jpg",
+    "served_from": "2007-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WY"
+  },
+  "B001267": {
+    "bioguide": "B001267",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Michael F. Bennet",
+    "lis": "S330",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001267_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CO"
+  },
+  "B001277": {
+    "bioguide": "B001277",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Richard Blumenthal",
+    "lis": "S341",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001277_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CT"
+  },
+  "B001278": {
+    "bioguide": "B001278",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Suzanne Bonamici",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001278_200.jpg",
+    "served_from": "2012-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OR"
+  },
+  "B001281": {
+    "bioguide": "B001281",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Joyce Beatty",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001281_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OH"
+  },
+  "B001282": {
+    "bioguide": "B001282",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Andy Barr",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001282_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "KY"
+  },
+  "B001285": {
+    "bioguide": "B001285",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Julia Brownley",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/68000188f22eaf56065817e8_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "B001287": {
+    "bioguide": "B001287",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Ami Bera",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001287_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "B001288": {
+    "bioguide": "B001288",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Cory A. Booker",
+    "lis": "S370",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001288_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NJ"
+  },
+  "B001291": {
+    "bioguide": "B001291",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Brian Babin",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001291_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "B001292": {
+    "bioguide": "B001292",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Donald S. Beyer",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001292_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "VA"
+  },
+  "B001295": {
+    "bioguide": "B001295",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mike Bost",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001295_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IL"
+  },
+  "B001296": {
+    "bioguide": "B001296",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Brendan F. Boyle",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/69b30d648b7b84825b02d362_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "PA"
+  },
+  "B001298": {
+    "bioguide": "B001298",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Don Bacon",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_ne_2_bacon_don_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NE"
+  },
+  "B001299": {
+    "bioguide": "B001299",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Jim Banks",
+    "lis": "S429",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677d83cbfdb6cf36bbb64998_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IN"
+  },
+  "B001300": {
+    "bioguide": "B001300",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Nanette Diaz Barrag\u00e1n",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001300_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "B001301": {
+    "bioguide": "B001301",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jack Bergman",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001301_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MI"
+  },
+  "B001302": {
+    "bioguide": "B001302",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Andy Biggs",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001302_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AZ"
+  },
+  "B001303": {
+    "bioguide": "B001303",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Lisa Blunt Rochester",
+    "lis": "S430",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001303_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "DE"
+  },
+  "B001305": {
+    "bioguide": "B001305",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Ted Budd",
+    "lis": "S417",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001305_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NC"
+  },
+  "B001306": {
+    "bioguide": "B001306",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Troy Balderson",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_oh_12_balderson_troy_200.jpg",
+    "served_from": "2018-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OH"
+  },
+  "B001307": {
+    "bioguide": "B001307",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "James R. Baird",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001307_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IN"
+  },
+  "B001309": {
+    "bioguide": "B001309",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Tim Burchett",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001309_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TN"
+  },
+  "B001313": {
+    "bioguide": "B001313",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Shontel M. Brown",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001313_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OH"
+  },
+  "B001314": {
+    "bioguide": "B001314",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Aaron Bean",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001314_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "B001315": {
+    "bioguide": "B001315",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Nikki Budzinski",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001315_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IL"
+  },
+  "B001316": {
+    "bioguide": "B001316",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Eric Burlison",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001316_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MO"
+  },
+  "B001317": {
+    "bioguide": "B001317",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Josh Brecheen",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001317_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OK"
+  },
+  "B001318": {
+    "bioguide": "B001318",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Becca Balint",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001318_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "VT"
+  },
+  "B001319": {
+    "bioguide": "B001319",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Katie Boyd Britt",
+    "lis": "S416",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001319_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AL"
+  },
+  "B001321": {
+    "bioguide": "B001321",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Tom Barrett",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/6774207d0b34857ecc909034_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MI"
+  },
+  "B001322": {
+    "bioguide": "B001322",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Michael Baumgartner",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/6774212e0b34857ecc90903a_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WA"
+  },
+  "B001323": {
+    "bioguide": "B001323",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Nicholas J. Begich",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/6774217e0b34857ecc909040_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AK"
+  },
+  "B001324": {
+    "bioguide": "B001324",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Wesley Bell",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677422240b34857ecc90904a_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MO"
+  },
+  "B001325": {
+    "bioguide": "B001325",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Sheri Biggs",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677422990b34857ecc909052_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "SC"
+  },
+  "B001326": {
+    "bioguide": "B001326",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Janelle S. Bynum",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677423150b34857ecc909059_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OR"
+  },
+  "B001327": {
+    "bioguide": "B001327",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Robert P. Bresnahan",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/6774236f0b34857ecc90905f_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "PA"
+  },
+  "C000059": {
+    "bioguide": "C000059",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Ken Calvert",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c000059_200.jpg",
+    "served_from": "1993-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "C000127": {
+    "bioguide": "C000127",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Maria Cantwell",
+    "lis": "S275",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c000127_200.jpg",
+    "served_from": "1993-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WA"
+  },
+  "C000537": {
+    "bioguide": "C000537",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "James E. Clyburn",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c000537_200.jpg",
+    "served_from": "1993-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "SC"
+  },
+  "C000880": {
+    "bioguide": "C000880",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Mike Crapo",
+    "lis": "S266",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c000880_200.jpg",
+    "served_from": "1993-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "ID"
+  },
+  "C001035": {
+    "bioguide": "C001035",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Susan M. Collins",
+    "lis": "S252",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001035_200.jpg",
+    "served_from": "1997-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "ME"
+  },
+  "C001039": {
+    "bioguide": "C001039",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Kat Cammack",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001039_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "C001047": {
+    "bioguide": "C001047",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Shelley Moore Capito",
+    "lis": "S372",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001047_200.jpg",
+    "served_from": "2001-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WV"
+  },
+  "C001051": {
+    "bioguide": "C001051",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "John R. Carter",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001051_200.jpg",
+    "served_from": "2003-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "C001053": {
+    "bioguide": "C001053",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Tom Cole",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001053_200.jpg",
+    "served_from": "2003-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OK"
+  },
+  "C001055": {
+    "bioguide": "C001055",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Ed Case",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001055_200.jpg",
+    "served_from": "2002-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "HI"
+  },
+  "C001056": {
+    "bioguide": "C001056",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "John Cornyn",
+    "lis": "S287",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001056_200.jpg",
+    "served_from": "2002-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "C001059": {
+    "bioguide": "C001059",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jim Costa",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6973c909ded7781a35cc1fd7_200.jpg",
+    "served_from": "2005-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "C001061": {
+    "bioguide": "C001061",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Emanuel Cleaver",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_mo_5_cleaver_emanuel_200.jpg",
+    "served_from": "2005-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MO"
+  },
+  "C001063": {
+    "bioguide": "C001063",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Henry Cuellar",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_tx_28_cuellar_henry_200.jpg",
+    "served_from": "2005-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "C001066": {
+    "bioguide": "C001066",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Kathy Castor",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001066_200.jpg",
+    "served_from": "2007-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "C001067": {
+    "bioguide": "C001067",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Yvette D. Clarke",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/674dfc6b5c48ff736e6e1762_200.jpg",
+    "served_from": "2007-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "C001068": {
+    "bioguide": "C001068",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Steve Cohen",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001068_200.jpg",
+    "served_from": "2007-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TN"
+  },
+  "C001069": {
+    "bioguide": "C001069",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Joe Courtney",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001069_200.jpg",
+    "served_from": "2007-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CT"
+  },
+  "C001072": {
+    "bioguide": "C001072",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Andr\u00e9 Carson",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001072_200.jpg",
+    "served_from": "2008-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IN"
+  },
+  "C001075": {
+    "bioguide": "C001075",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Bill Cassidy",
+    "lis": "S373",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001075_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "LA"
+  },
+  "C001078": {
+    "bioguide": "C001078",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Gerald E. Connolly",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001078_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": "2025-01-03",
+    "source": "congress.gov/v3",
+    "state": "VA"
+  },
+  "C001080": {
+    "bioguide": "C001080",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Judy Chu",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001080_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "C001087": {
+    "bioguide": "C001087",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Eric A. \"Rick\" Crawford",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/6916065185b49de40f52cad4_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AR"
+  },
+  "C001088": {
+    "bioguide": "C001088",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Christopher A. Coons",
+    "lis": "S337",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001088_200.jpg",
+    "served_from": "2010-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "DE"
+  },
+  "C001091": {
+    "bioguide": "C001091",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Joaquin Castro",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/69d7f5b07304cef7a0c977b1_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "C001095": {
+    "bioguide": "C001095",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Tom Cotton",
+    "lis": "S374",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001095_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AR"
+  },
+  "C001096": {
+    "bioguide": "C001096",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Kevin Cramer",
+    "lis": "S398",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001096_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "ND"
+  },
+  "C001098": {
+    "bioguide": "C001098",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Ted Cruz",
+    "lis": "S355",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001098_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "C001101": {
+    "bioguide": "C001101",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Katherine M. Clark",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001101_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MA"
+  },
+  "C001103": {
+    "bioguide": "C001103",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Earl L. \"Buddy\" Carter",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001103_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "GA"
+  },
+  "C001108": {
+    "bioguide": "C001108",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "James Comer",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001108_200.jpg",
+    "served_from": "2016-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "KY"
+  },
+  "C001110": {
+    "bioguide": "C001110",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "J. Luis Correa",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_ca_46_correa_j_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "C001112": {
+    "bioguide": "C001112",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Salud O. Carbajal",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_ca_24_carbajal_salud_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "C001113": {
+    "bioguide": "C001113",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Catherine Cortez Masto",
+    "lis": "S385",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001113_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NV"
+  },
+  "C001114": {
+    "bioguide": "C001114",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "John R. Curtis",
+    "lis": "S431",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/1b9d1007d6895a37da28a67cd8149803_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "UT"
+  },
+  "C001115": {
+    "bioguide": "C001115",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Michael Cloud",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_tx_27_cloud_michael_200.jpg",
+    "served_from": "2018-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "C001116": {
+    "bioguide": "C001116",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Andrew S. Clyde",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001116_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "GA"
+  },
+  "C001117": {
+    "bioguide": "C001117",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Sean Casten",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001117_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IL"
+  },
+  "C001118": {
+    "bioguide": "C001118",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Ben Cline",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/69825a92eb30d32718993747_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "VA"
+  },
+  "C001119": {
+    "bioguide": "C001119",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Angie Craig",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001119_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MN"
+  },
+  "C001120": {
+    "bioguide": "C001120",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Dan Crenshaw",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/698a13d0a7af728d4e6c2038_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "C001121": {
+    "bioguide": "C001121",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jason Crow",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/69d7f4167304cef7a0c9779f_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CO"
+  },
+  "C001123": {
+    "bioguide": "C001123",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Gilbert Ray Cisneros",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6807d8d63e52ea7df920ef05_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "C001125": {
+    "bioguide": "C001125",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Troy A. Carter",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001125_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "LA"
+  },
+  "C001126": {
+    "bioguide": "C001126",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mike Carey",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001126_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OH"
+  },
+  "C001127": {
+    "bioguide": "C001127",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Sheila Cherfilus-McCormick",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001127_200.jpg",
+    "served_from": "2022-01-03",
+    "served_to": "2026-01-03",
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "C001129": {
+    "bioguide": "C001129",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mike Collins",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001129_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "GA"
+  },
+  "C001130": {
+    "bioguide": "C001130",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jasmine Crockett",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001130_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "C001131": {
+    "bioguide": "C001131",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Greg Casar",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001131_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "C001132": {
+    "bioguide": "C001132",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Elijah Crane",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001132_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AZ"
+  },
+  "C001133": {
+    "bioguide": "C001133",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Juan Ciscomani",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001133_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AZ"
+  },
+  "C001136": {
+    "bioguide": "C001136",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Herbert C. Conaway",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6774243a0b34857ecc90906b_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NJ"
+  },
+  "C001137": {
+    "bioguide": "C001137",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jeff Crank",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677424810b34857ecc909071_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CO"
+  },
+  "D000032": {
+    "bioguide": "D000032",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Byron Donalds",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/d000032_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "D000096": {
+    "bioguide": "D000096",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Danny K. Davis",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_il_7_davis_danny_200.jpg",
+    "served_from": "1997-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IL"
+  },
+  "D000197": {
+    "bioguide": "D000197",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Diana DeGette",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_co_1_degette_diana_200.jpg",
+    "served_from": "1997-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CO"
+  },
+  "D000216": {
+    "bioguide": "D000216",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Rosa L. DeLauro",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_ct_3_delauro_rosa_200.jpg",
+    "served_from": "1991-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CT"
+  },
+  "D000230": {
+    "bioguide": "D000230",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Donald G. Davis",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/d000230_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NC"
+  },
+  "D000399": {
+    "bioguide": "D000399",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Lloyd Doggett",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/d000399_200.jpg",
+    "served_from": "1995-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "D000530": {
+    "bioguide": "D000530",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Christopher R. Deluzio",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/d000530_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "PA"
+  },
+  "D000563": {
+    "bioguide": "D000563",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Richard J. Durbin",
+    "lis": "S253",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/d000563_200.jpg",
+    "served_from": "1983-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IL"
+  },
+  "D000594": {
+    "bioguide": "D000594",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Monica De La Cruz",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/66d9fd54f440bf1dc174fbf6_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "D000600": {
+    "bioguide": "D000600",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mario Diaz-Balart",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_fl_25_diazbalart_mario_200.jpg",
+    "served_from": "2003-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "D000616": {
+    "bioguide": "D000616",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Scott DesJarlais",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/d000616_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TN"
+  },
+  "D000617": {
+    "bioguide": "D000617",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Suzan K. DelBene",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/d000617_200.jpg",
+    "served_from": "2012-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WA"
+  },
+  "D000618": {
+    "bioguide": "D000618",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Steve Daines",
+    "lis": "S375",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/d000618_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MT"
+  },
+  "D000622": {
+    "bioguide": "D000622",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Tammy Duckworth",
+    "lis": "S386",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/d000622_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IL"
+  },
+  "D000623": {
+    "bioguide": "D000623",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mark DeSaulnier",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_ca_11_desaulnier_mark_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "D000624": {
+    "bioguide": "D000624",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Debbie Dingell",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/d000624_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MI"
+  },
+  "D000626": {
+    "bioguide": "D000626",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Warren Davidson",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_oh_8_davidson_warren_200.jpg",
+    "served_from": "2016-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OH"
+  },
+  "D000628": {
+    "bioguide": "D000628",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Neal P. Dunn",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_fl_2_dunn_neal_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "D000629": {
+    "bioguide": "D000629",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Sharice Davids",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/d000629_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "KS"
+  },
+  "D000631": {
+    "bioguide": "D000631",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Madeleine Dean",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/d000631_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "PA"
+  },
+  "D000634": {
+    "bioguide": "D000634",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Troy Downing",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677424da0b34857ecc909077_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MT"
+  },
+  "D000635": {
+    "bioguide": "D000635",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Maxine Dexter",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677425260b34857ecc90907d_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OR"
+  },
+  "E000071": {
+    "bioguide": "E000071",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jake Ellzey",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/e000071_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "E000235": {
+    "bioguide": "E000235",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mike Ezell",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/e000235_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MS"
+  },
+  "E000246": {
+    "bioguide": "E000246",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Chuck Edwards",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/e000246_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NC"
+  },
+  "E000294": {
+    "bioguide": "E000294",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Tom Emmer",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/e000294_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MN"
+  },
+  "E000295": {
+    "bioguide": "E000295",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Joni Ernst",
+    "lis": "S376",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/e000295_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IA"
+  },
+  "E000296": {
+    "bioguide": "E000296",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Dwight Evans",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_pa_2_evans_dwight_200.jpg",
+    "served_from": "2016-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "PA"
+  },
+  "E000297": {
+    "bioguide": "E000297",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Adriano Espaillat",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/e000297_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "E000298": {
+    "bioguide": "E000298",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Ron Estes",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/e000298_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "KS"
+  },
+  "E000299": {
+    "bioguide": "E000299",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Veronica Escobar",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/e000299_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "E000300": {
+    "bioguide": "E000300",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Gabe Evans",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677425730b34857ecc909083_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CO"
+  },
+  "E000301": {
+    "bioguide": "E000301",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Sarah Elfreth",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677425dc0b34857ecc909089_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MD"
+  },
+  "F000110": {
+    "bioguide": "F000110",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Cleo Fields",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677426250b34857ecc90908f_200.jpg",
+    "served_from": "1993-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "LA"
+  },
+  "F000246": {
+    "bioguide": "F000246",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Pat Fallon",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/f000246_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "F000446": {
+    "bioguide": "F000446",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Randy Feenstra",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/f000446_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IA"
+  },
+  "F000450": {
+    "bioguide": "F000450",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Virginia Foxx",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_nc_5_foxx_virginia_200.jpg",
+    "served_from": "2005-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NC"
+  },
+  "F000454": {
+    "bioguide": "F000454",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Bill Foster",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/f000454_200.jpg",
+    "served_from": "2008-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IL"
+  },
+  "F000459": {
+    "bioguide": "F000459",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Charles J. \"Chuck\" Fleischmann",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/f000459_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TN"
+  },
+  "F000462": {
+    "bioguide": "F000462",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Lois Frankel",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/f000462_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "F000463": {
+    "bioguide": "F000463",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Deb Fischer",
+    "lis": "S357",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/669ec7925d19788d1f2034a1_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NE"
+  },
+  "F000466": {
+    "bioguide": "F000466",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Brian K. Fitzpatrick",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_pa_1_fitzpatrick_brian_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "PA"
+  },
+  "F000468": {
+    "bioguide": "F000468",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Lizzie Fletcher",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67813f931f9ad6ea6fb1eb6f_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "F000469": {
+    "bioguide": "F000469",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Russ Fulcher",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/f000469_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "ID"
+  },
+  "F000470": {
+    "bioguide": "F000470",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Michelle Fischbach",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/f000470_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MN"
+  },
+  "F000471": {
+    "bioguide": "F000471",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Scott Fitzgerald",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/f000471_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WI"
+  },
+  "F000472": {
+    "bioguide": "F000472",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Scott Franklin",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/f000472_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "F000474": {
+    "bioguide": "F000474",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mike Flood",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67b4de4a61bd80d04553b0a5_200.jpg",
+    "served_from": "2022-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NE"
+  },
+  "F000475": {
+    "bioguide": "F000475",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Brad Finstad",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/f000475_200.jpg",
+    "served_from": "2022-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MN"
+  },
+  "F000476": {
+    "bioguide": "F000476",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Maxwell Frost",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/69a71e9a55f11d8282dde7a0_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "F000477": {
+    "bioguide": "F000477",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Valerie P. Foushee",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/68122e57246d1b6bd8d9f6ab_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NC"
+  },
+  "F000478": {
+    "bioguide": "F000478",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Russell Fry",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/f000478_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "SC"
+  },
+  "F000479": {
+    "bioguide": "F000479",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "John Fetterman",
+    "lis": "S418",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/f000479_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "PA"
+  },
+  "F000480": {
+    "bioguide": "F000480",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Vince Fong",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/669ff04f5d19788d1f2034aa_200.jpg",
+    "served_from": "2024-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "F000481": {
+    "bioguide": "F000481",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Shomari Figures",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/68013eaa4e51529406f18e42_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AL"
+  },
+  "F000482": {
+    "bioguide": "F000482",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Julie Fedorchak",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677426c20b34857ecc90909b_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "ND"
+  },
+  "F000483": {
+    "bioguide": "F000483",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Laura Friedman",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6a0f0dcb69b12040cf77e337_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "F000484": {
+    "bioguide": "F000484",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Randy Fine",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67efda8c1b05a5a598f7fde0_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "F000485": {
+    "bioguide": "F000485",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Clay Fuller",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/69eb95e5277642362f6112c5_200.jpg",
+    "served_from": "2026-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "GA"
+  },
+  "G000359": {
+    "bioguide": "G000359",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Lindsey Graham",
+    "lis": "S293",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000359_200.jpg",
+    "served_from": "1995-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "SC"
+  },
+  "G000386": {
+    "bioguide": "G000386",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Chuck Grassley",
+    "lis": "S153",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000386_200.jpg",
+    "served_from": "1975-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IA"
+  },
+  "G000546": {
+    "bioguide": "G000546",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Sam Graves",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000546_200.jpg",
+    "served_from": "2001-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MO"
+  },
+  "G000551": {
+    "bioguide": "G000551",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Ra\u00fal M. Grijalva",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/g000551_200.jpg",
+    "served_from": "2003-01-03",
+    "served_to": "2025-01-03",
+    "source": "congress.gov/v3",
+    "state": "AZ"
+  },
+  "G000553": {
+    "bioguide": "G000553",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Al Green",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_tx_9_green_al_200.jpg",
+    "served_from": "2005-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "G000555": {
+    "bioguide": "G000555",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Kirsten E. Gillibrand",
+    "lis": "S331",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/g000555_200.jpg",
+    "served_from": "2007-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "G000558": {
+    "bioguide": "G000558",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Brett Guthrie",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000558_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "KY"
+  },
+  "G000559": {
+    "bioguide": "G000559",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "John Garamendi",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/g000559_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "G000565": {
+    "bioguide": "G000565",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Paul A. Gosar",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000565_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AZ"
+  },
+  "G000568": {
+    "bioguide": "G000568",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "H. Morgan Griffith",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/68094df86c2e6631263de737_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "VA"
+  },
+  "G000574": {
+    "bioguide": "G000574",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Ruben Gallego",
+    "lis": "S432",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c6870f487cf4bc9a568d8afbe61d754b_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AZ"
+  },
+  "G000576": {
+    "bioguide": "G000576",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Glenn Grothman",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_wi_6_grothman_glenn_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WI"
+  },
+  "G000581": {
+    "bioguide": "G000581",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Vicente Gonzalez",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_tx_15_gonzalez_vicente_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "G000583": {
+    "bioguide": "G000583",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Josh Gottheimer",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_nj_5_gottheimer_josh_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NJ"
+  },
+  "G000585": {
+    "bioguide": "G000585",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jimmy Gomez",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_ca_34_gomez_jimmy_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "G000586": {
+    "bioguide": "G000586",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jes\u00fas G. \"Chuy\" Garc\u00eda",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/g000586_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IL"
+  },
+  "G000587": {
+    "bioguide": "G000587",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Sylvia R. Garcia",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/g000587_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "G000589": {
+    "bioguide": "G000589",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Lance Gooden",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000589_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "G000590": {
+    "bioguide": "G000590",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mark E. Green",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000590_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": "2025-01-03",
+    "source": "congress.gov/v3",
+    "state": "TN"
+  },
+  "G000591": {
+    "bioguide": "G000591",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Michael Guest",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000591_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MS"
+  },
+  "G000592": {
+    "bioguide": "G000592",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jared F. Golden",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/g000592_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "ME"
+  },
+  "G000593": {
+    "bioguide": "G000593",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Carlos A. Gimenez",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000593_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "G000594": {
+    "bioguide": "G000594",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Tony Gonzales",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000594_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": "2026-01-03",
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "G000596": {
+    "bioguide": "G000596",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Marjorie Taylor Greene",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000596_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": "2026-01-03",
+    "source": "congress.gov/v3",
+    "state": "GA"
+  },
+  "G000597": {
+    "bioguide": "G000597",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Andrew R. Garbarino",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000597_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "G000598": {
+    "bioguide": "G000598",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Robert Garcia",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/g000598_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "G000599": {
+    "bioguide": "G000599",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Daniel S. Goldman",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/g000599_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "G000600": {
+    "bioguide": "G000600",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Marie Gluesenkamp Perez",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/g000600_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WA"
+  },
+  "G000601": {
+    "bioguide": "G000601",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Craig A. Goldman",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/6774277d0b34857ecc9090a7_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "G000602": {
+    "bioguide": "G000602",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Laura Gillen",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677427de0b34857ecc9090ad_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "G000603": {
+    "bioguide": "G000603",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Brandon Gill",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677428ac0b34857ecc9090b3_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "G000604": {
+    "bioguide": "G000604",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Maggie Goodlander",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/678ff62d66bf616cf1a7ce13_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NH"
+  },
+  "G000605": {
+    "bioguide": "G000605",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Adam Gray",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67742a160b34857ecc9090bf_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "G000606": {
+    "bioguide": "G000606",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Adelita S. Grijalva",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6915fa2585b49de40f52cacb_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AZ"
+  },
+  "H000273": {
+    "bioguide": "H000273",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "John W. Hickenlooper",
+    "lis": "S408",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/h000273_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CO"
+  },
+  "H000601": {
+    "bioguide": "H000601",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Bill Hagerty",
+    "lis": "S407",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h000601_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TN"
+  },
+  "H000874": {
+    "bioguide": "H000874",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Steny H. Hoyer",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_md_5_hoyer_steny_200.jpg",
+    "served_from": "1981-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MD"
+  },
+  "H001042": {
+    "bioguide": "H001042",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Mazie K. Hirono",
+    "lis": "S361",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/h001042_200.jpg",
+    "served_from": "2007-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "HI"
+  },
+  "H001046": {
+    "bioguide": "H001046",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Martin Heinrich",
+    "lis": "S359",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/h001046_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NM"
+  },
+  "H001047": {
+    "bioguide": "H001047",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "James A. Himes",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/h001047_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CT"
+  },
+  "H001052": {
+    "bioguide": "H001052",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Andy Harris",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/117_rp_md_1_harris_andy_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MD"
+  },
+  "H001058": {
+    "bioguide": "H001058",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Bill Huizenga",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001058_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MI"
+  },
+  "H001061": {
+    "bioguide": "H001061",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "John Hoeven",
+    "lis": "S344",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001061_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "ND"
+  },
+  "H001066": {
+    "bioguide": "H001066",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Steven Horsford",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/h001066_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NV"
+  },
+  "H001067": {
+    "bioguide": "H001067",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Richard Hudson",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001067_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NC"
+  },
+  "H001068": {
+    "bioguide": "H001068",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jared Huffman",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/h001068_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "H001072": {
+    "bioguide": "H001072",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "J. French Hill",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001072_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AR"
+  },
+  "H001076": {
+    "bioguide": "H001076",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Margaret Wood Hassan",
+    "lis": "S388",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/h001076_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NH"
+  },
+  "H001077": {
+    "bioguide": "H001077",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Clay Higgins",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001077_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "LA"
+  },
+  "H001079": {
+    "bioguide": "H001079",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Cindy Hyde-Smith",
+    "lis": "S395",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001079_200.jpg",
+    "served_from": "2018-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MS"
+  },
+  "H001081": {
+    "bioguide": "H001081",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jahana Hayes",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/h001081_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CT"
+  },
+  "H001082": {
+    "bioguide": "H001082",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Kevin Hern",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001082_200.jpg",
+    "served_from": "2018-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OK"
+  },
+  "H001085": {
+    "bioguide": "H001085",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Chrissy Houlahan",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/681bc0e6b763f94d6e471f50_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "PA"
+  },
+  "H001086": {
+    "bioguide": "H001086",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Diana Harshbarger",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001086_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TN"
+  },
+  "H001089": {
+    "bioguide": "H001089",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Josh Hawley",
+    "lis": "S399",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001089_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MO"
+  },
+  "H001090": {
+    "bioguide": "H001090",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Josh Harder",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/h001090_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "H001091": {
+    "bioguide": "H001091",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Ashley Hinson",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677ed0c7514c773869b6b920_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IA"
+  },
+  "H001093": {
+    "bioguide": "H001093",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Erin Houchin",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001093_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IN"
+  },
+  "H001094": {
+    "bioguide": "H001094",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Val T. Hoyle",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/h001094_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OR"
+  },
+  "H001095": {
+    "bioguide": "H001095",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Wesley Hunt",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001095_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "H001096": {
+    "bioguide": "H001096",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Harriet M. Hageman",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001096_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WY"
+  },
+  "H001098": {
+    "bioguide": "H001098",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Abraham J. Hamadeh",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67742a6f0b34857ecc9090c5_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AZ"
+  },
+  "H001099": {
+    "bioguide": "H001099",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mike Haridopolos",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67742ab50b34857ecc9090cb_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "H001100": {
+    "bioguide": "H001100",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jeff Hurd",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67742c5e0b34857ecc9090d1_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CO"
+  },
+  "H001101": {
+    "bioguide": "H001101",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Pat Harrigan",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67742ca40b34857ecc9090d7_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NC"
+  },
+  "H001102": {
+    "bioguide": "H001102",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mark Harris",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67742d1b0b34857ecc9090dd_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NC"
+  },
+  "H001103": {
+    "bioguide": "H001103",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Pablo Jose Hern\u00e1ndez",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67742d980b34857ecc9090e3_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "PR"
+  },
+  "H001104": {
+    "bioguide": "H001104",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Jon Husted",
+    "lis": "S438",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67f0316b1b05a5a598f7fdf3_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OH"
+  },
+  "I000056": {
+    "bioguide": "I000056",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Darrell Issa",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/i000056_200.jpg",
+    "served_from": "2001-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "I000058": {
+    "bioguide": "I000058",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Glenn Ivey",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/i000058_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MD"
+  },
+  "J000288": {
+    "bioguide": "J000288",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Henry C. \"Hank\" Johnson",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/j000288_200.jpg",
+    "served_from": "2007-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "GA"
+  },
+  "J000289": {
+    "bioguide": "J000289",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jim Jordan",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/j000289_200.jpg",
+    "served_from": "2007-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OH"
+  },
+  "J000293": {
+    "bioguide": "J000293",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Ron Johnson",
+    "lis": "S345",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/j000293_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WI"
+  },
+  "J000294": {
+    "bioguide": "J000294",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Hakeem S. Jeffries",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/j000294_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "J000295": {
+    "bioguide": "J000295",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "David P. Joyce",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/j000295_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OH"
+  },
+  "J000298": {
+    "bioguide": "J000298",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Pramila Jayapal",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_wa_7_jayapal_pramila_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WA"
+  },
+  "J000299": {
+    "bioguide": "J000299",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mike Johnson",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67ffcb2af22eaf56065817c4_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "LA"
+  },
+  "J000301": {
+    "bioguide": "J000301",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Dusty Johnson",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/j000301_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "SD"
+  },
+  "J000302": {
+    "bioguide": "J000302",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "John Joyce",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/j000302_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "PA"
+  },
+  "J000304": {
+    "bioguide": "J000304",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Ronny Jackson",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/j000304_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "J000305": {
+    "bioguide": "J000305",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Sara Jacobs",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/j000305_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "J000307": {
+    "bioguide": "J000307",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "John James",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/j000307_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MI"
+  },
+  "J000309": {
+    "bioguide": "J000309",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jonathan L. Jackson",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/j000309_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IL"
+  },
+  "J000310": {
+    "bioguide": "J000310",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Julie Johnson",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67742df60b34857ecc9090e9_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "J000311": {
+    "bioguide": "J000311",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Brian Jack",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67742e330b34857ecc9090ef_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "GA"
+  },
+  "J000312": {
+    "bioguide": "J000312",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "James C. Justice",
+    "lis": "S437",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67c86b5e6159152e59828b1a_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WV"
+  },
+  "K000009": {
+    "bioguide": "K000009",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Marcy Kaptur",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/k000009_200.jpg",
+    "served_from": "1983-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OH"
+  },
+  "K000367": {
+    "bioguide": "K000367",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Amy Klobuchar",
+    "lis": "S311",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/k000367_200.jpg",
+    "served_from": "2007-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MN"
+  },
+  "K000375": {
+    "bioguide": "K000375",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "William R. Keating",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/k000375_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MA"
+  },
+  "K000376": {
+    "bioguide": "K000376",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mike Kelly",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/k000376_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "PA"
+  },
+  "K000377": {
+    "bioguide": "K000377",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Mark Kelly",
+    "lis": "S406",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/k000377_200.jpg",
+    "served_from": "2020-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AZ"
+  },
+  "K000383": {
+    "bioguide": "K000383",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Angus S., Jr. King",
+    "lis": "S363",
+    "party": "I",
+    "photo_url": "https://www.congress.gov/img/member/k000383_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "ME"
+  },
+  "K000384": {
+    "bioguide": "K000384",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Tim Kaine",
+    "lis": "S362",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/k000384_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "VA"
+  },
+  "K000385": {
+    "bioguide": "K000385",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Robin L. Kelly",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_il_2_kelly_robin_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IL"
+  },
+  "K000388": {
+    "bioguide": "K000388",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Trent Kelly",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_ms_1_kelly_trent_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MS"
+  },
+  "K000389": {
+    "bioguide": "K000389",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Ro Khanna",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/k000389_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "K000391": {
+    "bioguide": "K000391",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Raja Krishnamoorthi",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/k000391_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IL"
+  },
+  "K000392": {
+    "bioguide": "K000392",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "David Kustoff",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_tn_8_kustoff_david_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TN"
+  },
+  "K000393": {
+    "bioguide": "K000393",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "John Kennedy",
+    "lis": "S389",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/k000393_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "LA"
+  },
+  "K000394": {
+    "bioguide": "K000394",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Andy Kim",
+    "lis": "S426",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677d84cbfdb6cf36bbb649a1_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NJ"
+  },
+  "K000397": {
+    "bioguide": "K000397",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Young Kim",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/k000397_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "K000398": {
+    "bioguide": "K000398",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Thomas H. Kean",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/k000398_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NJ"
+  },
+  "K000399": {
+    "bioguide": "K000399",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jennifer A. Kiggans",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/66b0ce45b0288a917d98f619_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "VA"
+  },
+  "K000400": {
+    "bioguide": "K000400",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Sydney Kamlager-Dove",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/k000400_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "K000401": {
+    "bioguide": "K000401",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Kevin Kiley",
+    "lis": null,
+    "party": "I",
+    "photo_url": "https://www.congress.gov/img/member/k000401_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "K000402": {
+    "bioguide": "K000402",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Timothy M. Kennedy",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/668e9306658443009a697c8c_200.jpg",
+    "served_from": "2024-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "K000403": {
+    "bioguide": "K000403",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mike Kennedy",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67742e7c0b34857ecc9090f5_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "UT"
+  },
+  "K000404": {
+    "bioguide": "K000404",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Kimberlyn King-Hinds",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67742f0a0b34857ecc9090fb_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MP"
+  },
+  "K000405": {
+    "bioguide": "K000405",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Brad Knott",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67742fc60b34857ecc909101_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NC"
+  },
+  "L000273": {
+    "bioguide": "L000273",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Teresa Leger Fernandez",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/l000273_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NM"
+  },
+  "L000397": {
+    "bioguide": "L000397",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Zoe Lofgren",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/671024d7ec807bca66057fcb_200.jpg",
+    "served_from": "1995-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "L000491": {
+    "bioguide": "L000491",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Frank D. Lucas",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_ok_3_lucas_frank_200.jpg",
+    "served_from": "1993-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OK"
+  },
+  "L000557": {
+    "bioguide": "L000557",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "John B. Larson",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/l000557_200.jpg",
+    "served_from": "1999-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CT"
+  },
+  "L000560": {
+    "bioguide": "L000560",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Rick Larsen",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_wa_2_larsen_rick_200.jpg",
+    "served_from": "2001-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WA"
+  },
+  "L000562": {
+    "bioguide": "L000562",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Stephen F. Lynch",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/l000562_200.jpg",
+    "served_from": "2001-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MA"
+  },
+  "L000566": {
+    "bioguide": "L000566",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Robert E. Latta",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/6984c938b1dfe04d989a1e21_200.jpg",
+    "served_from": "2007-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OH"
+  },
+  "L000570": {
+    "bioguide": "L000570",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Ben Ray Luj\u00e1n",
+    "lis": "S409",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/l000570_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NM"
+  },
+  "L000571": {
+    "bioguide": "L000571",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Cynthia M. Lummis",
+    "lis": "S410",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/l000571_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WY"
+  },
+  "L000575": {
+    "bioguide": "L000575",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "James Lankford",
+    "lis": "S378",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/l000575_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OK"
+  },
+  "L000577": {
+    "bioguide": "L000577",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Mike Lee",
+    "lis": "S346",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/l000577_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "UT"
+  },
+  "L000578": {
+    "bioguide": "L000578",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Doug LaMalfa",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/l000578_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": "2026-01-03",
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "L000582": {
+    "bioguide": "L000582",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Ted Lieu",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/l000582_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "L000583": {
+    "bioguide": "L000583",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Barry Loudermilk",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_ga_11_loudermilk_barry_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "GA"
+  },
+  "L000585": {
+    "bioguide": "L000585",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Darin LaHood",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_il_18_lahood_darin_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IL"
+  },
+  "L000590": {
+    "bioguide": "L000590",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Susie Lee",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/l000590_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NV"
+  },
+  "L000593": {
+    "bioguide": "L000593",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mike Levin",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/l000593_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "L000595": {
+    "bioguide": "L000595",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Julia Letlow",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/l000595_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "LA"
+  },
+  "L000596": {
+    "bioguide": "L000596",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Anna Paulina Luna",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/l000596_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "L000597": {
+    "bioguide": "L000597",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Laurel M. Lee",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/l000597_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "L000598": {
+    "bioguide": "L000598",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Nick LaLota",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/l000598_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "L000599": {
+    "bioguide": "L000599",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Michael Lawler",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/l000599_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "L000600": {
+    "bioguide": "L000600",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Nicholas A. Langworthy",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/680901e76c2e6631263de716_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "L000601": {
+    "bioguide": "L000601",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Greg Landsman",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/l000601_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OH"
+  },
+  "L000602": {
+    "bioguide": "L000602",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Summer L. Lee",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/l000602_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "PA"
+  },
+  "L000603": {
+    "bioguide": "L000603",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Morgan Luttrell",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/l000603_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "L000606": {
+    "bioguide": "L000606",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "George Latimer",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6774301b0b34857ecc909107_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "L000607": {
+    "bioguide": "L000607",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Sam T. Liccardo",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6774305d0b34857ecc90910d_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "M000133": {
+    "bioguide": "M000133",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Edward J. Markey",
+    "lis": "S369",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m000133_200.jpg",
+    "served_from": "1977-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MA"
+  },
+  "M000194": {
+    "bioguide": "M000194",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Nancy Mace",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m000194_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "SC"
+  },
+  "M000312": {
+    "bioguide": "M000312",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "James P. McGovern",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/117_rp_ma_2_mcgovern_james_200.jpg",
+    "served_from": "1997-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MA"
+  },
+  "M000317": {
+    "bioguide": "M000317",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Nicole Malliotakis",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m000317_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "M000355": {
+    "bioguide": "M000355",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Mitch McConnell",
+    "lis": "S174",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m000355_200.jpg",
+    "served_from": "1985-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "KY"
+  },
+  "M000687": {
+    "bioguide": "M000687",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Kweisi Mfume",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m000687_200.jpg",
+    "served_from": "1987-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MD"
+  },
+  "M000871": {
+    "bioguide": "M000871",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Tracey Mann",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m000871_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "KS"
+  },
+  "M000934": {
+    "bioguide": "M000934",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Jerry Moran",
+    "lis": "S347",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m000934_200.jpg",
+    "served_from": "1997-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "KS"
+  },
+  "M001111": {
+    "bioguide": "M001111",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Patty Murray",
+    "lis": "S229",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/1b52ad6d215684d847d1c2bf28b9b262_200.jpg",
+    "served_from": "1993-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WA"
+  },
+  "M001136": {
+    "bioguide": "M001136",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Lisa C. McClain",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677836d41658e791a384976d_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MI"
+  },
+  "M001137": {
+    "bioguide": "M001137",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Gregory W. Meeks",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001137_200.jpg",
+    "served_from": "1997-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "M001143": {
+    "bioguide": "M001143",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Betty McCollum",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_mn_4_mccollum_betty_200.jpg",
+    "served_from": "2001-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MN"
+  },
+  "M001153": {
+    "bioguide": "M001153",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Lisa Murkowski",
+    "lis": "S288",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001153_200.jpg",
+    "served_from": "2002-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AK"
+  },
+  "M001157": {
+    "bioguide": "M001157",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Michael T. McCaul",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_tx_10_mccaul_michael_200.jpg",
+    "served_from": "2005-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "M001160": {
+    "bioguide": "M001160",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Gwen Moore",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_wi_4_moore_gwen_200.jpg",
+    "served_from": "2005-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WI"
+  },
+  "M001163": {
+    "bioguide": "M001163",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Doris O. Matsui",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001163_200.jpg",
+    "served_from": "2005-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "M001169": {
+    "bioguide": "M001169",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Christopher Murphy",
+    "lis": "S364",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001169_200.jpg",
+    "served_from": "2007-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CT"
+  },
+  "M001176": {
+    "bioguide": "M001176",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Jeff Merkley",
+    "lis": "S322",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001176_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OR"
+  },
+  "M001177": {
+    "bioguide": "M001177",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Tom McClintock",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001177_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "M001184": {
+    "bioguide": "M001184",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Thomas Massie",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001184_200.jpg",
+    "served_from": "2012-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "KY"
+  },
+  "M001188": {
+    "bioguide": "M001188",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Grace Meng",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001188_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "M001190": {
+    "bioguide": "M001190",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Markwayne Mullin",
+    "lis": "S419",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001190_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": "2026-01-03",
+    "source": "congress.gov/v3",
+    "state": "OK"
+  },
+  "M001194": {
+    "bioguide": "M001194",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "John R. Moolenaar",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001194_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MI"
+  },
+  "M001196": {
+    "bioguide": "M001196",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Seth Moulton",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001196_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MA"
+  },
+  "M001198": {
+    "bioguide": "M001198",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Roger Marshall",
+    "lis": "S411",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001198_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "KS"
+  },
+  "M001199": {
+    "bioguide": "M001199",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Brian J. Mast",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_fl_18_mast_brian_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "M001204": {
+    "bioguide": "M001204",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Daniel Meuser",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/6776ebbf4f8a93753830ca7d_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "PA"
+  },
+  "M001205": {
+    "bioguide": "M001205",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Carol D. Miller",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001205_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WV"
+  },
+  "M001206": {
+    "bioguide": "M001206",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Joseph D. Morelle",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67ffc962f22eaf56065817bb_200.jpg",
+    "served_from": "2018-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "M001208": {
+    "bioguide": "M001208",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Lucy McBath",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001208_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "GA"
+  },
+  "M001210": {
+    "bioguide": "M001210",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Gregory F. Murphy",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001210_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NC"
+  },
+  "M001211": {
+    "bioguide": "M001211",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mary E. Miller",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001211_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IL"
+  },
+  "M001212": {
+    "bioguide": "M001212",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Barry Moore",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001212_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AL"
+  },
+  "M001213": {
+    "bioguide": "M001213",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Blake D. Moore",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001213_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "UT"
+  },
+  "M001214": {
+    "bioguide": "M001214",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Frank J. Mrvan",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001214_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IN"
+  },
+  "M001215": {
+    "bioguide": "M001215",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mariannette Miller-Meeks",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001215_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IA"
+  },
+  "M001216": {
+    "bioguide": "M001216",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Cory Mills",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001216_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "M001217": {
+    "bioguide": "M001217",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jared Moskowitz",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001217_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "M001218": {
+    "bioguide": "M001218",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Richard McCormick",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001218_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "GA"
+  },
+  "M001219": {
+    "bioguide": "M001219",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "James C. Moylan",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001219_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "GU"
+  },
+  "M001220": {
+    "bioguide": "M001220",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Morgan McGarvey",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001220_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "KY"
+  },
+  "M001222": {
+    "bioguide": "M001222",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Max L. Miller",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/680908fb6c2e6631263de71f_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OH"
+  },
+  "M001223": {
+    "bioguide": "M001223",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Seth Magaziner",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001223_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "RI"
+  },
+  "M001224": {
+    "bioguide": "M001224",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Nathaniel Moran",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/680008c5f22eaf56065817f4_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "M001225": {
+    "bioguide": "M001225",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Kevin Mullin",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001225_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "M001226": {
+    "bioguide": "M001226",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Robert Menendez",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/681231f6246d1b6bd8d9f6b4_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NJ"
+  },
+  "M001227": {
+    "bioguide": "M001227",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jennifer L. McClellan",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001227_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "VA"
+  },
+  "M001228": {
+    "bioguide": "M001228",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Celeste Maloy",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001228_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "UT"
+  },
+  "M001229": {
+    "bioguide": "M001229",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "LaMonica McIver",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/681dfed94fc893ce843e24b8_200.jpg",
+    "served_from": "2024-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NJ"
+  },
+  "M001230": {
+    "bioguide": "M001230",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Ryan Mackenzie",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677430a40b34857ecc909113_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "PA"
+  },
+  "M001231": {
+    "bioguide": "M001231",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "John W. Mannion",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677446860b34857ecc909119_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "M001232": {
+    "bioguide": "M001232",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "April McClain Delaney",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677446d80b34857ecc90911f_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MD"
+  },
+  "M001233": {
+    "bioguide": "M001233",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mark B. Messmer",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677448630b34857ecc909125_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IN"
+  },
+  "M001234": {
+    "bioguide": "M001234",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Kelly Morrison",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677449a70b34857ecc90912b_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MN"
+  },
+  "M001235": {
+    "bioguide": "M001235",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Riley M. Moore",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677449fd0b34857ecc909131_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WV"
+  },
+  "M001236": {
+    "bioguide": "M001236",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Tim Moore",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67744a540b34857ecc909137_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NC"
+  },
+  "M001237": {
+    "bioguide": "M001237",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Kristen McDonald Rivet",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/69d7f4ee7304cef7a0c977a8_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MI"
+  },
+  "M001238": {
+    "bioguide": "M001238",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Sarah McBride",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67744b460b34857ecc909143_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "DE"
+  },
+  "M001239": {
+    "bioguide": "M001239",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "John J. McGuire",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67744ba20b34857ecc909149_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "VA"
+  },
+  "M001240": {
+    "bioguide": "M001240",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Addison P. McDowell",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67744e930b34857ecc90914f_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NC"
+  },
+  "M001241": {
+    "bioguide": "M001241",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Dave Min",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67744ed90b34857ecc909155_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "M001242": {
+    "bioguide": "M001242",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Bernie Moreno",
+    "lis": "S434",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67c8694e6159152e59828afb_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OH"
+  },
+  "M001243": {
+    "bioguide": "M001243",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "David McCormick",
+    "lis": "S433",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677d85e0fdb6cf36bbb649aa_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "PA"
+  },
+  "M001244": {
+    "bioguide": "M001244",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Ashley Moody",
+    "lis": "S439",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/https://bioguide.congress.gov/photo/695d82c8550dfb80c3063bee.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "M001245": {
+    "bioguide": "M001245",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Christian D. Menefee",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/698206b2eb30d3271899373e_200.jpg",
+    "served_from": "2026-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "M001246": {
+    "bioguide": "M001246",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Analilia Mejia",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/69eb9743277642362f6112ce_200.jpg",
+    "served_from": "2026-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NJ"
+  },
+  "N000002": {
+    "bioguide": "N000002",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jerrold Nadler",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/n000002_200.jpg",
+    "served_from": "1991-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "N000015": {
+    "bioguide": "N000015",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Richard E. Neal",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/n000015_200.jpg",
+    "served_from": "1989-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MA"
+  },
+  "N000026": {
+    "bioguide": "N000026",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Troy E. Nehls",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/n000026_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "N000147": {
+    "bioguide": "N000147",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Eleanor Holmes Norton",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_dg_dc_norton_eleanor_200.jpg",
+    "served_from": "1991-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "DC"
+  },
+  "N000188": {
+    "bioguide": "N000188",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Donald Norcross",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/n000188_200.jpg",
+    "served_from": "2014-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NJ"
+  },
+  "N000189": {
+    "bioguide": "N000189",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Dan Newhouse",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_wa_4_newhouse_dan_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WA"
+  },
+  "N000190": {
+    "bioguide": "N000190",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Ralph Norman",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/n000190_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "SC"
+  },
+  "N000191": {
+    "bioguide": "N000191",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Joe Neguse",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/n000191_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CO"
+  },
+  "N000193": {
+    "bioguide": "N000193",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Zachary Nunn",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/n000193_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IA"
+  },
+  "O000019": {
+    "bioguide": "O000019",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jay Obernolte",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/o000019_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "O000086": {
+    "bioguide": "O000086",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Burgess Owens",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/o000086_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "UT"
+  },
+  "O000172": {
+    "bioguide": "O000172",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Alexandria Ocasio-Cortez",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/o000172_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "O000173": {
+    "bioguide": "O000173",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Ilhan Omar",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/o000173_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MN"
+  },
+  "O000174": {
+    "bioguide": "O000174",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Jon Ossoff",
+    "lis": "S414",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/o000174_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "GA"
+  },
+  "O000175": {
+    "bioguide": "O000175",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Andrew Ogles",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/o000175_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TN"
+  },
+  "O000176": {
+    "bioguide": "O000176",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Johnny Olszewski",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67744f4e0b34857ecc90915b_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MD"
+  },
+  "O000177": {
+    "bioguide": "O000177",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Robert F. Onder",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67744f970b34857ecc909161_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MO"
+  },
+  "P000034": {
+    "bioguide": "P000034",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Frank Pallone",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/p000034_200.jpg",
+    "served_from": "1987-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NJ"
+  },
+  "P000048": {
+    "bioguide": "P000048",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "August Pfluger",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/p000048_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "P000145": {
+    "bioguide": "P000145",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Alex Padilla",
+    "lis": "S413",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/p000145_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "P000197": {
+    "bioguide": "P000197",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Nancy Pelosi",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/p000197_200.jpg",
+    "served_from": "1987-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "P000595": {
+    "bioguide": "P000595",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Gary C. Peters",
+    "lis": "S380",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/p000595_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MI"
+  },
+  "P000597": {
+    "bioguide": "P000597",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Chellie Pingree",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/p000597_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "ME"
+  },
+  "P000603": {
+    "bioguide": "P000603",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Rand Paul",
+    "lis": "S348",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/p000603_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "KY"
+  },
+  "P000605": {
+    "bioguide": "P000605",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Scott Perry",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677ec7d3514c773869b6b915_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "PA"
+  },
+  "P000607": {
+    "bioguide": "P000607",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mark Pocan",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/p000607_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WI"
+  },
+  "P000608": {
+    "bioguide": "P000608",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Scott H. Peters",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/p000608_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "P000609": {
+    "bioguide": "P000609",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Gary J. Palmer",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/p000609_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AL"
+  },
+  "P000610": {
+    "bioguide": "P000610",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Stacey E. Plaskett",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_dg_vi_plaskett_stacey_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "VI"
+  },
+  "P000613": {
+    "bioguide": "P000613",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jimmy Panetta",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_ca_20_panetta_jimmy_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "P000614": {
+    "bioguide": "P000614",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Chris Pappas",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/p000614_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NH"
+  },
+  "P000617": {
+    "bioguide": "P000617",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Ayanna Pressley",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/p000617_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MA"
+  },
+  "P000620": {
+    "bioguide": "P000620",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Brittany Pettersen",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/p000620_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CO"
+  },
+  "P000621": {
+    "bioguide": "P000621",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Nellie Pou",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67745d3b0b34857ecc909167_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NJ"
+  },
+  "P000622": {
+    "bioguide": "P000622",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jimmy Patronis",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67efdb991b05a5a598f7fde9_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "Q000023": {
+    "bioguide": "Q000023",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mike Quigley",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/q000023_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IL"
+  },
+  "R000122": {
+    "bioguide": "R000122",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Jack Reed",
+    "lis": "S259",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/r000122_200.jpg",
+    "served_from": "1991-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "RI"
+  },
+  "R000305": {
+    "bioguide": "R000305",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Deborah K. Ross",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/r000305_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NC"
+  },
+  "R000395": {
+    "bioguide": "R000395",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Harold Rogers",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000395_200.jpg",
+    "served_from": "1981-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "KY"
+  },
+  "R000575": {
+    "bioguide": "R000575",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mike D. Rogers",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_al_3_rogers_mike_200.jpg",
+    "served_from": "2003-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AL"
+  },
+  "R000579": {
+    "bioguide": "R000579",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Patrick Ryan",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/r000579_200.jpg",
+    "served_from": "2022-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "R000584": {
+    "bioguide": "R000584",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "James E. Risch",
+    "lis": "S323",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000584_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "ID"
+  },
+  "R000595": {
+    "bioguide": "R000595",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Marco Rubio",
+    "lis": "S350",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000595_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": "2025-01-03",
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "R000599": {
+    "bioguide": "R000599",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Raul Ruiz",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/66e1aec832c796cea99fe06f_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "R000600": {
+    "bioguide": "R000600",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Aumua Amata Coleman Radewagen",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000600_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AS"
+  },
+  "R000603": {
+    "bioguide": "R000603",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "David Rouzer",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000603_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NC"
+  },
+  "R000605": {
+    "bioguide": "R000605",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Mike Rounds",
+    "lis": "S381",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000605_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "SD"
+  },
+  "R000606": {
+    "bioguide": "R000606",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jamie Raskin",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/r000606_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MD"
+  },
+  "R000608": {
+    "bioguide": "R000608",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Jacky Rosen",
+    "lis": "S402",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/r000608_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NV"
+  },
+  "R000609": {
+    "bioguide": "R000609",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "John H. Rutherford",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000609_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "R000610": {
+    "bioguide": "R000610",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Guy Reschenthaler",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000610_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "PA"
+  },
+  "R000612": {
+    "bioguide": "R000612",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "John W. Rose",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000612_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TN"
+  },
+  "R000614": {
+    "bioguide": "R000614",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Chip Roy",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000614_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "R000617": {
+    "bioguide": "R000617",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Delia C. Ramirez",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/684c2356333714e4aee2e1fd_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IL"
+  },
+  "R000618": {
+    "bioguide": "R000618",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Pete Ricketts",
+    "lis": "S423",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000618_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NE"
+  },
+  "R000619": {
+    "bioguide": "R000619",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Michael A. Rulli",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/69401dcc8cd6e06e9e3b36de_200.jpg",
+    "served_from": "2024-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OH"
+  },
+  "R000620": {
+    "bioguide": "R000620",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Luz M. Rivas",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67745d860b34857ecc90916d_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "R000621": {
+    "bioguide": "R000621",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Emily Randall",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67745dcf0b34857ecc909173_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WA"
+  },
+  "R000622": {
+    "bioguide": "R000622",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Josh Riley",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67745e360b34857ecc909179_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "S000033": {
+    "bioguide": "S000033",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Bernard Sanders",
+    "lis": "S313",
+    "party": "I",
+    "photo_url": "https://www.congress.gov/img/member/s000033_200.jpg",
+    "served_from": "1991-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "VT"
+  },
+  "S000148": {
+    "bioguide": "S000148",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Charles E. Schumer",
+    "lis": "S270",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s000148_200.jpg",
+    "served_from": "1981-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "S000168": {
+    "bioguide": "S000168",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Maria Elvira Salazar",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s000168_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "S000185": {
+    "bioguide": "S000185",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Robert C. \"Bobby\" Scott",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s000185_200.jpg",
+    "served_from": "1993-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "VA"
+  },
+  "S000250": {
+    "bioguide": "S000250",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Pete Sessions",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_tx_32_sessions_pete_200.jpg",
+    "served_from": "1997-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "S000344": {
+    "bioguide": "S000344",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Brad Sherman",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s000344_200.jpg",
+    "served_from": "1997-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "S000510": {
+    "bioguide": "S000510",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Adam Smith",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_wa_9_smith_adam_200.jpg",
+    "served_from": "1997-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WA"
+  },
+  "S000522": {
+    "bioguide": "S000522",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Christopher H. Smith",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s000522_200.jpg",
+    "served_from": "1981-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NJ"
+  },
+  "S000929": {
+    "bioguide": "S000929",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Victoria Spartz",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s000929_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IN"
+  },
+  "S001145": {
+    "bioguide": "S001145",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Janice D. Schakowsky",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_il_9_schakowsky_janice_200.jpg",
+    "served_from": "1999-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IL"
+  },
+  "S001148": {
+    "bioguide": "S001148",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Michael K. Simpson",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/678152c81f9ad6ea6fb1eb7f_200.jpg",
+    "served_from": "1999-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "ID"
+  },
+  "S001150": {
+    "bioguide": "S001150",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Adam B. Schiff",
+    "lis": "S427",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677d870dfdb6cf36bbb649b3_200.jpg",
+    "served_from": "2001-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "S001156": {
+    "bioguide": "S001156",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Linda T. S\u00e1nchez",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_ca_38_snchez_linda_200.jpg",
+    "served_from": "2003-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "S001157": {
+    "bioguide": "S001157",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "David Scott",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_ga_13_scott_david_200.jpg",
+    "served_from": "2003-01-03",
+    "served_to": "2026-01-03",
+    "source": "congress.gov/v3",
+    "state": "GA"
+  },
+  "S001159": {
+    "bioguide": "S001159",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Marilyn Strickland",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001159_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WA"
+  },
+  "S001172": {
+    "bioguide": "S001172",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Adrian Smith",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001172_200.jpg",
+    "served_from": "2007-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NE"
+  },
+  "S001176": {
+    "bioguide": "S001176",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Steve Scalise",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001176_200.jpg",
+    "served_from": "2008-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "LA"
+  },
+  "S001181": {
+    "bioguide": "S001181",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Jeanne Shaheen",
+    "lis": "S324",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001181_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NH"
+  },
+  "S001183": {
+    "bioguide": "S001183",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "David Schweikert",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001183_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AZ"
+  },
+  "S001184": {
+    "bioguide": "S001184",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Tim Scott",
+    "lis": "S365",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001184_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "SC"
+  },
+  "S001185": {
+    "bioguide": "S001185",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Terri A. Sewell",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001185_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AL"
+  },
+  "S001188": {
+    "bioguide": "S001188",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Marlin A. Stutzman",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67745e7e0b34857ecc90917f_200.jpg",
+    "served_from": "2010-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IN"
+  },
+  "S001189": {
+    "bioguide": "S001189",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Austin Scott",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001189_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "GA"
+  },
+  "S001190": {
+    "bioguide": "S001190",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Bradley Scott Schneider",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001190_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IL"
+  },
+  "S001193": {
+    "bioguide": "S001193",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Eric Swalwell",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001193_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": "2026-01-03",
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "S001194": {
+    "bioguide": "S001194",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Brian Schatz",
+    "lis": "S353",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001194_200.jpg",
+    "served_from": "2012-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "HI"
+  },
+  "S001195": {
+    "bioguide": "S001195",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jason Smith",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001195_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MO"
+  },
+  "S001196": {
+    "bioguide": "S001196",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Elise M. Stefanik",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001196_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "S001198": {
+    "bioguide": "S001198",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Dan Sullivan",
+    "lis": "S383",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001198_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AK"
+  },
+  "S001199": {
+    "bioguide": "S001199",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Lloyd Smucker",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001199_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "PA"
+  },
+  "S001200": {
+    "bioguide": "S001200",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Darren Soto",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_fl_9_soto_darren_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "S001201": {
+    "bioguide": "S001201",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Thomas R. Suozzi",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_ny_3_suozzi_thomas_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "S001203": {
+    "bioguide": "S001203",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Tina Smith",
+    "lis": "S394",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001203_200.jpg",
+    "served_from": "2018-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MN"
+  },
+  "S001205": {
+    "bioguide": "S001205",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mary Gay Scanlon",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_pa_5_scanlon_mary_200.jpg",
+    "served_from": "2018-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "PA"
+  },
+  "S001207": {
+    "bioguide": "S001207",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mikie Sherrill",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001207_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": "2025-01-03",
+    "source": "congress.gov/v3",
+    "state": "NJ"
+  },
+  "S001208": {
+    "bioguide": "S001208",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Elissa Slotkin",
+    "lis": "S436",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001208_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MI"
+  },
+  "S001211": {
+    "bioguide": "S001211",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Greg Stanton",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001211_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AZ"
+  },
+  "S001212": {
+    "bioguide": "S001212",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Pete Stauber",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001212_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MN"
+  },
+  "S001213": {
+    "bioguide": "S001213",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Bryan Steil",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001213_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WI"
+  },
+  "S001214": {
+    "bioguide": "S001214",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "W. Gregory Steube",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001214_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "S001215": {
+    "bioguide": "S001215",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Haley M. Stevens",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001215_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MI"
+  },
+  "S001216": {
+    "bioguide": "S001216",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Kim Schrier",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001216_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WA"
+  },
+  "S001217": {
+    "bioguide": "S001217",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Rick Scott",
+    "lis": "S404",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001217_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "S001218": {
+    "bioguide": "S001218",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Melanie A. Stansbury",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001218_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NM"
+  },
+  "S001220": {
+    "bioguide": "S001220",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Dale W. Strong",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001220_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AL"
+  },
+  "S001221": {
+    "bioguide": "S001221",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Hillary J. Scholten",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001221_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MI"
+  },
+  "S001223": {
+    "bioguide": "S001223",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Emilia Strong Sykes",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001223_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OH"
+  },
+  "S001224": {
+    "bioguide": "S001224",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Keith Self",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001224_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "S001225": {
+    "bioguide": "S001225",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Eric Sorensen",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001225_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IL"
+  },
+  "S001226": {
+    "bioguide": "S001226",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Andrea Salinas",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001226_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OR"
+  },
+  "S001227": {
+    "bioguide": "S001227",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Eric Schmitt",
+    "lis": "S420",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b66a0806e77f63e862391b15a0b1f753_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MO"
+  },
+  "S001228": {
+    "bioguide": "S001228",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Derek Schmidt",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67745ec50b34857ecc909185_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "KS"
+  },
+  "S001229": {
+    "bioguide": "S001229",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jefferson Shreve",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67745f0e0b34857ecc90918b_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IN"
+  },
+  "S001230": {
+    "bioguide": "S001230",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Suhas Subramanyam",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6797be8bc75fbc6f720e476a_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "VA"
+  },
+  "S001231": {
+    "bioguide": "S001231",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Lateefah Simon",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67745f940b34857ecc909197_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "S001232": {
+    "bioguide": "S001232",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Tim Sheehy",
+    "lis": "S435",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677d8231fdb6cf36bbb6498b_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MT"
+  },
+  "T000165": {
+    "bioguide": "T000165",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Thomas P. Tiffany",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/t000165_200.jpg",
+    "served_from": "2020-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WI"
+  },
+  "T000193": {
+    "bioguide": "T000193",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Bennie G. Thompson",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/t000193_200.jpg",
+    "served_from": "1993-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MS"
+  },
+  "T000250": {
+    "bioguide": "T000250",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "John Thune",
+    "lis": "S303",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/t000250_200.jpg",
+    "served_from": "1997-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "SD"
+  },
+  "T000278": {
+    "bioguide": "T000278",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Tommy Tuberville",
+    "lis": "S412",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/t000278_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AL"
+  },
+  "T000460": {
+    "bioguide": "T000460",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mike Thompson",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_ca_5_thompson_mike_200.jpg",
+    "served_from": "1999-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "T000463": {
+    "bioguide": "T000463",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Michael R. Turner",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/68014c674e51529406f18e56_200.jpg",
+    "served_from": "2003-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OH"
+  },
+  "T000467": {
+    "bioguide": "T000467",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Glenn Thompson",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/115_rp_pa_5_thompson_glenn_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "PA"
+  },
+  "T000468": {
+    "bioguide": "T000468",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Dina Titus",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6809296b6c2e6631263de728_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NV"
+  },
+  "T000469": {
+    "bioguide": "T000469",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Paul Tonko",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/t000469_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "T000472": {
+    "bioguide": "T000472",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Mark Takano",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/t000472_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "T000474": {
+    "bioguide": "T000474",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Norma J. Torres",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/t000474_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "T000476": {
+    "bioguide": "T000476",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Thomas Tillis",
+    "lis": "S384",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/t000476_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NC"
+  },
+  "T000478": {
+    "bioguide": "T000478",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Claudia Tenney",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/t000478_200.jpg",
+    "served_from": "2017-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "T000480": {
+    "bioguide": "T000480",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "William R. Timmons",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/t000480_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "SC"
+  },
+  "T000481": {
+    "bioguide": "T000481",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Rashida Tlaib",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/t000481_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MI"
+  },
+  "T000482": {
+    "bioguide": "T000482",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Lori Trahan",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/t000482_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MA"
+  },
+  "T000486": {
+    "bioguide": "T000486",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Ritchie Torres",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/t000486_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "T000487": {
+    "bioguide": "T000487",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jill N. Tokuda",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/t000487_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "HI"
+  },
+  "T000488": {
+    "bioguide": "T000488",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Shri Thanedar",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/68122bf2246d1b6bd8d9f6a2_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MI"
+  },
+  "T000489": {
+    "bioguide": "T000489",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Sylvester Turner",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67745fdc0b34857ecc90919d_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": "2025-01-03",
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "T000490": {
+    "bioguide": "T000490",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "David J. Taylor",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677460190b34857ecc9091a3_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OH"
+  },
+  "T000491": {
+    "bioguide": "T000491",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Derek Tran",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6774606d0b34857ecc9091a9_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "U000040": {
+    "bioguide": "U000040",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Lauren Underwood",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/u000040_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IL"
+  },
+  "V000081": {
+    "bioguide": "V000081",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Nydia M. Vel\u00e1zquez",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/v000081_200.jpg",
+    "served_from": "1993-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NY"
+  },
+  "V000128": {
+    "bioguide": "V000128",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Chris Van Hollen",
+    "lis": "S390",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/v000128_200.jpg",
+    "served_from": "2003-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MD"
+  },
+  "V000129": {
+    "bioguide": "V000129",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "David G. Valadao",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/v000129_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "V000130": {
+    "bioguide": "V000130",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Juan Vargas",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/v000130_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "V000131": {
+    "bioguide": "V000131",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Marc A. Veasey",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/v000131_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "V000133": {
+    "bioguide": "V000133",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Jefferson Van Drew",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67c0c39d53fe81a4b3c0cac1_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NJ"
+  },
+  "V000134": {
+    "bioguide": "V000134",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Beth Van Duyne",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677461060b34857ecc9091af_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "V000135": {
+    "bioguide": "V000135",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Derrick Van Orden",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/v000135_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WI"
+  },
+  "V000136": {
+    "bioguide": "V000136",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Gabe Vasquez",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/v000136_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NM"
+  },
+  "V000137": {
+    "bioguide": "V000137",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "J. D. Vance",
+    "lis": "S421",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/v000137_200.jpg",
+    "served_from": "2023-01-03",
+    "served_to": "2025-01-03",
+    "source": "congress.gov/v3",
+    "state": "OH"
+  },
+  "V000138": {
+    "bioguide": "V000138",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Eugene Simon Vindman",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/6774617a0b34857ecc9091b5_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "VA"
+  },
+  "V000139": {
+    "bioguide": "V000139",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Matt Van Epps",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/6931b7d5f478c0f1228e9abe_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TN"
+  },
+  "W000187": {
+    "bioguide": "W000187",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Maxine Waters",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/w000187_200.jpg",
+    "served_from": "1991-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "W000437": {
+    "bioguide": "W000437",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Roger F. Wicker",
+    "lis": "S318",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/f49ac425119375e9fe6a075762734079_200.jpg",
+    "served_from": "1995-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MS"
+  },
+  "W000779": {
+    "bioguide": "W000779",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Ron Wyden",
+    "lis": "S247",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/w000779_200.jpg",
+    "served_from": "1981-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "OR"
+  },
+  "W000788": {
+    "bioguide": "W000788",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Nikema Williams",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/w000788_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "GA"
+  },
+  "W000790": {
+    "bioguide": "W000790",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Raphael G. Warnock",
+    "lis": "S415",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/w000790_200.jpg",
+    "served_from": "2021-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "GA"
+  },
+  "W000795": {
+    "bioguide": "W000795",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Joe Wilson",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_sc_2_wilson_joe_200.jpg",
+    "served_from": "2001-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "SC"
+  },
+  "W000797": {
+    "bioguide": "W000797",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Debbie Wasserman Schultz",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/116_rp_fl_23_wassermanschultz_debbie_200.jpg",
+    "served_from": "2005-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "W000798": {
+    "bioguide": "W000798",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Tim Walberg",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/w000798_200.jpg",
+    "served_from": "2007-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MI"
+  },
+  "W000800": {
+    "bioguide": "W000800",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Peter Welch",
+    "lis": "S422",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/fd3cb364b8bf93c25834cff750637802_200.jpg",
+    "served_from": "2007-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "VT"
+  },
+  "W000802": {
+    "bioguide": "W000802",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Sheldon Whitehouse",
+    "lis": "S316",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/w000802_200.jpg",
+    "served_from": "2007-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "RI"
+  },
+  "W000804": {
+    "bioguide": "W000804",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Robert J. Wittman",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/w000804_200.jpg",
+    "served_from": "2007-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "VA"
+  },
+  "W000805": {
+    "bioguide": "W000805",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Mark R. Warner",
+    "lis": "S327",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/w000805_200.jpg",
+    "served_from": "2009-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "VA"
+  },
+  "W000806": {
+    "bioguide": "W000806",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Daniel Webster",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/w000806_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "W000808": {
+    "bioguide": "W000808",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Frederica S. Wilson",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/w000808_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "W000809": {
+    "bioguide": "W000809",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Steve Womack",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/117_rp_ar_3_womack_steve_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AR"
+  },
+  "W000812": {
+    "bioguide": "W000812",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Ann Wagner",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/695fc654ddd5f76cbbad2f67_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MO"
+  },
+  "W000814": {
+    "bioguide": "W000814",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Randy K. Sr. Weber",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/w000814_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "W000816": {
+    "bioguide": "W000816",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Roger Williams",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/w000816_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "TX"
+  },
+  "W000817": {
+    "bioguide": "W000817",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Elizabeth Warren",
+    "lis": "S366",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/w000817_200.jpg",
+    "served_from": "2013-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MA"
+  },
+  "W000821": {
+    "bioguide": "W000821",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Bruce Westerman",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/w000821_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "AR"
+  },
+  "W000822": {
+    "bioguide": "W000822",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Bonnie Watson Coleman",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/w000822_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "NJ"
+  },
+  "W000823": {
+    "bioguide": "W000823",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Michael Waltz",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/w000823_200.jpg",
+    "served_from": "2019-01-03",
+    "served_to": "2025-01-03",
+    "source": "congress.gov/v3",
+    "state": "FL"
+  },
+  "W000829": {
+    "bioguide": "W000829",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Tony Wied",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/6734b6724c72e343a6aff9e6_200.jpg",
+    "served_from": "2024-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "WI"
+  },
+  "W000830": {
+    "bioguide": "W000830",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "George Whitesides",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/68dc43db199559bad714973d_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "CA"
+  },
+  "W000831": {
+    "bioguide": "W000831",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "James R. Walkinshaw",
+    "lis": null,
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/68c1bd4ca929c5b98220e069_200.jpg",
+    "served_from": "2025-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "VA"
+  },
+  "Y000064": {
+    "bioguide": "Y000064",
+    "chamber": "Senate",
+    "district": null,
+    "full_name": "Todd Young",
+    "lis": "S391",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/y000064_200.jpg",
+    "served_from": "2011-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IN"
+  },
+  "Y000067": {
+    "bioguide": "Y000067",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Rudy Yakym",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/y000067_200.jpg",
+    "served_from": "2022-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "IN"
+  },
+  "Z000018": {
+    "bioguide": "Z000018",
+    "chamber": "House of Representatives",
+    "district": null,
+    "full_name": "Ryan K. Zinke",
+    "lis": null,
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/117_rp_mt_1_zinke_ryan_200.jpg",
+    "served_from": "2015-01-03",
+    "served_to": null,
+    "source": "congress.gov/v3",
+    "state": "MT"
+  }
+}

+ 1240 - 0
data/119/senate/roster.json

@@ -0,0 +1,1240 @@
+{
+  "S153": {
+    "bioguide": "G000386",
+    "chamber": "senate",
+    "full_name": "Chuck Grassley",
+    "lis": "S153",
+    "name": "Chuck Grassley",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000386_200.jpg",
+    "served_from": "1975-01-03",
+    "served_partial": false,
+    "state": "IA"
+  },
+  "S174": {
+    "bioguide": "M000355",
+    "chamber": "senate",
+    "full_name": "Mitch McConnell",
+    "lis": "S174",
+    "name": "Mitch McConnell",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m000355_200.jpg",
+    "served_from": "1985-01-03",
+    "served_partial": false,
+    "state": "KY"
+  },
+  "S229": {
+    "bioguide": "M001111",
+    "chamber": "senate",
+    "full_name": "Patty Murray",
+    "lis": "S229",
+    "name": "Patty Murray",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/1b52ad6d215684d847d1c2bf28b9b262_200.jpg",
+    "served_from": "1993-01-03",
+    "served_partial": false,
+    "state": "WA"
+  },
+  "S247": {
+    "bioguide": "W000779",
+    "chamber": "senate",
+    "full_name": "Ron Wyden",
+    "lis": "S247",
+    "name": "Ron Wyden",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/w000779_200.jpg",
+    "served_from": "1981-01-03",
+    "served_partial": false,
+    "state": "OR"
+  },
+  "S252": {
+    "bioguide": "C001035",
+    "chamber": "senate",
+    "full_name": "Susan M. Collins",
+    "lis": "S252",
+    "name": "Susan Collins",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001035_200.jpg",
+    "served_from": "1997-01-03",
+    "served_partial": false,
+    "state": "ME"
+  },
+  "S253": {
+    "bioguide": "D000563",
+    "chamber": "senate",
+    "full_name": "Richard J. Durbin",
+    "lis": "S253",
+    "name": "Richard Durbin",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/d000563_200.jpg",
+    "served_from": "1983-01-03",
+    "served_partial": false,
+    "state": "IL"
+  },
+  "S259": {
+    "bioguide": "R000122",
+    "chamber": "senate",
+    "full_name": "Jack Reed",
+    "lis": "S259",
+    "name": "John Reed",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/r000122_200.jpg",
+    "served_from": "1991-01-03",
+    "served_partial": false,
+    "state": "RI"
+  },
+  "S266": {
+    "bioguide": "C000880",
+    "chamber": "senate",
+    "full_name": "Mike Crapo",
+    "lis": "S266",
+    "name": "Mike Crapo",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c000880_200.jpg",
+    "served_from": "1993-01-03",
+    "served_partial": false,
+    "state": "ID"
+  },
+  "S270": {
+    "bioguide": "S000148",
+    "chamber": "senate",
+    "full_name": "Charles E. Schumer",
+    "lis": "S270",
+    "name": "Charles Schumer",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s000148_200.jpg",
+    "served_from": "1981-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "S275": {
+    "bioguide": "C000127",
+    "chamber": "senate",
+    "full_name": "Maria Cantwell",
+    "lis": "S275",
+    "name": "Maria Cantwell",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c000127_200.jpg",
+    "served_from": "1993-01-03",
+    "served_partial": false,
+    "state": "WA"
+  },
+  "S287": {
+    "bioguide": "C001056",
+    "chamber": "senate",
+    "full_name": "John Cornyn",
+    "lis": "S287",
+    "name": "John Cornyn",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001056_200.jpg",
+    "served_from": "2002-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "S288": {
+    "bioguide": "M001153",
+    "chamber": "senate",
+    "full_name": "Lisa Murkowski",
+    "lis": "S288",
+    "name": "Lisa Murkowski",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001153_200.jpg",
+    "served_from": "2002-01-03",
+    "served_partial": false,
+    "state": "AK"
+  },
+  "S293": {
+    "bioguide": "G000359",
+    "chamber": "senate",
+    "full_name": "Lindsey Graham",
+    "lis": "S293",
+    "name": "Lindsey Graham",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/g000359_200.jpg",
+    "served_from": "1995-01-03",
+    "served_partial": false,
+    "state": "SC"
+  },
+  "S303": {
+    "bioguide": "T000250",
+    "chamber": "senate",
+    "full_name": "John Thune",
+    "lis": "S303",
+    "name": "John Thune",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/t000250_200.jpg",
+    "served_from": "1997-01-03",
+    "served_partial": false,
+    "state": "SD"
+  },
+  "S311": {
+    "bioguide": "K000367",
+    "chamber": "senate",
+    "full_name": "Amy Klobuchar",
+    "lis": "S311",
+    "name": "Amy Klobuchar",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/k000367_200.jpg",
+    "served_from": "2007-01-03",
+    "served_partial": false,
+    "state": "MN"
+  },
+  "S313": {
+    "bioguide": "S000033",
+    "chamber": "senate",
+    "full_name": "Bernard Sanders",
+    "lis": "S313",
+    "name": "Bernie Sanders",
+    "party": "I",
+    "photo_url": "https://www.congress.gov/img/member/s000033_200.jpg",
+    "served_from": "1991-01-03",
+    "served_partial": false,
+    "state": "VT"
+  },
+  "S316": {
+    "bioguide": "W000802",
+    "chamber": "senate",
+    "full_name": "Sheldon Whitehouse",
+    "lis": "S316",
+    "name": "Sheldon Whitehouse",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/w000802_200.jpg",
+    "served_from": "2007-01-03",
+    "served_partial": false,
+    "state": "RI"
+  },
+  "S317": {
+    "bioguide": "B001261",
+    "chamber": "senate",
+    "full_name": "John Barrasso",
+    "lis": "S317",
+    "name": "John Barrasso",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001261_200.jpg",
+    "served_from": "2007-01-03",
+    "served_partial": false,
+    "state": "WY"
+  },
+  "S318": {
+    "bioguide": "W000437",
+    "chamber": "senate",
+    "full_name": "Roger F. Wicker",
+    "lis": "S318",
+    "name": "Roger Wicker",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/f49ac425119375e9fe6a075762734079_200.jpg",
+    "served_from": "1995-01-03",
+    "served_partial": false,
+    "state": "MS"
+  },
+  "S322": {
+    "bioguide": "M001176",
+    "chamber": "senate",
+    "full_name": "Jeff Merkley",
+    "lis": "S322",
+    "name": "Jeff Merkley",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001176_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "state": "OR"
+  },
+  "S323": {
+    "bioguide": "R000584",
+    "chamber": "senate",
+    "full_name": "James E. Risch",
+    "lis": "S323",
+    "name": "James Risch",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000584_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "state": "ID"
+  },
+  "S324": {
+    "bioguide": "S001181",
+    "chamber": "senate",
+    "full_name": "Jeanne Shaheen",
+    "lis": "S324",
+    "name": "Jeanne Shaheen",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001181_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "state": "NH"
+  },
+  "S327": {
+    "bioguide": "W000805",
+    "chamber": "senate",
+    "full_name": "Mark R. Warner",
+    "lis": "S327",
+    "name": "Mark Warner",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/w000805_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "state": "VA"
+  },
+  "S330": {
+    "bioguide": "B001267",
+    "chamber": "senate",
+    "full_name": "Michael F. Bennet",
+    "lis": "S330",
+    "name": "Michael Bennet",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001267_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "state": "CO"
+  },
+  "S331": {
+    "bioguide": "G000555",
+    "chamber": "senate",
+    "full_name": "Kirsten E. Gillibrand",
+    "lis": "S331",
+    "name": "Kirsten Gillibrand",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/g000555_200.jpg",
+    "served_from": "2007-01-03",
+    "served_partial": false,
+    "state": "NY"
+  },
+  "S337": {
+    "bioguide": "C001088",
+    "chamber": "senate",
+    "full_name": "Christopher A. Coons",
+    "lis": "S337",
+    "name": "Christopher Coons",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001088_200.jpg",
+    "served_from": "2010-01-03",
+    "served_partial": false,
+    "state": "DE"
+  },
+  "S341": {
+    "bioguide": "B001277",
+    "chamber": "senate",
+    "full_name": "Richard Blumenthal",
+    "lis": "S341",
+    "name": "Richard Blumenthal",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001277_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "CT"
+  },
+  "S343": {
+    "bioguide": "B001236",
+    "chamber": "senate",
+    "full_name": "John Boozman",
+    "lis": "S343",
+    "name": "John Boozman",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001236_200.jpg",
+    "served_from": "2001-01-03",
+    "served_partial": false,
+    "state": "AR"
+  },
+  "S344": {
+    "bioguide": "H001061",
+    "chamber": "senate",
+    "full_name": "John Hoeven",
+    "lis": "S344",
+    "name": "John Hoeven",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001061_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "ND"
+  },
+  "S345": {
+    "bioguide": "J000293",
+    "chamber": "senate",
+    "full_name": "Ron Johnson",
+    "lis": "S345",
+    "name": "Ron Johnson",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/j000293_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "WI"
+  },
+  "S346": {
+    "bioguide": "L000577",
+    "chamber": "senate",
+    "full_name": "Mike Lee",
+    "lis": "S346",
+    "name": "Mike Lee",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/l000577_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "UT"
+  },
+  "S347": {
+    "bioguide": "M000934",
+    "chamber": "senate",
+    "full_name": "Jerry Moran",
+    "lis": "S347",
+    "name": "Jerry Moran",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m000934_200.jpg",
+    "served_from": "1997-01-03",
+    "served_partial": false,
+    "state": "KS"
+  },
+  "S348": {
+    "bioguide": "P000603",
+    "chamber": "senate",
+    "full_name": "Rand Paul",
+    "lis": "S348",
+    "name": "Rand Paul",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/p000603_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "KY"
+  },
+  "S350": {
+    "bioguide": "R000595",
+    "chamber": "senate",
+    "full_name": "Marco Rubio",
+    "lis": "S350",
+    "name": "Marco Rubio",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000595_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "served_to": "2025-01-03",
+    "state": "FL"
+  },
+  "S353": {
+    "bioguide": "S001194",
+    "chamber": "senate",
+    "full_name": "Brian Schatz",
+    "lis": "S353",
+    "name": "Brian Schatz",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001194_200.jpg",
+    "served_from": "2012-01-03",
+    "served_partial": false,
+    "state": "HI"
+  },
+  "S354": {
+    "bioguide": "B001230",
+    "chamber": "senate",
+    "full_name": "Tammy Baldwin",
+    "lis": "S354",
+    "name": "Tammy Baldwin",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001230_200.jpg",
+    "served_from": "1999-01-03",
+    "served_partial": false,
+    "state": "WI"
+  },
+  "S355": {
+    "bioguide": "C001098",
+    "chamber": "senate",
+    "full_name": "Ted Cruz",
+    "lis": "S355",
+    "name": "Ted Cruz",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001098_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "TX"
+  },
+  "S357": {
+    "bioguide": "F000463",
+    "chamber": "senate",
+    "full_name": "Deb Fischer",
+    "lis": "S357",
+    "name": "Deb Fischer",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/669ec7925d19788d1f2034a1_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "NE"
+  },
+  "S359": {
+    "bioguide": "H001046",
+    "chamber": "senate",
+    "full_name": "Martin Heinrich",
+    "lis": "S359",
+    "name": "Martin Heinrich",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/h001046_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "state": "NM"
+  },
+  "S361": {
+    "bioguide": "H001042",
+    "chamber": "senate",
+    "full_name": "Mazie K. Hirono",
+    "lis": "S361",
+    "name": "Mazie Hirono",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/h001042_200.jpg",
+    "served_from": "2007-01-03",
+    "served_partial": false,
+    "state": "HI"
+  },
+  "S362": {
+    "bioguide": "K000384",
+    "chamber": "senate",
+    "full_name": "Tim Kaine",
+    "lis": "S362",
+    "name": "Timothy Kaine",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/k000384_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "VA"
+  },
+  "S363": {
+    "bioguide": "K000383",
+    "chamber": "senate",
+    "full_name": "Angus S., Jr. King",
+    "lis": "S363",
+    "name": "Angus King",
+    "party": "I",
+    "photo_url": "https://www.congress.gov/img/member/k000383_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "ME"
+  },
+  "S364": {
+    "bioguide": "M001169",
+    "chamber": "senate",
+    "full_name": "Christopher Murphy",
+    "lis": "S364",
+    "name": "Christopher Murphy",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m001169_200.jpg",
+    "served_from": "2007-01-03",
+    "served_partial": false,
+    "state": "CT"
+  },
+  "S365": {
+    "bioguide": "S001184",
+    "chamber": "senate",
+    "full_name": "Tim Scott",
+    "lis": "S365",
+    "name": "Tim Scott",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001184_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "SC"
+  },
+  "S366": {
+    "bioguide": "W000817",
+    "chamber": "senate",
+    "full_name": "Elizabeth Warren",
+    "lis": "S366",
+    "name": "Elizabeth Warren",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/w000817_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "MA"
+  },
+  "S369": {
+    "bioguide": "M000133",
+    "chamber": "senate",
+    "full_name": "Edward J. Markey",
+    "lis": "S369",
+    "name": "Edward Markey",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/m000133_200.jpg",
+    "served_from": "1977-01-03",
+    "served_partial": false,
+    "state": "MA"
+  },
+  "S370": {
+    "bioguide": "B001288",
+    "chamber": "senate",
+    "full_name": "Cory A. Booker",
+    "lis": "S370",
+    "name": "Cory Booker",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001288_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "NJ"
+  },
+  "S372": {
+    "bioguide": "C001047",
+    "chamber": "senate",
+    "full_name": "Shelley Moore Capito",
+    "lis": "S372",
+    "name": "Shelley Capito",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001047_200.jpg",
+    "served_from": "2001-01-03",
+    "served_partial": false,
+    "state": "WV"
+  },
+  "S373": {
+    "bioguide": "C001075",
+    "chamber": "senate",
+    "full_name": "Bill Cassidy",
+    "lis": "S373",
+    "name": "Bill Cassidy",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001075_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "state": "LA"
+  },
+  "S374": {
+    "bioguide": "C001095",
+    "chamber": "senate",
+    "full_name": "Tom Cotton",
+    "lis": "S374",
+    "name": "Tom Cotton",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001095_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "AR"
+  },
+  "S375": {
+    "bioguide": "D000618",
+    "chamber": "senate",
+    "full_name": "Steve Daines",
+    "lis": "S375",
+    "name": "Steve Daines",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/d000618_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "MT"
+  },
+  "S376": {
+    "bioguide": "E000295",
+    "chamber": "senate",
+    "full_name": "Joni Ernst",
+    "lis": "S376",
+    "name": "Joni Ernst",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/e000295_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "IA"
+  },
+  "S378": {
+    "bioguide": "L000575",
+    "chamber": "senate",
+    "full_name": "James Lankford",
+    "lis": "S378",
+    "name": "James Lankford",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/l000575_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "OK"
+  },
+  "S380": {
+    "bioguide": "P000595",
+    "chamber": "senate",
+    "full_name": "Gary C. Peters",
+    "lis": "S380",
+    "name": "Gary Peters",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/p000595_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "state": "MI"
+  },
+  "S381": {
+    "bioguide": "R000605",
+    "chamber": "senate",
+    "full_name": "Mike Rounds",
+    "lis": "S381",
+    "name": "Mike Rounds",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000605_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "SD"
+  },
+  "S383": {
+    "bioguide": "S001198",
+    "chamber": "senate",
+    "full_name": "Dan Sullivan",
+    "lis": "S383",
+    "name": "Dan Sullivan",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001198_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "AK"
+  },
+  "S384": {
+    "bioguide": "T000476",
+    "chamber": "senate",
+    "full_name": "Thomas Tillis",
+    "lis": "S384",
+    "name": "Thomas Tillis",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/t000476_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "NC"
+  },
+  "S385": {
+    "bioguide": "C001113",
+    "chamber": "senate",
+    "full_name": "Catherine Cortez Masto",
+    "lis": "S385",
+    "name": "Catherine Cortez Masto",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c001113_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "NV"
+  },
+  "S386": {
+    "bioguide": "D000622",
+    "chamber": "senate",
+    "full_name": "Tammy Duckworth",
+    "lis": "S386",
+    "name": "Tammy Duckworth",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/d000622_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "IL"
+  },
+  "S388": {
+    "bioguide": "H001076",
+    "chamber": "senate",
+    "full_name": "Margaret Wood Hassan",
+    "lis": "S388",
+    "name": "Maggie Hassan",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/h001076_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "NH"
+  },
+  "S389": {
+    "bioguide": "K000393",
+    "chamber": "senate",
+    "full_name": "John Kennedy",
+    "lis": "S389",
+    "name": "John Kennedy",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/k000393_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "LA"
+  },
+  "S390": {
+    "bioguide": "V000128",
+    "chamber": "senate",
+    "full_name": "Chris Van Hollen",
+    "lis": "S390",
+    "name": "Chris Van Hollen",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/v000128_200.jpg",
+    "served_from": "2003-01-03",
+    "served_partial": false,
+    "state": "MD"
+  },
+  "S391": {
+    "bioguide": "Y000064",
+    "chamber": "senate",
+    "full_name": "Todd Young",
+    "lis": "S391",
+    "name": "Todd Young",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/y000064_200.jpg",
+    "served_from": "2011-01-03",
+    "served_partial": false,
+    "state": "IN"
+  },
+  "S394": {
+    "bioguide": "S001203",
+    "chamber": "senate",
+    "full_name": "Tina Smith",
+    "lis": "S394",
+    "name": "Tina Smith",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001203_200.jpg",
+    "served_from": "2018-01-03",
+    "served_partial": false,
+    "state": "MN"
+  },
+  "S395": {
+    "bioguide": "H001079",
+    "chamber": "senate",
+    "full_name": "Cindy Hyde-Smith",
+    "lis": "S395",
+    "name": "Cindy Hyde-Smith",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001079_200.jpg",
+    "served_from": "2018-01-03",
+    "served_partial": false,
+    "state": "MS"
+  },
+  "S396": {
+    "bioguide": "B001243",
+    "chamber": "senate",
+    "full_name": "Marsha Blackburn",
+    "lis": "S396",
+    "name": "Marsha Blackburn",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001243_200.jpg",
+    "served_from": "2003-01-03",
+    "served_partial": false,
+    "state": "TN"
+  },
+  "S398": {
+    "bioguide": "C001096",
+    "chamber": "senate",
+    "full_name": "Kevin Cramer",
+    "lis": "S398",
+    "name": "Kevin Cramer",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/c001096_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "state": "ND"
+  },
+  "S399": {
+    "bioguide": "H001089",
+    "chamber": "senate",
+    "full_name": "Josh Hawley",
+    "lis": "S399",
+    "name": "Josh Hawley",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h001089_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "MO"
+  },
+  "S402": {
+    "bioguide": "R000608",
+    "chamber": "senate",
+    "full_name": "Jacky Rosen",
+    "lis": "S402",
+    "name": "Jacky Rosen",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/r000608_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "NV"
+  },
+  "S404": {
+    "bioguide": "S001217",
+    "chamber": "senate",
+    "full_name": "Rick Scott",
+    "lis": "S404",
+    "name": "Rick Scott",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/s001217_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "S406": {
+    "bioguide": "K000377",
+    "chamber": "senate",
+    "full_name": "Mark Kelly",
+    "lis": "S406",
+    "name": "Mark Kelly",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/k000377_200.jpg",
+    "served_from": "2020-01-03",
+    "served_partial": false,
+    "state": "AZ"
+  },
+  "S407": {
+    "bioguide": "H000601",
+    "chamber": "senate",
+    "full_name": "Bill Hagerty",
+    "lis": "S407",
+    "name": "Bill Hagerty",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/h000601_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "TN"
+  },
+  "S408": {
+    "bioguide": "H000273",
+    "chamber": "senate",
+    "full_name": "John W. Hickenlooper",
+    "lis": "S408",
+    "name": "John Hickenlooper",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/h000273_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "CO"
+  },
+  "S409": {
+    "bioguide": "L000570",
+    "chamber": "senate",
+    "full_name": "Ben Ray Luj\u00e1n",
+    "lis": "S409",
+    "name": "Ben Lujan",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/l000570_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "state": "NM"
+  },
+  "S410": {
+    "bioguide": "L000571",
+    "chamber": "senate",
+    "full_name": "Cynthia M. Lummis",
+    "lis": "S410",
+    "name": "Cynthia Lummis",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/l000571_200.jpg",
+    "served_from": "2009-01-03",
+    "served_partial": false,
+    "state": "WY"
+  },
+  "S411": {
+    "bioguide": "M001198",
+    "chamber": "senate",
+    "full_name": "Roger Marshall",
+    "lis": "S411",
+    "name": "Roger Marshall",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001198_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "KS"
+  },
+  "S412": {
+    "bioguide": "T000278",
+    "chamber": "senate",
+    "full_name": "Tommy Tuberville",
+    "lis": "S412",
+    "name": "Tommy Tuberville",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/t000278_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "AL"
+  },
+  "S413": {
+    "bioguide": "P000145",
+    "chamber": "senate",
+    "full_name": "Alex Padilla",
+    "lis": "S413",
+    "name": "Alex Padilla",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/p000145_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "S414": {
+    "bioguide": "O000174",
+    "chamber": "senate",
+    "full_name": "Jon Ossoff",
+    "lis": "S414",
+    "name": "Jon Ossoff",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/o000174_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "GA"
+  },
+  "S415": {
+    "bioguide": "W000790",
+    "chamber": "senate",
+    "full_name": "Raphael G. Warnock",
+    "lis": "S415",
+    "name": "Raphael Warnock",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/w000790_200.jpg",
+    "served_from": "2021-01-03",
+    "served_partial": false,
+    "state": "GA"
+  },
+  "S416": {
+    "bioguide": "B001319",
+    "chamber": "senate",
+    "full_name": "Katie Boyd Britt",
+    "lis": "S416",
+    "name": "Katie Britt",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001319_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "AL"
+  },
+  "S417": {
+    "bioguide": "B001305",
+    "chamber": "senate",
+    "full_name": "Ted Budd",
+    "lis": "S417",
+    "name": "Ted Budd",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b001305_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "NC"
+  },
+  "S418": {
+    "bioguide": "F000479",
+    "chamber": "senate",
+    "full_name": "John Fetterman",
+    "lis": "S418",
+    "name": "John Fetterman",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/f000479_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "PA"
+  },
+  "S419": {
+    "bioguide": "M001190",
+    "chamber": "senate",
+    "full_name": "Markwayne Mullin",
+    "lis": "S419",
+    "name": "Markwayne Mullin",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/m001190_200.jpg",
+    "served_from": "2013-01-03",
+    "served_partial": false,
+    "served_to": "2026-01-03",
+    "state": "OK"
+  },
+  "S420": {
+    "bioguide": "S001227",
+    "chamber": "senate",
+    "full_name": "Eric Schmitt",
+    "lis": "S420",
+    "name": "Eric Schmitt",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/b66a0806e77f63e862391b15a0b1f753_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "MO"
+  },
+  "S421": {
+    "bioguide": "V000137",
+    "chamber": "senate",
+    "full_name": "J. D. Vance",
+    "lis": "S421",
+    "name": "J. Vance",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/v000137_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "served_to": "2025-01-03",
+    "state": "OH"
+  },
+  "S422": {
+    "bioguide": "W000800",
+    "chamber": "senate",
+    "full_name": "Peter Welch",
+    "lis": "S422",
+    "name": "Peter Welch",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/fd3cb364b8bf93c25834cff750637802_200.jpg",
+    "served_from": "2007-01-03",
+    "served_partial": false,
+    "state": "VT"
+  },
+  "S423": {
+    "bioguide": "R000618",
+    "chamber": "senate",
+    "full_name": "Pete Ricketts",
+    "lis": "S423",
+    "name": "Pete Ricketts",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/r000618_200.jpg",
+    "served_from": "2023-01-03",
+    "served_partial": false,
+    "state": "NE"
+  },
+  "S426": {
+    "bioguide": "K000394",
+    "chamber": "senate",
+    "full_name": "Andy Kim",
+    "lis": "S426",
+    "name": "Andy Kim",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677d84cbfdb6cf36bbb649a1_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "NJ"
+  },
+  "S427": {
+    "bioguide": "S001150",
+    "chamber": "senate",
+    "full_name": "Adam B. Schiff",
+    "lis": "S427",
+    "name": "Adam Schiff",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/677d870dfdb6cf36bbb649b3_200.jpg",
+    "served_from": "2001-01-03",
+    "served_partial": false,
+    "state": "CA"
+  },
+  "S428": {
+    "bioguide": "A000382",
+    "chamber": "senate",
+    "full_name": "Angela D. Alsobrooks",
+    "lis": "S428",
+    "name": "Angela Alsobrooks",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/67acdbbf044eb506e25958f2_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "MD"
+  },
+  "S429": {
+    "bioguide": "B001299",
+    "chamber": "senate",
+    "full_name": "Jim Banks",
+    "lis": "S429",
+    "name": "Jim Banks",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677d83cbfdb6cf36bbb64998_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "IN"
+  },
+  "S430": {
+    "bioguide": "B001303",
+    "chamber": "senate",
+    "full_name": "Lisa Blunt Rochester",
+    "lis": "S430",
+    "name": "Lisa Blunt Rochester",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/b001303_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "DE"
+  },
+  "S431": {
+    "bioguide": "C001114",
+    "chamber": "senate",
+    "full_name": "John R. Curtis",
+    "lis": "S431",
+    "name": "John Curtis",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/1b9d1007d6895a37da28a67cd8149803_200.jpg",
+    "served_from": "2017-01-03",
+    "served_partial": false,
+    "state": "UT"
+  },
+  "S432": {
+    "bioguide": "G000574",
+    "chamber": "senate",
+    "full_name": "Ruben Gallego",
+    "lis": "S432",
+    "name": "Ruben Gallego",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/c6870f487cf4bc9a568d8afbe61d754b_200.jpg",
+    "served_from": "2015-01-03",
+    "served_partial": false,
+    "state": "AZ"
+  },
+  "S433": {
+    "bioguide": "M001243",
+    "chamber": "senate",
+    "full_name": "David McCormick",
+    "lis": "S433",
+    "name": "David McCormick",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677d85e0fdb6cf36bbb649aa_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "PA"
+  },
+  "S434": {
+    "bioguide": "M001242",
+    "chamber": "senate",
+    "full_name": "Bernie Moreno",
+    "lis": "S434",
+    "name": "Bernie Moreno",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67c8694e6159152e59828afb_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "OH"
+  },
+  "S435": {
+    "bioguide": "S001232",
+    "chamber": "senate",
+    "full_name": "Tim Sheehy",
+    "lis": "S435",
+    "name": "Tim Sheehy",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/677d8231fdb6cf36bbb6498b_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "MT"
+  },
+  "S436": {
+    "bioguide": "S001208",
+    "chamber": "senate",
+    "full_name": "Elissa Slotkin",
+    "lis": "S436",
+    "name": "Elissa Slotkin",
+    "party": "D",
+    "photo_url": "https://www.congress.gov/img/member/s001208_200.jpg",
+    "served_from": "2019-01-03",
+    "served_partial": false,
+    "state": "MI"
+  },
+  "S437": {
+    "bioguide": "J000312",
+    "chamber": "senate",
+    "full_name": "James C. Justice",
+    "lis": "S437",
+    "name": "James Justice",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67c86b5e6159152e59828b1a_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "WV"
+  },
+  "S438": {
+    "bioguide": "H001104",
+    "chamber": "senate",
+    "full_name": "Jon Husted",
+    "lis": "S438",
+    "name": "Jon Husted",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/67f0316b1b05a5a598f7fdf3_200.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "OH"
+  },
+  "S439": {
+    "bioguide": "M001244",
+    "chamber": "senate",
+    "full_name": "Ashley Moody",
+    "lis": "S439",
+    "name": "Ashley Moody",
+    "party": "R",
+    "photo_url": "https://www.congress.gov/img/member/https://bioguide.congress.gov/photo/695d82c8550dfb80c3063bee.jpg",
+    "served_from": "2025-01-03",
+    "served_partial": false,
+    "state": "FL"
+  },
+  "S440": {
+    "bioguide": "A000383",
+    "chamber": "senate",
+    "full_name": "Alan Armstrong",
+    "lis": "S440",
+    "name": "Alan Armstrong",
+    "party": "R",
+    "served_from": "2026-01-03",
+    "served_partial": false,
+    "state": "OK"
+  }
+}

ファイルの差分が大きいため隠しています
+ 273 - 0
data/119/senate/votes.jsonl


+ 308 - 0
enrich_roster.py

@@ -0,0 +1,308 @@
+#!/usr/bin/env python3
+"""Enrich the 119th-Congress roster via Congress.gov API.
+
+Writes:
+  data/<C>/members_directory.json  — dict keyed by bioguide
+  data/<C>/lis_to_bioguide.json    — Senate LIS -> bioguide crosswalk
+  data/<C>/api_cache/<sha>.json    — raw cached API responses (idempotent)
+
+Reads CONGRESS_GOV_API_KEY from ./.env (never CLI/env-var).
+Standard library only. Throttled 350 ms between live requests.
+"""
+import argparse, hashlib, json, os, re, sys, time, unicodedata, urllib.error, urllib.request
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent
+DATA_ROOT = ROOT / "data"
+ENV_PATH = ROOT / ".env"
+API_BASE = "https://api.congress.gov/v3"
+THROTTLE_SEC = 0.35
+RETRY_BACKOFFS = (0.5, 1.0, 2.0)
+
+PARTY_MAP = {
+    "R": "R", "Republican": "R",
+    "D": "D", "Democratic": "D", "Democrat": "D",
+    "I": "I", "Independent": "I", "ID": "I",
+}
+STATE_NAME_TO_CODE = {
+    "Alabama":"AL","Alaska":"AK","Arizona":"AZ","Arkansas":"AR","California":"CA",
+    "Colorado":"CO","Connecticut":"CT","Delaware":"DE","Florida":"FL","Georgia":"GA",
+    "Hawaii":"HI","Idaho":"ID","Illinois":"IL","Indiana":"IN","Iowa":"IA","Kansas":"KS",
+    "Kentucky":"KY","Louisiana":"LA","Maine":"ME","Maryland":"MD","Massachusetts":"MA",
+    "Michigan":"MI","Minnesota":"MN","Mississippi":"MS","Missouri":"MO","Montana":"MT",
+    "Nebraska":"NE","Nevada":"NV","New Hampshire":"NH","New Jersey":"NJ","New Mexico":"NM",
+    "New York":"NY","North Carolina":"NC","North Dakota":"ND","Ohio":"OH","Oklahoma":"OK",
+    "Oregon":"OR","Pennsylvania":"PA","Rhode Island":"RI","South Carolina":"SC",
+    "South Dakota":"SD","Tennessee":"TN","Texas":"TX","Utah":"UT","Vermont":"VT",
+    "Virginia":"VA","Washington":"WA","West Virginia":"WV","Wisconsin":"WI","Wyoming":"WY",
+    "American Samoa":"AS","District of Columbia":"DC","Guam":"GU",
+    "Northern Mariana Islands":"MP","Puerto Rico":"PR","Virgin Islands":"VI",
+}
+LIS_KEY_RE = re.compile(r"(?i)\b(lis|senateid|lis[_-]?id|lis[_-]?member[_-]?id)\b")
+
+
+def _load_api_key():
+    if not ENV_PATH.exists():
+        print(f"ERROR: .env not found at {ENV_PATH}", file=sys.stderr)
+        sys.exit(2)
+    for line in ENV_PATH.read_text().splitlines():
+        line = line.strip()
+        if not line or line.startswith("#") or "=" not in line:
+            continue
+        k, _, v = line.partition("=")
+        if k.strip() == "CONGRESS_GOV_API_KEY":
+            return v.strip().strip('"').strip("'")
+    print("ERROR: CONGRESS_GOV_API_KEY not set in .env", file=sys.stderr)
+    sys.exit(2)
+
+
+def _cache_path(cache_dir, url):
+    # Cache key strips api_key so re-keying doesn't invalidate cache.
+    clean = re.sub(r"([?&])api_key=[^&]*", r"\1", url).rstrip("?&")
+    sha = hashlib.sha256(clean.encode("utf-8")).hexdigest()
+    return cache_dir / f"{sha}.json"
+
+
+def _fetch(url, cache_dir, warnings, label="request"):
+    cp = _cache_path(cache_dir, url)
+    if cp.exists():
+        try:
+            return json.loads(cp.read_text())
+        except Exception:
+            pass  # fall through and re-fetch
+    last_err = None
+    for i, backoff in enumerate((0,) + RETRY_BACKOFFS):
+        if backoff:
+            time.sleep(backoff)
+        try:
+            req = urllib.request.Request(url, headers={"Accept": "application/json", "User-Agent": "polisci-pipeline/1.0 (+enrich_roster.py)"})
+            with urllib.request.urlopen(req, timeout=30) as resp:
+                body = resp.read()
+            data = json.loads(body)
+            cp.parent.mkdir(parents=True, exist_ok=True)
+            cp.write_text(json.dumps(data))
+            time.sleep(THROTTLE_SEC)
+            return data
+        except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, json.JSONDecodeError) as e:
+            last_err = e
+    warnings.append(f"{label}: {last_err}")
+    return None
+
+
+def _party_letter(member):
+    pa = member.get("partyHistory") or []
+    if pa:
+        last = pa[-1]
+        for key in ("partyAbbreviation", "partyName"):
+            v = last.get(key)
+            if v and v in PARTY_MAP:
+                return PARTY_MAP[v]
+    pn = member.get("partyName") or ""
+    return PARTY_MAP.get(pn, "I" if pn else "")
+
+
+def _state_code(member):
+    s = member.get("state") or ""
+    if len(s) == 2 and s.isupper():
+        return s
+    return STATE_NAME_TO_CODE.get(s, s[:2].upper() if s else "")
+
+
+def _latest_chamber(member):
+    terms = (member.get("terms") or {}).get("item") or member.get("terms") or []
+    if isinstance(terms, dict):
+        terms = terms.get("item") or []
+    if not terms:
+        return ""
+    # Sort by startYear if available
+    def sk(t): return t.get("startYear") or 0
+    last = sorted(terms, key=sk)[-1]
+    return (last.get("chamber") or "").strip()
+
+
+def _served_dates(member):
+    terms = (member.get("terms") or {}).get("item") or member.get("terms") or []
+    if isinstance(terms, dict):
+        terms = terms.get("item") or []
+    if not terms:
+        return None, None
+    starts = []
+    ends = []
+    for t in terms:
+        sy = t.get("startYear")
+        ey = t.get("endYear")
+        if sy:
+            starts.append(f"{int(sy):04d}-01-03")
+        if ey:
+            ends.append(f"{int(ey):04d}-01-03")
+    served_from = min(starts) if starts else None
+    served_to = max(ends) if ends and len(ends) == len(starts) else None
+    return served_from, served_to
+
+
+def _scan_for_lis(obj):
+    """Recursively scan obj for any key matching LIS pattern; return string value or None."""
+    if isinstance(obj, dict):
+        for k, v in obj.items():
+            if isinstance(k, str) and LIS_KEY_RE.search(k):
+                if isinstance(v, str) and re.match(r"^S?\d{3,4}$", v.strip()):
+                    val = v.strip()
+                    if not val.startswith("S"):
+                        val = "S" + val.zfill(3)
+                    return val
+            found = _scan_for_lis(v)
+            if found:
+                return found
+    elif isinstance(obj, list):
+        for it in obj:
+            found = _scan_for_lis(it)
+            if found:
+                return found
+    return None
+
+
+def _normalize_member(m):
+    bioguide = (m.get("bioguideId") or "").strip()
+    if not bioguide:
+        return None
+    chamber = _latest_chamber(m)
+    served_from, served_to = _served_dates(m)
+    district = m.get("district")
+    if district is not None:
+        district = str(district)
+    name = m.get("directOrderName") or m.get("name") or ""
+    if not name:
+        first = m.get("firstName") or ""
+        last = m.get("lastName") or ""
+        name = (first + " " + last).strip()
+    # If name is "Last, First" prefer invertedOrderName? Use as-is otherwise.
+    if "," in name and not m.get("directOrderName"):
+        parts = [p.strip() for p in name.split(",", 1)]
+        if len(parts) == 2:
+            name = parts[1] + " " + parts[0]
+    photo = ((m.get("depiction") or {}).get("imageUrl")) or None
+    return {
+        "bioguide": bioguide,
+        "lis": None,
+        "full_name": name,
+        "party": _party_letter(m),
+        "state": _state_code(m),
+        "district": district if chamber.lower() == "house" else None,
+        "chamber": chamber,
+        "served_from": served_from,
+        "served_to": served_to,
+        "photo_url": photo,
+        "source": "congress.gov/v3",
+    }
+
+
+def _write_partial(out_dir, directory, lis_map, warnings, note):
+    out_dir.mkdir(parents=True, exist_ok=True)
+    (out_dir / "members_directory.json").write_text(
+        json.dumps(directory, indent=2, sort_keys=True))
+    (out_dir / "lis_to_bioguide.json").write_text(
+        json.dumps(lis_map, indent=2, sort_keys=True))
+    print(f"enrich_roster: WARNING {note}", file=sys.stderr)
+
+
+def main():
+    ap = argparse.ArgumentParser(description="Enrich Congress roster via Congress.gov API.")
+    ap.add_argument("--congress", type=int, default=119)
+    args = ap.parse_args()
+
+    api_key = _load_api_key()
+    out_dir = DATA_ROOT / str(args.congress)
+    cache_dir = out_dir / "api_cache"
+    cache_dir.mkdir(parents=True, exist_ok=True)
+    warnings = []
+    directory = {}
+
+    # Pass 1: paginate full member list
+    url = (f"{API_BASE}/member/congress/{args.congress}"
+           f"?currentMember=false&limit=250&format=json&api_key={api_key}")
+    page = 0
+    while url:
+        page += 1
+        print(f"enrich_roster: fetching page {page}", file=sys.stderr)
+        data = _fetch(url, cache_dir, warnings, label=f"page {page}")
+        if data is None:
+            _write_partial(out_dir, directory, {}, warnings,
+                           f"pagination failed at page {page}; partial directory written")
+            print(f"enrich_roster: {len(directory)} members directory written; "
+                  f"0 senators with LIS resolved; {len(warnings)} warnings")
+            return 0
+        for m in data.get("members") or []:
+            norm = _normalize_member(m)
+            if norm:
+                directory[norm["bioguide"]] = norm
+        nxt = ((data.get("pagination") or {}).get("next")) or None
+        if nxt and "api_key=" not in nxt:
+            sep = "&" if "?" in nxt else "?"
+            nxt = f"{nxt}{sep}api_key={api_key}"
+        url = nxt
+
+    # Pass 2: LIS lookup for senators
+    lis_map = {}
+    senators = [b for b, e in directory.items() if (e.get("chamber") or "").lower() == "senate"]
+    print(f"enrich_roster: resolving LIS for {len(senators)} senators", file=sys.stderr)
+    resolved = 0
+    for bid in senators:
+        url = f"{API_BASE}/member/{bid}?format=json&api_key={api_key}"
+        data = _fetch(url, cache_dir, warnings, label=f"member/{bid}")
+        if data is None:
+            warnings.append(f"member/{bid}: fetch failed")
+            continue
+        member = (data.get("member") or {})
+        lis = _scan_for_lis(member)
+        if lis:
+            directory[bid]["lis"] = lis
+            lis_map[lis] = bid
+            resolved += 1
+        else:
+            warnings.append(f"member/{bid}: LIS not derivable from API")
+
+    # Fallback: name+state+party match against vote-derived senate roster.
+    # Why: Congress.gov v3 does not expose LIS reliably; name match is unique within a Congress.
+    sen_roster_path = out_dir / "senate" / "roster.json"
+    if sen_roster_path.exists():
+        def _norm(s):
+            return ''.join(c for c in unicodedata.normalize('NFKD', s or '') if not unicodedata.combining(c))
+        def _last(n):
+            p = re.sub(r'[.,]', '', _norm(n)).split()
+            return p[-1].lower() if p else ''
+        vote_sen = json.loads(sen_roster_path.read_text())
+        idx = {}
+        for bg in senators:
+            e = directory[bg]
+            idx.setdefault((_last(e.get("full_name", "")), e.get("state", ""), e.get("party", "")), []).append(bg)
+        for lis_key, v in vote_sen.items():
+            if re.match(r'^[A-Z]\d{6}$', lis_key):
+                continue
+            if lis_key in lis_map:
+                continue
+            k = (_last(v.get("name", "")), v.get("state", ""), v.get("party", ""))
+            candidates = idx.get(k, [])
+            if len(candidates) != 1:
+                candidates = [bg for (l, s, _), bs in idx.items() for bg in bs
+                              if l == k[0] and s == k[1]]
+            if len(candidates) == 1:
+                bg = candidates[0]
+                directory[bg]["lis"] = lis_key
+                lis_map[lis_key] = bg
+                resolved += 1
+
+    out_dir.mkdir(parents=True, exist_ok=True)
+    (out_dir / "members_directory.json").write_text(
+        json.dumps(directory, indent=2, sort_keys=True))
+    (out_dir / "lis_to_bioguide.json").write_text(
+        json.dumps(lis_map, indent=2, sort_keys=True))
+
+    print(f"enrich_roster: {len(directory)} members directory written; "
+          f"{resolved} senators with LIS resolved; {len(warnings)} warnings")
+    for w in warnings[:10]:
+        print(f"  warn: {w}", file=sys.stderr)
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())

+ 131 - 0
fetch.py

@@ -0,0 +1,131 @@
+#!/usr/bin/env python3
+"""Fetch raw roll-call XML for a given Congress + chamber.
+
+Usage:
+  python3 fetch.py --congress 119 --chamber house
+  python3 fetch.py --congress 119 --chamber senate
+  python3 fetch.py --congress 119 --chamber both     # default
+
+Output: data/<congress>/<chamber>/cache/*.xml (idempotent — only fetches missing).
+Throttled at 350 ms between network requests.
+"""
+import argparse, os, sys, time, urllib.request, re
+
+UA = "Mozilla/5.0 (research; polisci-analysis)"
+THROTTLE_S = 0.35
+DATA_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
+
+def _http(url):
+    req = urllib.request.Request(url, headers={"User-Agent": UA})
+    with urllib.request.urlopen(req, timeout=30) as r:
+        return r.read()
+
+def _save(path, data):
+    os.makedirs(os.path.dirname(path), exist_ok=True)
+    with open(path, "wb") as f: f.write(data)
+
+# ---------- House ----------
+
+def house_year_for_congress(congress):
+    """119 -> [2025, 2026]; 120 -> [2027, 2028]; etc."""
+    first = 1789 + (congress - 1) * 2
+    return [first, first + 1]
+
+def house_max_roll(year):
+    """Scrape the year index page to discover the highest roll number."""
+    # Try descending block pages until one returns links.
+    for block in (700, 600, 500, 400, 300, 200, 100, 0):
+        try:
+            html = _http(f"https://clerk.house.gov/evs/{year}/ROLL_{block:03d}.asp").decode("utf-8", "ignore")
+            nums = [int(n) for n in re.findall(r"rollnumber=(\d+)", html)]
+            if nums:
+                time.sleep(THROTTLE_S)
+                return max(nums)
+        except Exception:
+            time.sleep(THROTTLE_S)
+            continue
+    return 0
+
+def fetch_house(congress):
+    out_dir = os.path.join(DATA_ROOT, str(congress), "house", "cache")
+    os.makedirs(out_dir, exist_ok=True)
+    years = house_year_for_congress(congress)
+    total_fetched = 0
+    for year in years:
+        max_roll = house_max_roll(year)
+        if not max_roll:
+            print(f"  [house {year}] no votes discovered", file=sys.stderr)
+            continue
+        print(f"  [house {year}] up to roll {max_roll}", file=sys.stderr)
+        for roll in range(1, max_roll + 1):
+            path = os.path.join(out_dir, f"{year}_{roll:03d}.xml")
+            if os.path.exists(path) and os.path.getsize(path) > 200:
+                continue
+            url = f"https://clerk.house.gov/evs/{year}/roll{roll:03d}.xml"
+            try:
+                data = _http(url)
+                _save(path, data)
+                total_fetched += 1
+                time.sleep(THROTTLE_S)
+            except Exception as e:
+                print(f"    FAIL {year}/{roll}: {e}", file=sys.stderr)
+        print(f"  [house {year}] done", file=sys.stderr)
+    return total_fetched
+
+# ---------- Senate ----------
+
+def senate_max_vote(congress, session):
+    """Read vote_menu to discover the highest vote_number."""
+    url = (f"https://www.senate.gov/legislative/LIS/roll_call_lists/"
+           f"vote_menu_{congress}_{session}.xml")
+    try:
+        data = _http(url).decode("utf-8", "ignore")
+        time.sleep(THROTTLE_S)
+        nums = [int(n) for n in re.findall(r"<vote_number>(\d+)</vote_number>", data)]
+        return max(nums) if nums else 0
+    except Exception as e:
+        print(f"  [senate menu {session}] FAIL: {e}", file=sys.stderr)
+        return 0
+
+def fetch_senate(congress):
+    out_dir = os.path.join(DATA_ROOT, str(congress), "senate", "cache")
+    os.makedirs(out_dir, exist_ok=True)
+    total_fetched = 0
+    for session in (1, 2):
+        max_v = senate_max_vote(congress, session)
+        if not max_v:
+            print(f"  [senate s{session}] no votes discovered", file=sys.stderr)
+            continue
+        print(f"  [senate s{session}] up to vote {max_v}", file=sys.stderr)
+        for v in range(1, max_v + 1):
+            path = os.path.join(out_dir, f"{session}_{v:05d}.xml")
+            if os.path.exists(path) and os.path.getsize(path) > 200:
+                continue
+            url = (f"https://www.senate.gov/legislative/LIS/roll_call_votes/"
+                   f"vote{congress}{session}/vote_{congress}_{session}_{v:05d}.xml")
+            try:
+                data = _http(url)
+                _save(path, data)
+                total_fetched += 1
+                time.sleep(THROTTLE_S)
+            except Exception as e:
+                print(f"    FAIL s{session}/{v}: {e}", file=sys.stderr)
+        print(f"  [senate s{session}] done", file=sys.stderr)
+    return total_fetched
+
+# ---------- CLI ----------
+
+def main():
+    ap = argparse.ArgumentParser()
+    ap.add_argument("--congress", type=int, required=True)
+    ap.add_argument("--chamber", choices=["house", "senate", "both"], default="both")
+    args = ap.parse_args()
+    if args.chamber in ("house", "both"):
+        n = fetch_house(args.congress)
+        print(f"House: {n} new files", file=sys.stderr)
+    if args.chamber in ("senate", "both"):
+        n = fetch_senate(args.congress)
+        print(f"Senate: {n} new files", file=sys.stderr)
+
+if __name__ == "__main__":
+    main()

ファイルの差分が大きいため隠しています
+ 92 - 0
legacy/AlexandriaOcasioCortez119.html


ファイルの差分が大きいため隠しています
+ 92 - 0
legacy/ByronDonalds119.html


ファイルの差分が大きいため隠しています
+ 92 - 0
legacy/IlhanOmar119.html


ファイルの差分が大きいため隠しています
+ 92 - 0
legacy/JimJordan119.html


ファイルの差分が大きいため隠しています
+ 93 - 0
legacy/LindseyGraham119.html


ファイルの差分が大きいため隠しています
+ 92 - 0
legacy/MarjorieTaylorGreene119.html


ファイルの差分が大きいため隠しています
+ 92 - 0
legacy/RoKhanna119.html


ファイルの差分が大きいため隠しています
+ 122 - 0
legacy/ThomasMassie119.html


+ 469 - 0
legacy/build_dashboard.py

@@ -0,0 +1,469 @@
+#!/usr/bin/env python3
+"""Generate self-contained HTML dashboard for Massie 119th Congress voting analysis."""
+import json
+from collections import Counter, defaultdict
+
+votes = json.load(open("/home/user/polisci/votes.json"))
+
+# Aggregate
+align_counts = Counter(v["alignment"] for v in votes)
+massie_counts = Counter(v["massie"] for v in votes)
+blocked_counts = Counter(v["blocked"] for v in votes if v["blocked"])
+
+# Monthly alignment trend
+month_align = defaultdict(lambda: Counter())
+MONTHS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
+def month_key(v):
+    # date like "3-Jan-2025"
+    try:
+        d, m, y = v["date"].split("-")
+        return f"{y}-{MONTHS.index(m)+1:02d}"
+    except Exception:
+        return None
+
+for v in votes:
+    mk = month_key(v)
+    if mk:
+        month_align[mk][v["alignment"]] += 1
+
+months_sorted = sorted(month_align.keys())
+align_labels = ["Helped Republicans", "Helped Democrats", "Helped Both", "Helped Neither"]
+monthly_series = {lab: [month_align[m].get(lab, 0) for m in months_sorted] for lab in align_labels}
+
+# Blocked measures - list for table
+blocked_dem = [v for v in votes if v["blocked"] == "Democrat"]
+blocked_rep = [v for v in votes if v["blocked"] == "Republican"]
+
+# All vote rows (compact) for filterable table
+def row(v):
+    return {
+        "y": v["year"], "r": v["roll"], "d": v["date"],
+        "ln": v["legis_num"], "q": v["question"],
+        "ds": v["desc"][:90],
+        "rs": v["result"],
+        "m": v["massie"],
+        "ry": v["R"]["yea"], "rn": v["R"]["nay"],
+        "dy": v["D"]["yea"], "dn": v["D"]["nay"],
+        "a": v["alignment"], "b": v["blocked"] or "",
+    }
+rows = [row(v) for v in votes]
+
+# Summary stats
+total = len(votes)
+voting = sum(1 for v in votes if v["massie"] in ("Yea","Nay","Aye","No"))
+yeas = sum(1 for v in votes if v["massie"] in ("Yea","Aye"))
+nays = sum(1 for v in votes if v["massie"] in ("Nay","No"))
+nv = sum(1 for v in votes if v["massie"] == "Not Voting")
+present = sum(1 for v in votes if v["massie"] == "Present")
+
+# Lone-wolf: cases where Massie was on the losing side with very few co-defectors of his own party
+lone_wolf = 0
+for v in votes:
+    if v["massie"] not in ("Yea","Nay","Aye","No"):
+        continue
+    m_norm = "Yea" if v["massie"] in ("Yea","Aye") else "Nay"
+    r_pos = "Yea" if v["R"]["yea"] > v["R"]["nay"] else ("Nay" if v["R"]["nay"] > v["R"]["yea"] else "Split")
+    if r_pos != "Split" and m_norm != r_pos:
+        # how many Republicans defected with him?
+        defectors = v["R"]["nay"] if r_pos == "Yea" else v["R"]["yea"]
+        if defectors <= 5:
+            lone_wolf += 1
+
+voted_against_gop = 0
+voted_against_dem = 0
+voted_with_gop = 0
+voted_with_dem = 0
+for v in votes:
+    if v["massie"] not in ("Yea","Nay","Aye","No"):
+        continue
+    m_norm = "Yea" if v["massie"] in ("Yea","Aye") else "Nay"
+    r_pos = "Yea" if v["R"]["yea"] > v["R"]["nay"] else ("Nay" if v["R"]["nay"] > v["R"]["yea"] else "Split")
+    d_pos = "Yea" if v["D"]["yea"] > v["D"]["nay"] else ("Nay" if v["D"]["nay"] > v["D"]["yea"] else "Split")
+    if r_pos != "Split":
+        if m_norm == r_pos: voted_with_gop += 1
+        else: voted_against_gop += 1
+    if d_pos != "Split":
+        if m_norm == d_pos: voted_with_dem += 1
+        else: voted_against_dem += 1
+
+data = {
+    "total": total, "voting": voting, "yeas": yeas, "nays": nays,
+    "nv": nv, "present": present,
+    "alignment": dict(align_counts),
+    "massie": dict(massie_counts),
+    "blocked": dict(blocked_counts),
+    "months": months_sorted,
+    "monthly": monthly_series,
+    "rows": rows,
+    "blocked_dem_count": len(blocked_dem),
+    "blocked_rep_count": len(blocked_rep),
+    "lone_wolf": lone_wolf,
+    "voted_against_gop": voted_against_gop,
+    "voted_against_dem": voted_against_dem,
+    "voted_with_gop": voted_with_gop,
+    "voted_with_dem": voted_with_dem,
+}
+
+# Write to template
+HTML = """<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Thomas Massie - 119th Congress Voting Dashboard</title>
+<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
+<style>
+  body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
+         margin: 0; padding: 24px; background: #f5f5f7; color: #1d1d1f; }
+  h1 { margin: 0 0 4px; font-size: 28px; }
+  .sub { color: #6e6e73; margin-bottom: 24px; }
+  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; margin-bottom: 24px; }
+  .card { background: #fff; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); cursor: grab; transition: box-shadow .15s, transform .15s; }
+  .card:active { cursor: grabbing; }
+  .card.sortable-ghost { opacity: 0.4; }
+  .card.sortable-chosen { box-shadow: 0 4px 16px rgba(0,0,0,0.15); transform: scale(1.02); }
+  .card.sortable-drag { box-shadow: 0 8px 24px rgba(0,0,0,0.2); }
+  .drag-hint { font-size: 11px; color: #6e6e73; margin-bottom: 8px; }
+  .drag-hint button { margin-left: 8px; padding: 2px 8px; font-size: 11px; border: 1px solid #d2d2d7; border-radius: 4px; background: #fff; cursor: pointer; }
+  .kpi { font-size: 32px; font-weight: 700; margin: 6px 0 2px; }
+  .pct { font-size: 14px; font-weight: 500; color: #6e6e73; margin-left: 4px; }
+  .kpi-label { color: #6e6e73; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; }
+  .chart-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 24px; }
+  @media (max-width: 900px) { .chart-row { grid-template-columns: 1fr; } }
+  .chart-card { background: #fff; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); min-width: 0; }
+  .chart-box { position: relative; height: 300px; width: 100%; }
+  h2 { font-size: 18px; margin: 0 0 12px; }
+  table { width: 100%; border-collapse: collapse; font-size: 13px; }
+  th, td { padding: 6px 8px; text-align: left; border-bottom: 1px solid #e8e8ed; }
+  th { background: #fafafa; position: sticky; top: 0; cursor: pointer; user-select: none; }
+  th:hover { background: #f0f0f5; }
+  .table-wrap { max-height: 600px; overflow: auto; border: 1px solid #e8e8ed; border-radius: 8px; }
+  .filter-bar { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
+  .filter-bar input, .filter-bar select { padding: 8px 12px; border: 1px solid #d2d2d7; border-radius: 8px; font-size: 14px; }
+  .filter-bar input { flex: 1; min-width: 200px; }
+  .badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
+  .b-rep { background: #fde2e2; color: #a30000; }
+  .b-dem { background: #dde7fa; color: #1644a0; }
+  .b-both { background: #e0f0e0; color: #1e6b1e; }
+  .b-neither { background: #f0e0f0; color: #6b1e6b; }
+  .b-na { background: #eee; color: #666; }
+  .v-yea { color: #1e6b1e; font-weight: 600; }
+  .v-nay { color: #a30000; font-weight: 600; }
+  .v-nv, .v-present { color: #888; }
+  .note { font-size: 12px; color: #6e6e73; line-height: 1.5; margin-top: 12px; }
+  .footer { text-align: center; color: #6e6e73; font-size: 12px; margin-top: 32px; }
+</style>
+</head>
+<body>
+<h1>Thomas Massie (R-KY) — 119th Congress Voting Analysis</h1>
+<div class="sub">Bioguide M001184 · House roll-call votes, Jan 3, 2025 – present · Source: clerk.house.gov</div>
+
+<div class="drag-hint">↕ Drag any card to rearrange. <button id="resetOrder">Reset order</button></div>
+<div class="grid" id="cardGrid">
+  <div class="card" data-id="participation"><div class="kpi-label">Roll Calls / Participation</div><div class="kpi">__VOTING__ <span style="font-size:18px;color:#6e6e73">/ __TOTAL__</span> <span class="pct">__VOTING_PCT__%</span></div><div class="note">119th Congress, Jan 2025 – May 2026 · Yea/Aye: __YEAS__ · Nay/No: __NAYS__ · Not Voting: __NV__ · Present: __PRESENT__</div></div>
+  <div class="card" data-id="va_gop"><div class="kpi-label">Voted Against GOP Majority</div><div class="kpi" style="color:#a30000">__VA_GOP__ <span class="pct">__VA_GOP_PCT__%</span></div><div class="note">of __PARTISAN_R__ votes where R majority took a side</div></div>
+  <div class="card" data-id="va_dem"><div class="kpi-label">Voted Against Dem Majority</div><div class="kpi" style="color:#1644a0">__VA_DEM__ <span class="pct">__VA_DEM_PCT__%</span></div><div class="note">of __PARTISAN_D__ votes where D majority took a side</div></div>
+  <div class="card" data-id="blk_dem"><div class="kpi-label">Blocked Dem-Backed</div><div class="kpi" style="color:#1644a0">__BLK_DEM__ <span class="pct">__BLK_DEM_PCT__%</span></div><div class="note">Massie Nay + D wanted Yea + measure failed</div></div>
+  <div class="card" data-id="blk_rep"><div class="kpi-label">Blocked GOP-Backed</div><div class="kpi" style="color:#a30000">__BLK_REP__ <span class="pct">__BLK_REP_PCT__%</span></div><div class="note">Massie Nay + R wanted Yea + measure failed</div></div>
+  <div class="card" data-id="lone"><div class="kpi-label">Lone Wolf Defections</div><div class="kpi">__LONE__ <span class="pct">__LONE_PCT__%</span></div><div class="note">Against R majority w/ ≤5 fellow GOP defectors</div></div>
+</div>
+
+<div class="chart-row">
+  <div class="chart-card"><h2>Alignment Classification</h2><div class="chart-box"><canvas id="alignChart"></canvas></div></div>
+  <div class="chart-card"><h2>Massie's Vote Distribution</h2><div class="chart-box"><canvas id="voteChart"></canvas></div></div>
+</div>
+
+<div class="chart-row">
+  <div class="chart-card"><h2>Voted With vs. Against — by Party Majority</h2><div class="chart-box"><canvas id="partyChart"></canvas></div></div>
+  <div class="chart-card"><h2>Blocking Wins</h2><div class="chart-box"><canvas id="blockChart"></canvas></div></div>
+</div>
+
+<div class="chart-row">
+  <div class="chart-card" style="grid-column: 1 / -1"><h2>Alignment Over Time (monthly)</h2><div class="chart-box" style="height:320px"><canvas id="trendChart"></canvas></div></div>
+</div>
+
+<div class="chart-row">
+  <div class="chart-card" style="grid-column: 1 / -1">
+    <h2>All Votes (filterable)</h2>
+    <div class="filter-bar">
+      <input id="search" placeholder="Search bill/description…">
+      <select id="alignFilter">
+        <option value="">All alignments</option>
+        <option>Helped Republicans</option>
+        <option>Helped Democrats</option>
+        <option>Helped Both</option>
+        <option>Helped Neither</option>
+      </select>
+      <select id="blockFilter">
+        <option value="">All votes</option>
+        <option value="Democrat">Blocked Dem-backed</option>
+        <option value="Republican">Blocked GOP-backed</option>
+      </select>
+      <select id="massieFilter">
+        <option value="">Any Massie vote</option>
+        <option>Yea</option><option>Aye</option><option>Nay</option><option>No</option>
+        <option>Not Voting</option><option>Present</option>
+      </select>
+    </div>
+    <div class="table-wrap">
+      <table id="voteTable">
+        <thead><tr>
+          <th data-k="y">Yr</th><th data-k="r">#</th><th data-k="d">Date</th>
+          <th data-k="ln">Bill</th><th data-k="q">Question</th><th data-k="ds">Description</th>
+          <th data-k="rs">Result</th><th data-k="m">Massie</th>
+          <th data-k="ry">R Yea</th><th data-k="rn">R Nay</th>
+          <th data-k="dy">D Yea</th><th data-k="dn">D Nay</th>
+          <th data-k="a">Alignment</th><th data-k="b">Blocked</th>
+        </tr></thead>
+        <tbody id="voteBody"></tbody>
+      </table>
+    </div>
+    <div class="note">Click column headers to sort. Showing <span id="row-count">0</span> rows.</div>
+  </div>
+</div>
+
+<div class="footer">
+  Data: clerk.house.gov XML rollcalls. Build date: 2026-05-23. Independent analysis.
+</div>
+
+<script>
+const DATA = __DATA__;
+
+// Drag-and-drop card reordering (SortableJS + localStorage persistence)
+const CARD_ORDER_KEY = 'massieDashCardOrder.v1';
+const grid = document.getElementById('cardGrid');
+function applyOrder(order) {
+  if (!order) return;
+  const byId = {};
+  Array.from(grid.children).forEach(c => byId[c.dataset.id] = c);
+  order.forEach(id => { if (byId[id]) grid.appendChild(byId[id]); });
+}
+try { applyOrder(JSON.parse(localStorage.getItem(CARD_ORDER_KEY))); } catch(e){}
+Sortable.create(grid, {
+  animation: 180,
+  ghostClass: 'sortable-ghost',
+  chosenClass: 'sortable-chosen',
+  dragClass: 'sortable-drag',
+  onEnd() {
+    const order = Array.from(grid.children).map(c => c.dataset.id);
+    localStorage.setItem(CARD_ORDER_KEY, JSON.stringify(order));
+  }
+});
+document.getElementById('resetOrder').addEventListener('click', () => {
+  localStorage.removeItem(CARD_ORDER_KEY);
+  location.reload();
+});
+
+// KPI bar
+const ALIGN_COLORS = {
+  "Helped Republicans": "#d93b3b",
+  "Helped Democrats":  "#3b6ed9",
+  "Helped Both":       "#3ba85a",
+  "Helped Neither":    "#a13bd9",
+  "N/A: Not Voting":   "#999",
+  "N/A: Present":      "#bbb",
+  "N/A: Emmer":        "#ccc"
+};
+
+const alignOrder = ["Helped Republicans","Helped Democrats","Helped Both","Helped Neither","N/A: Not Voting","N/A: Present","N/A: Emmer"];
+const alignLabels = alignOrder.filter(k => DATA.alignment[k]);
+const alignData = alignLabels.map(k => DATA.alignment[k]);
+const alignColors = alignLabels.map(k => ALIGN_COLORS[k] || "#888");
+
+const alignTotal = alignData.reduce((a,b)=>a+b,0);
+const alignLabelsPct = alignLabels.map((l,i) => `${l} — ${alignData[i]} (${(alignData[i]/alignTotal*100).toFixed(1)}%)`);
+new Chart(document.getElementById('alignChart'), {
+  type: 'doughnut',
+  data: {labels: alignLabelsPct, datasets:[{data: alignData, backgroundColor: alignColors}]},
+  options: {plugins:{legend:{position:'right', labels:{boxWidth:14, font:{size:12}}},
+    tooltip:{callbacks:{label: ctx => {
+      const v = ctx.parsed; const pct = (v/alignTotal*100).toFixed(1);
+      return `${alignLabels[ctx.dataIndex]}: ${v} (${pct}%)`;
+    }}}}, responsive:true, maintainAspectRatio:false}
+});
+
+const voteOrder = ["Yea","Aye","Nay","No","Not Voting","Present","Emmer"];
+const voteLabels = voteOrder.filter(k => DATA.massie[k]);
+const voteData = voteLabels.map(k => DATA.massie[k]);
+const voteTotal = voteData.reduce((a,b)=>a+b,0);
+const barLabelPlugin = {
+  id: 'barLabel',
+  afterDatasetsDraw(chart) {
+    const {ctx} = chart;
+    chart.data.datasets.forEach((ds, di) => {
+      const meta = chart.getDatasetMeta(di);
+      meta.data.forEach((bar, i) => {
+        const v = ds.data[i];
+        const pct = (v/voteTotal*100).toFixed(1);
+        ctx.fillStyle = '#333';
+        ctx.font = '600 11px -apple-system, sans-serif';
+        ctx.textAlign = 'center';
+        ctx.fillText(`${v} (${pct}%)`, bar.x, bar.y - 6);
+      });
+    });
+  }
+};
+new Chart(document.getElementById('voteChart'), {
+  type: 'bar',
+  data: {labels: voteLabels, datasets:[{label:'Count', data: voteData,
+    backgroundColor: voteLabels.map(l => l==='Yea'||l==='Aye'?'#1e8e3e': l==='Nay'||l==='No'?'#c5221f':'#888')}]},
+  options: {plugins:{legend:{display:false},
+    tooltip:{callbacks:{label: ctx => `${ctx.parsed.y} (${(ctx.parsed.y/voteTotal*100).toFixed(1)}%)`}}},
+    responsive:true, maintainAspectRatio:false,
+    layout:{padding:{top:20}},
+    scales:{y:{beginAtZero:true}}},
+  plugins: [barLabelPlugin]
+});
+
+// Stacked: with vs against, per party
+const partyTotR = DATA.voted_with_gop + DATA.voted_against_gop;
+const partyTotD = DATA.voted_with_dem + DATA.voted_against_dem;
+new Chart(document.getElementById('partyChart'), {
+  type: 'bar',
+  data: {
+    labels: ['GOP majority', 'Dem majority'],
+    datasets: [
+      {label:'Voted WITH', data:[DATA.voted_with_gop, DATA.voted_with_dem], backgroundColor:'#1e8e3e'},
+      {label:'Voted AGAINST', data:[DATA.voted_against_gop, DATA.voted_against_dem], backgroundColor:'#c5221f'}
+    ]
+  },
+  options: {responsive:true, maintainAspectRatio:false,
+    plugins:{tooltip:{callbacks:{label: ctx => {
+      const tot = ctx.dataIndex === 0 ? partyTotR : partyTotD;
+      return `${ctx.dataset.label}: ${ctx.parsed.y} (${(ctx.parsed.y/tot*100).toFixed(1)}%)`;
+    }}}},
+    scales:{x:{stacked:true}, y:{stacked:true, beginAtZero:true}}},
+  plugins: [{
+    id:'stackPct',
+    afterDatasetsDraw(chart) {
+      const {ctx} = chart;
+      ctx.fillStyle='#fff'; ctx.font='600 12px -apple-system, sans-serif'; ctx.textAlign='center';
+      chart.data.datasets.forEach((ds,di) => {
+        chart.getDatasetMeta(di).data.forEach((bar,i) => {
+          const tot = i === 0 ? partyTotR : partyTotD;
+          const v = ds.data[i];
+          if (v > 0) ctx.fillText(`${v} (${(v/tot*100).toFixed(1)}%)`, bar.x, (bar.y + bar.base)/2 + 4);
+        });
+      });
+    }
+  }]
+});
+
+const blkTot = DATA.blocked_dem_count + DATA.blocked_rep_count;
+new Chart(document.getElementById('blockChart'), {
+  type: 'bar',
+  data: {labels:['Blocked Dem-backed','Blocked GOP-backed'],
+    datasets:[{label:'Count', data:[DATA.blocked_dem_count, DATA.blocked_rep_count],
+      backgroundColor:['#3b6ed9','#d93b3b']}]},
+  options:{indexAxis:'y', responsive:true, maintainAspectRatio:false,
+    plugins:{legend:{display:false},
+      tooltip:{callbacks:{label: ctx => `${ctx.parsed.x} (${(ctx.parsed.x/blkTot*100).toFixed(1)}%)`}}},
+    scales:{x:{beginAtZero:true}}},
+  plugins:[{
+    id:'hbarLabel',
+    afterDatasetsDraw(chart){
+      const {ctx} = chart;
+      ctx.fillStyle='#333'; ctx.font='600 12px -apple-system, sans-serif'; ctx.textAlign='left'; ctx.textBaseline='middle';
+      chart.getDatasetMeta(0).data.forEach((bar,i) => {
+        const v = chart.data.datasets[0].data[i];
+        ctx.fillText(`  ${v} (${(v/blkTot*100).toFixed(1)}%)`, bar.x, bar.y);
+      });
+    }
+  }]
+});
+
+const trendLabels = DATA.months;
+const trendSets = ["Helped Republicans","Helped Democrats","Helped Both","Helped Neither"].map(k => ({
+  label: k, data: DATA.monthly[k], borderColor: ALIGN_COLORS[k], backgroundColor: ALIGN_COLORS[k]+'33',
+  tension: 0.3, fill: false
+}));
+new Chart(document.getElementById('trendChart'), {
+  type: 'line',
+  data: {labels: trendLabels, datasets: trendSets},
+  options: {responsive:true, maintainAspectRatio:false,
+    scales:{y:{beginAtZero:true, title:{display:true,text:'Votes per month'}}},
+    interaction:{mode:'index', intersect:false}}
+});
+
+// Table
+let sortKey = 'r', sortDesc = true, sortYearSecondary = true;
+function renderTable() {
+  const q = document.getElementById('search').value.toLowerCase();
+  const af = document.getElementById('alignFilter').value;
+  const bf = document.getElementById('blockFilter').value;
+  const mf = document.getElementById('massieFilter').value;
+  let rows = DATA.rows.filter(r => {
+    if (q && !((r.ln+' '+r.ds+' '+r.q).toLowerCase().includes(q))) return false;
+    if (af && r.a !== af) return false;
+    if (bf && r.b !== bf) return false;
+    if (mf && r.m !== mf) return false;
+    return true;
+  });
+  rows.sort((a,b)=>{
+    const av = a[sortKey], bv = b[sortKey];
+    let cmp;
+    if (typeof av === 'number') cmp = av - bv;
+    else cmp = String(av).localeCompare(String(bv));
+    if (cmp === 0 && sortKey !== 'r') cmp = (a.y - b.y) || (a.r - b.r);
+    return sortDesc ? -cmp : cmp;
+  });
+  const tb = document.getElementById('voteBody');
+  const aClass = a => 'badge ' + ({"Helped Republicans":"b-rep","Helped Democrats":"b-dem","Helped Both":"b-both","Helped Neither":"b-neither"}[a] || "b-na");
+  const mClass = m => 'v-' + (m==='Yea'||m==='Aye'?'yea':m==='Nay'||m==='No'?'nay':m==='Not Voting'?'nv':'present');
+  tb.innerHTML = rows.map(r => `<tr>
+    <td>${r.y}</td><td>${r.r}</td><td>${r.d}</td>
+    <td>${r.ln||''}</td><td>${r.q||''}</td><td>${r.ds||''}</td>
+    <td>${r.rs}</td>
+    <td class="${mClass(r.m)}">${r.m||''}</td>
+    <td>${r.ry}</td><td>${r.rn}</td><td>${r.dy}</td><td>${r.dn}</td>
+    <td><span class="${aClass(r.a)}">${r.a}</span></td>
+    <td>${r.b ? `<span class="badge ${r.b==='Democrat'?'b-dem':'b-rep'}">${r.b}</span>` : ''}</td>
+  </tr>`).join('');
+  document.getElementById('row-count').textContent = rows.length;
+}
+document.querySelectorAll('#voteTable th').forEach(th => {
+  th.addEventListener('click', () => {
+    const k = th.dataset.k;
+    if (sortKey === k) sortDesc = !sortDesc;
+    else { sortKey = k; sortDesc = true; }
+    renderTable();
+  });
+});
+['search','alignFilter','blockFilter','massieFilter'].forEach(id =>
+  document.getElementById(id).addEventListener('input', renderTable));
+renderTable();
+</script>
+</body>
+</html>
+"""
+
+def pct(n, d): return f"{(n/d*100):.1f}" if d else "0.0"
+partisan_r = data["voted_with_gop"] + data["voted_against_gop"]
+partisan_d = data["voted_with_dem"] + data["voted_against_dem"]
+
+html = (HTML
+    .replace("__TOTAL__", str(data["total"]))
+    .replace("__VOTING__", str(data["voting"]))
+    .replace("__VOTING_PCT__", pct(data["voting"], data["total"]))
+    .replace("__YEAS__", str(data["yeas"]))
+    .replace("__NAYS__", str(data["nays"]))
+    .replace("__NV__", str(data["nv"]))
+    .replace("__PRESENT__", str(data["present"]))
+    .replace("__VA_GOP__", str(data["voted_against_gop"]))
+    .replace("__VA_GOP_PCT__", pct(data["voted_against_gop"], partisan_r))
+    .replace("__VA_DEM__", str(data["voted_against_dem"]))
+    .replace("__VA_DEM_PCT__", pct(data["voted_against_dem"], partisan_d))
+    .replace("__PARTISAN_R__", str(partisan_r))
+    .replace("__PARTISAN_D__", str(partisan_d))
+    .replace("__BLK_DEM__", str(data["blocked_dem_count"]))
+    .replace("__BLK_DEM_PCT__", pct(data["blocked_dem_count"], data["total"]))
+    .replace("__BLK_REP__", str(data["blocked_rep_count"]))
+    .replace("__BLK_REP_PCT__", pct(data["blocked_rep_count"], data["total"]))
+    .replace("__LONE__", str(data["lone_wolf"]))
+    .replace("__LONE_PCT__", pct(data["lone_wolf"], data["voting"]))
+    .replace("__DATA__", json.dumps(data))
+)
+
+with open("/home/user/polisci/dashboard.html", "w") as f:
+    f.write(html)
+print(f"Wrote dashboard.html: {len(html)} bytes, {data['lone_wolf']} lone-wolf votes")

+ 87 - 0
legacy/build_index.py

@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+"""Generate results/index.html — a landing page linking to every dashboard
+in the results/ directory. Rerun anytime new dashboards are added."""
+import os, re, html
+
+RESULTS = "/home/user/polisci/results"
+
+# Metadata for each dashboard (name, party, chamber, district/state).
+# Anything not listed here is auto-rendered with just the filename.
+META = {
+    "ThomasMassie119.html":          ("Thomas Massie",            "R", "House",  "KY-4"),
+    "RoKhanna119.html":              ("Ro Khanna",                "D", "House",  "CA-17"),
+    "AlexandriaOcasioCortez119.html":("Alexandria Ocasio-Cortez", "D", "House",  "NY-14"),
+    "IlhanOmar119.html":             ("Ilhan Omar",               "D", "House",  "MN-5"),
+    "MarjorieTaylorGreene119.html":  ("Marjorie Taylor Greene",   "R", "House",  "GA-14"),
+    "JimJordan119.html":             ("Jim Jordan",               "R", "House",  "OH-4"),
+    "ByronDonalds119.html":          ("Byron Donalds",            "R", "House",  "FL-19"),
+    "LindseyGraham119.html":         ("Lindsey Graham",           "R", "Senate", "SC"),
+}
+
+PARTY_COLOR = {"R": "#d93b3b", "D": "#3b6ed9", "I": "#888"}
+CHAMBER_COLOR = {"House": "#1e6b1e", "Senate": "#5a3d8a"}
+
+def main():
+    files = sorted(f for f in os.listdir(RESULTS) if f.endswith(".html") and f != "index.html")
+    cards = []
+    for f in files:
+        meta = META.get(f)
+        if meta:
+            name, party, chamber, loc = meta
+        else:
+            name = re.sub(r"119\.html$", "", f)
+            party, chamber, loc = "?", "?", ""
+        pc = PARTY_COLOR.get(party, "#888")
+        cc = CHAMBER_COLOR.get(chamber, "#888")
+        cards.append(f"""
+    <a class="card" href="{html.escape(f)}">
+      <div class="name">{html.escape(name)}</div>
+      <div class="pills">
+        <span class="pill" style="background:{pc}">{html.escape(party)}</span>
+        <span class="pill" style="background:{cc}">{html.escape(chamber)}</span>
+        <span class="loc">{html.escape(loc)}</span>
+      </div>
+      <div class="file">{html.escape(f)}</div>
+    </a>""")
+
+    page = f"""<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>119th Congress Voting Dashboards — Index</title>
+<style>
+  body{{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
+       margin:0;padding:32px;background:#f5f5f7;color:#1d1d1f;max-width:1100px;margin-left:auto;margin-right:auto;}}
+  h1{{margin:0 0 6px;font-size:28px;}}
+  .sub{{color:#6e6e73;margin-bottom:28px;line-height:1.5;}}
+  .sub a{{color:#1644a0;}}
+  .grid{{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px;}}
+  .card{{background:#fff;border-radius:12px;padding:18px;box-shadow:0 1px 3px rgba(0,0,0,.06);
+        text-decoration:none;color:inherit;transition:transform .12s,box-shadow .12s;display:block;}}
+  .card:hover{{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.1);}}
+  .name{{font-size:17px;font-weight:600;margin-bottom:8px;}}
+  .pills{{display:flex;align-items:center;gap:6px;margin-bottom:8px;flex-wrap:wrap;}}
+  .pill{{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;color:#fff;}}
+  .loc{{font-size:12px;color:#6e6e73;margin-left:4px;}}
+  .file{{font-size:11px;color:#9a9aa1;font-family:ui-monospace,SF Mono,Menlo,monospace;}}
+  .footer{{margin-top:32px;text-align:center;color:#6e6e73;font-size:12px;}}
+</style>
+</head>
+<body>
+<h1>119th Congress — Voting Dashboards</h1>
+<div class="sub">
+  Per-member voting analysis for the 119th U.S. Congress (Jan 2025 – present).
+  Click any card to open its dashboard. See
+  <a href="../DOCUMENTATION.md">DOCUMENTATION.md</a> for methodology, data sources, and roster notes.
+</div>
+<div class="grid">{''.join(cards)}
+</div>
+<div class="footer">{len(files)} dashboards · Sources: clerk.house.gov (House) · senate.gov LIS (Senate)</div>
+</body></html>
+"""
+    out = os.path.join(RESULTS, "index.html")
+    with open(out, "w") as f: f.write(page)
+    print(f"Wrote {out} with {len(files)} entries")
+
+if __name__ == "__main__":
+    main()

+ 427 - 0
legacy/build_member.py

@@ -0,0 +1,427 @@
+#!/usr/bin/env python3
+"""Parameterized builder: parse cached clerk.house.gov XMLs for a given member
+and emit a self-contained dashboard HTML.
+
+Usage: python3 build_member.py <bioguide_id> "<Full Name>" <party R|D|I> <output_filename>
+"""
+import sys, os, json, glob
+import xml.etree.ElementTree as ET
+from collections import Counter, defaultdict
+
+CACHE_DIR = "/home/user/polisci/vote_cache"
+OUT_DIR = "/home/user/polisci/results"
+MONTHS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
+
+def parse_xml(path, year, roll, bioguide):
+    try:
+        with open(path, "rb") as f:
+            root = ET.fromstring(f.read())
+    except Exception:
+        return None
+    meta = root.find("vote-metadata")
+    if meta is None: return None
+    def t(tag):
+        el = meta.find(tag)
+        return (el.text or "").strip() if el is not None else ""
+    info = {
+        "year": year, "roll": roll,
+        "date": t("action-date"),
+        "question": t("vote-question"),
+        "result": t("vote-result"),
+        "legis_num": t("legis-num"),
+        "desc": t("vote-desc"),
+    }
+    party_totals = {}
+    for pt in meta.findall("vote-totals/totals-by-party"):
+        party = pt.findtext("party","").strip()
+        party_totals[party] = {
+            "yea": int(pt.findtext("yea-total","0") or 0),
+            "nay": int(pt.findtext("nay-total","0") or 0),
+        }
+    info["R"] = party_totals.get("Republican", {"yea":0,"nay":0})
+    info["D"] = party_totals.get("Democratic", {"yea":0,"nay":0})
+    member_vote = None
+    for rv in root.iter("recorded-vote"):
+        leg = rv.find("legislator")
+        if leg is not None and leg.get("name-id") == bioguide:
+            v = rv.find("vote")
+            member_vote = (v.text or "").strip() if v is not None else None
+            break
+    info["member"] = member_vote
+    return info
+
+def classify(v):
+    r_yea, r_nay = v["R"]["yea"], v["R"]["nay"]
+    d_yea, d_nay = v["D"]["yea"], v["D"]["nay"]
+    r_pos = "Yea" if r_yea > r_nay else ("Nay" if r_nay > r_yea else "Split")
+    d_pos = "Yea" if d_yea > d_nay else ("Nay" if d_nay > d_yea else "Split")
+    m = v["member"]
+    if m not in ("Yea","Nay","Aye","No"):
+        return ("N/A: " + (m or "absent"), None, r_pos, d_pos)
+    m_norm = "Yea" if m in ("Yea","Aye") else "Nay"
+    helped_r = (r_pos != "Split" and m_norm == r_pos)
+    helped_d = (d_pos != "Split" and m_norm == d_pos)
+    if helped_r and helped_d: align = "Helped Both"
+    elif helped_r: align = "Helped Republicans"
+    elif helped_d: align = "Helped Democrats"
+    else: align = "Helped Neither"
+    result = v["result"].lower()
+    measure_failed = "fail" in result or "reject" in result or "not agreed" in result or "not passed" in result
+    blocked = None
+    if d_pos == "Yea" and measure_failed and m_norm == "Nay" and r_pos != "Yea":
+        blocked = "Democrat"
+    if r_pos == "Yea" and measure_failed and m_norm == "Nay" and d_pos != "Yea":
+        blocked = "Republican"
+    return (align, blocked, r_pos, d_pos)
+
+def gather(bioguide, member_party):
+    votes = []
+    for path in sorted(glob.glob(f"{CACHE_DIR}/*.xml")):
+        base = os.path.basename(path).replace(".xml","")
+        try:
+            year, roll = base.split("_")
+            year, roll = int(year), int(roll)
+        except Exception:
+            continue
+        v = parse_xml(path, year, roll, bioguide)
+        if not v: continue
+        align, blocked, r_pos, d_pos = classify(v)
+        v["alignment"] = align
+        v["blocked"] = blocked
+        v["r_pos"] = r_pos
+        v["d_pos"] = d_pos
+        votes.append(v)
+    return votes
+
+def aggregate(votes, member_party):
+    align_counts = Counter(v["alignment"] for v in votes)
+    member_counts = Counter(v["member"] for v in votes)
+    blocked_counts = Counter(v["blocked"] for v in votes if v["blocked"])
+    month_align = defaultdict(Counter)
+    for v in votes:
+        try:
+            d,m,y = v["date"].split("-")
+            mk = f"{y}-{MONTHS.index(m)+1:02d}"
+            month_align[mk][v["alignment"]] += 1
+        except Exception:
+            pass
+    months_sorted = sorted(month_align.keys())
+    align_labels = ["Helped Republicans","Helped Democrats","Helped Both","Helped Neither"]
+    monthly = {l: [month_align[m].get(l,0) for m in months_sorted] for l in align_labels}
+
+    def row(v):
+        return {"y":v["year"],"r":v["roll"],"d":v["date"],"ln":v["legis_num"],
+                "q":v["question"],"ds":v["desc"][:90],"rs":v["result"],
+                "m":v["member"],
+                "ry":v["R"]["yea"],"rn":v["R"]["nay"],
+                "dy":v["D"]["yea"],"dn":v["D"]["nay"],
+                "a":v["alignment"],"b":v["blocked"] or ""}
+    rows = [row(v) for v in votes]
+
+    total = len(votes)
+    voting = sum(1 for v in votes if v["member"] in ("Yea","Nay","Aye","No"))
+    yeas = sum(1 for v in votes if v["member"] in ("Yea","Aye"))
+    nays = sum(1 for v in votes if v["member"] in ("Nay","No"))
+    nv = sum(1 for v in votes if v["member"] == "Not Voting")
+    present = sum(1 for v in votes if v["member"] == "Present")
+
+    va_gop = vw_gop = va_dem = vw_dem = 0
+    for v in votes:
+        if v["member"] not in ("Yea","Nay","Aye","No"): continue
+        m_norm = "Yea" if v["member"] in ("Yea","Aye") else "Nay"
+        r_pos = v["r_pos"]; d_pos = v["d_pos"]
+        if r_pos != "Split":
+            if m_norm == r_pos: vw_gop += 1
+            else: va_gop += 1
+        if d_pos != "Split":
+            if m_norm == d_pos: vw_dem += 1
+            else: va_dem += 1
+
+    # Lone-wolf: defected from OWN party majority with <=5 fellow defectors
+    own = member_party
+    lone_wolf = 0
+    for v in votes:
+        if v["member"] not in ("Yea","Nay","Aye","No"): continue
+        m_norm = "Yea" if v["member"] in ("Yea","Aye") else "Nay"
+        own_pos = v["r_pos"] if own == "R" else v["d_pos"]
+        if own_pos == "Split" or m_norm == own_pos: continue
+        own_totals = v["R"] if own == "R" else v["D"]
+        defectors = own_totals["nay"] if own_pos == "Yea" else own_totals["yea"]
+        if defectors <= 5: lone_wolf += 1
+
+    blocked_dem = sum(1 for v in votes if v["blocked"] == "Democrat")
+    blocked_rep = sum(1 for v in votes if v["blocked"] == "Republican")
+
+    return {
+        "total":total,"voting":voting,"yeas":yeas,"nays":nays,"nv":nv,"present":present,
+        "alignment":dict(align_counts),"member":dict(member_counts),
+        "blocked":dict(blocked_counts),"months":months_sorted,"monthly":monthly,
+        "rows":rows,
+        "blocked_dem_count":blocked_dem,"blocked_rep_count":blocked_rep,
+        "lone_wolf":lone_wolf,
+        "voted_against_gop":va_gop,"voted_against_dem":va_dem,
+        "voted_with_gop":vw_gop,"voted_with_dem":vw_dem,
+        "voted_against_own":(va_gop if own=="R" else va_dem),
+        "voted_with_own":(vw_gop if own=="R" else vw_dem),
+    }
+
+HTML_TEMPLATE = r"""<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>__NAME__ - 119th Congress Voting Dashboard</title>
+<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
+<style>
+  body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;margin:0;padding:24px;background:#f5f5f7;color:#1d1d1f;}
+  h1{margin:0 0 4px;font-size:28px;} .sub{color:#6e6e73;margin-bottom:24px;}
+  .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;margin-bottom:24px;}
+  .card{background:#fff;border-radius:12px;padding:20px;box-shadow:0 1px 3px rgba(0,0,0,.06);cursor:grab;transition:box-shadow .15s,transform .15s;}
+  .card:active{cursor:grabbing;} .card.sortable-ghost{opacity:.4;}
+  .card.sortable-chosen{box-shadow:0 4px 16px rgba(0,0,0,.15);transform:scale(1.02);}
+  .card.sortable-drag{box-shadow:0 8px 24px rgba(0,0,0,.2);}
+  .kpi{font-size:32px;font-weight:700;margin:6px 0 2px;} .pct{font-size:14px;font-weight:500;color:#6e6e73;margin-left:4px;}
+  .kpi-label{color:#6e6e73;font-size:13px;text-transform:uppercase;letter-spacing:.5px;}
+  .chart-row{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:24px;}
+  @media (max-width:900px){.chart-row{grid-template-columns:1fr;}}
+  .chart-card{background:#fff;border-radius:12px;padding:20px;box-shadow:0 1px 3px rgba(0,0,0,.06);min-width:0;}
+  .chart-box{position:relative;height:300px;width:100%;}
+  h2{font-size:18px;margin:0 0 12px;}
+  table{width:100%;border-collapse:collapse;font-size:13px;}
+  th,td{padding:6px 8px;text-align:left;border-bottom:1px solid #e8e8ed;}
+  th{background:#fafafa;position:sticky;top:0;cursor:pointer;user-select:none;}
+  th:hover{background:#f0f0f5;}
+  .table-wrap{max-height:600px;overflow:auto;border:1px solid #e8e8ed;border-radius:8px;}
+  .filter-bar{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;}
+  .filter-bar input,.filter-bar select{padding:8px 12px;border:1px solid #d2d2d7;border-radius:8px;font-size:14px;}
+  .filter-bar input{flex:1;min-width:200px;}
+  .badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;}
+  .b-rep{background:#fde2e2;color:#a30000;} .b-dem{background:#dde7fa;color:#1644a0;}
+  .b-both{background:#e0f0e0;color:#1e6b1e;} .b-neither{background:#f0e0f0;color:#6b1e6b;}
+  .b-na{background:#eee;color:#666;}
+  .v-yea{color:#1e6b1e;font-weight:600;} .v-nay{color:#a30000;font-weight:600;}
+  .v-nv,.v-present{color:#888;}
+  .note{font-size:12px;color:#6e6e73;line-height:1.5;margin-top:12px;}
+  .drag-hint{font-size:11px;color:#6e6e73;margin-bottom:8px;}
+  .drag-hint button{margin-left:8px;padding:2px 8px;font-size:11px;border:1px solid #d2d2d7;border-radius:4px;background:#fff;cursor:pointer;}
+  .party-pill{display:inline-block;padding:2px 8px;border-radius:10px;font-size:12px;font-weight:600;margin-left:8px;color:#fff;}
+  .footer{text-align:center;color:#6e6e73;font-size:12px;margin-top:32px;}
+  .member-note{background:#fff8e1;border-left:4px solid #f5b400;padding:12px 16px;border-radius:6px;margin:0 0 20px;font-size:14px;line-height:1.5;color:#5a4500;}
+  .member-note strong{color:#3d2f00;}
+</style>
+</head>
+<body>
+<h1>__NAME__<span class="party-pill" style="background:__PARTY_COLOR__">__PARTY__</span></h1>
+<div class="sub">Bioguide __BIOGUIDE__ · House roll-call votes, 119th Congress (Jan 2025 – present) · Source: clerk.house.gov</div>
+__MEMBER_NOTE__
+
+<div class="drag-hint">↕ Drag any card to rearrange. <button id="resetOrder">Reset order</button></div>
+<div class="grid" id="cardGrid">
+  <div class="card" data-id="participation"><div class="kpi-label">Roll Calls / Participation</div><div class="kpi">__VOTING__ <span style="font-size:18px;color:#6e6e73">/ __TOTAL__</span> <span class="pct">__VOTING_PCT__%</span></div><div class="note">119th Congress · Yea/Aye: __YEAS__ · Nay/No: __NAYS__ · Not Voting: __NV__ · Present: __PRESENT__</div></div>
+  <div class="card" data-id="va_gop"><div class="kpi-label">Voted Against GOP Majority</div><div class="kpi" style="color:#a30000">__VA_GOP__ <span class="pct">__VA_GOP_PCT__%</span></div><div class="note">of __PARTISAN_R__ votes where R majority took a side</div></div>
+  <div class="card" data-id="va_dem"><div class="kpi-label">Voted Against Dem Majority</div><div class="kpi" style="color:#1644a0">__VA_DEM__ <span class="pct">__VA_DEM_PCT__%</span></div><div class="note">of __PARTISAN_D__ votes where D majority took a side</div></div>
+  <div class="card" data-id="blk_dem"><div class="kpi-label">Blocked Dem-Backed</div><div class="kpi" style="color:#1644a0">__BLK_DEM__ <span class="pct">__BLK_DEM_PCT__%</span></div><div class="note">Member Nay + D wanted Yea + measure failed</div></div>
+  <div class="card" data-id="blk_rep"><div class="kpi-label">Blocked GOP-Backed</div><div class="kpi" style="color:#a30000">__BLK_REP__ <span class="pct">__BLK_REP_PCT__%</span></div><div class="note">Member Nay + R wanted Yea + measure failed</div></div>
+  <div class="card" data-id="lone"><div class="kpi-label">Lone Wolf Defections</div><div class="kpi">__LONE__ <span class="pct">__LONE_PCT__%</span></div><div class="note">Defected from own (__PARTY__) party majority w/ ≤5 fellow defectors</div></div>
+</div>
+
+<div class="chart-row">
+  <div class="chart-card"><h2>Alignment Classification</h2><div class="chart-box"><canvas id="alignChart"></canvas></div></div>
+  <div class="chart-card"><h2>Vote Distribution</h2><div class="chart-box"><canvas id="voteChart"></canvas></div></div>
+</div>
+
+<div class="chart-row">
+  <div class="chart-card"><h2>Voted With vs. Against — by Party Majority</h2><div class="chart-box"><canvas id="partyChart"></canvas></div></div>
+  <div class="chart-card"><h2>Blocking Wins</h2><div class="chart-box"><canvas id="blockChart"></canvas></div></div>
+</div>
+
+<div class="chart-row">
+  <div class="chart-card" style="grid-column:1 / -1"><h2>Alignment Over Time (monthly)</h2><div class="chart-box" style="height:320px"><canvas id="trendChart"></canvas></div></div>
+</div>
+
+<div class="chart-row">
+  <div class="chart-card" style="grid-column:1 / -1">
+    <h2>All Votes (filterable)</h2>
+    <div class="filter-bar">
+      <input id="search" placeholder="Search bill/description…">
+      <select id="alignFilter"><option value="">All alignments</option><option>Helped Republicans</option><option>Helped Democrats</option><option>Helped Both</option><option>Helped Neither</option></select>
+      <select id="blockFilter"><option value="">All votes</option><option value="Democrat">Blocked Dem-backed</option><option value="Republican">Blocked GOP-backed</option></select>
+      <select id="memberFilter"><option value="">Any vote</option><option>Yea</option><option>Aye</option><option>Nay</option><option>No</option><option>Not Voting</option><option>Present</option></select>
+    </div>
+    <div class="table-wrap"><table id="voteTable">
+      <thead><tr><th data-k="y">Yr</th><th data-k="r">#</th><th data-k="d">Date</th><th data-k="ln">Bill</th><th data-k="q">Question</th><th data-k="ds">Description</th><th data-k="rs">Result</th><th data-k="m">Vote</th><th data-k="ry">R Yea</th><th data-k="rn">R Nay</th><th data-k="dy">D Yea</th><th data-k="dn">D Nay</th><th data-k="a">Alignment</th><th data-k="b">Blocked</th></tr></thead>
+      <tbody id="voteBody"></tbody></table></div>
+    <div class="note">Click headers to sort. Showing <span id="row-count">0</span> rows.</div>
+  </div>
+</div>
+<div class="footer">Source: clerk.house.gov XML rollcalls · Built 2026-05-23 · See DOCUMENTATION.md for methodology</div>
+
+<script>
+const DATA = __DATA__;
+const MEMBER_PARTY = "__PARTY__";
+
+const CARD_ORDER_KEY = 'dashCardOrder.__BIOGUIDE__';
+const grid = document.getElementById('cardGrid');
+function applyOrder(order){if(!order)return;const byId={};Array.from(grid.children).forEach(c=>byId[c.dataset.id]=c);order.forEach(id=>{if(byId[id])grid.appendChild(byId[id]);});}
+try{applyOrder(JSON.parse(localStorage.getItem(CARD_ORDER_KEY)));}catch(e){}
+Sortable.create(grid,{animation:180,ghostClass:'sortable-ghost',chosenClass:'sortable-chosen',dragClass:'sortable-drag',
+  onEnd(){localStorage.setItem(CARD_ORDER_KEY,JSON.stringify(Array.from(grid.children).map(c=>c.dataset.id)));}});
+document.getElementById('resetOrder').addEventListener('click',()=>{localStorage.removeItem(CARD_ORDER_KEY);location.reload();});
+
+const ALIGN_COLORS={"Helped Republicans":"#d93b3b","Helped Democrats":"#3b6ed9","Helped Both":"#3ba85a","Helped Neither":"#a13bd9","N/A: Not Voting":"#999","N/A: Present":"#bbb"};
+const alignOrder=["Helped Republicans","Helped Democrats","Helped Both","Helped Neither","N/A: Not Voting","N/A: Present"];
+const alignLabels=alignOrder.filter(k=>DATA.alignment[k]);
+const alignData=alignLabels.map(k=>DATA.alignment[k]);
+const alignColors=alignLabels.map(k=>ALIGN_COLORS[k]||"#888");
+const alignTotal=alignData.reduce((a,b)=>a+b,0);
+const alignLabelsPct=alignLabels.map((l,i)=>`${l} — ${alignData[i]} (${(alignData[i]/alignTotal*100).toFixed(1)}%)`);
+new Chart(document.getElementById('alignChart'),{type:'doughnut',data:{labels:alignLabelsPct,datasets:[{data:alignData,backgroundColor:alignColors}]},
+  options:{plugins:{legend:{position:'right',labels:{boxWidth:14,font:{size:12}}},
+    tooltip:{callbacks:{label:ctx=>`${alignLabels[ctx.dataIndex]}: ${ctx.parsed} (${(ctx.parsed/alignTotal*100).toFixed(1)}%)`}}},
+    responsive:true,maintainAspectRatio:false}});
+
+const voteOrder=["Yea","Aye","Nay","No","Not Voting","Present"];
+const voteLabels=voteOrder.filter(k=>DATA.member[k]);
+const voteData=voteLabels.map(k=>DATA.member[k]);
+const voteTotal=voteData.reduce((a,b)=>a+b,0);
+const barLabelPlugin={id:'barLabel',afterDatasetsDraw(chart){const {ctx}=chart;
+  chart.data.datasets.forEach((ds,di)=>{chart.getDatasetMeta(di).data.forEach((bar,i)=>{
+    const v=ds.data[i],pct=(v/voteTotal*100).toFixed(1);
+    ctx.fillStyle='#333';ctx.font='600 11px -apple-system, sans-serif';ctx.textAlign='center';
+    ctx.fillText(`${v} (${pct}%)`,bar.x,bar.y-6);});});}};
+new Chart(document.getElementById('voteChart'),{type:'bar',
+  data:{labels:voteLabels,datasets:[{label:'Count',data:voteData,
+    backgroundColor:voteLabels.map(l=>l==='Yea'||l==='Aye'?'#1e8e3e':l==='Nay'||l==='No'?'#c5221f':'#888')}]},
+  options:{plugins:{legend:{display:false},tooltip:{callbacks:{label:ctx=>`${ctx.parsed.y} (${(ctx.parsed.y/voteTotal*100).toFixed(1)}%)`}}},
+    responsive:true,maintainAspectRatio:false,layout:{padding:{top:20}},scales:{y:{beginAtZero:true}}},
+  plugins:[barLabelPlugin]});
+
+const partyTotR=DATA.voted_with_gop+DATA.voted_against_gop;
+const partyTotD=DATA.voted_with_dem+DATA.voted_against_dem;
+new Chart(document.getElementById('partyChart'),{type:'bar',
+  data:{labels:['GOP majority','Dem majority'],datasets:[
+    {label:'Voted WITH',data:[DATA.voted_with_gop,DATA.voted_with_dem],backgroundColor:'#1e8e3e'},
+    {label:'Voted AGAINST',data:[DATA.voted_against_gop,DATA.voted_against_dem],backgroundColor:'#c5221f'}]},
+  options:{responsive:true,maintainAspectRatio:false,
+    plugins:{tooltip:{callbacks:{label:ctx=>{const tot=ctx.dataIndex===0?partyTotR:partyTotD;return `${ctx.dataset.label}: ${ctx.parsed.y} (${(ctx.parsed.y/tot*100).toFixed(1)}%)`;}}}},
+    scales:{x:{stacked:true},y:{stacked:true,beginAtZero:true}}},
+  plugins:[{id:'stackPct',afterDatasetsDraw(chart){const {ctx}=chart;
+    ctx.fillStyle='#fff';ctx.font='600 12px -apple-system, sans-serif';ctx.textAlign='center';
+    chart.data.datasets.forEach((ds,di)=>{chart.getDatasetMeta(di).data.forEach((bar,i)=>{
+      const tot=i===0?partyTotR:partyTotD,v=ds.data[i];
+      if(v>0)ctx.fillText(`${v} (${(v/tot*100).toFixed(1)}%)`,bar.x,(bar.y+bar.base)/2+4);});});}}]});
+
+const blkTot=DATA.blocked_dem_count+DATA.blocked_rep_count;
+new Chart(document.getElementById('blockChart'),{type:'bar',
+  data:{labels:['Blocked Dem-backed','Blocked GOP-backed'],
+    datasets:[{label:'Count',data:[DATA.blocked_dem_count,DATA.blocked_rep_count],backgroundColor:['#3b6ed9','#d93b3b']}]},
+  options:{indexAxis:'y',responsive:true,maintainAspectRatio:false,
+    plugins:{legend:{display:false},tooltip:{callbacks:{label:ctx=>`${ctx.parsed.x} (${blkTot?(ctx.parsed.x/blkTot*100).toFixed(1):'0.0'}%)`}}},
+    scales:{x:{beginAtZero:true}}},
+  plugins:[{id:'hbarLabel',afterDatasetsDraw(chart){const {ctx}=chart;
+    ctx.fillStyle='#333';ctx.font='600 12px -apple-system, sans-serif';ctx.textAlign='left';ctx.textBaseline='middle';
+    chart.getDatasetMeta(0).data.forEach((bar,i)=>{const v=chart.data.datasets[0].data[i];
+      ctx.fillText(`  ${v} (${blkTot?(v/blkTot*100).toFixed(1):'0.0'}%)`,bar.x,bar.y);});}}]});
+
+const trendLabels=DATA.months;
+const trendSets=["Helped Republicans","Helped Democrats","Helped Both","Helped Neither"].map(k=>({
+  label:k,data:DATA.monthly[k],borderColor:ALIGN_COLORS[k],backgroundColor:ALIGN_COLORS[k]+'33',tension:.3,fill:false}));
+new Chart(document.getElementById('trendChart'),{type:'line',data:{labels:trendLabels,datasets:trendSets},
+  options:{responsive:true,maintainAspectRatio:false,
+    scales:{y:{beginAtZero:true,title:{display:true,text:'Votes per month'}}},interaction:{mode:'index',intersect:false}}});
+
+let sortKey='r',sortDesc=true;
+function renderTable(){
+  const q=document.getElementById('search').value.toLowerCase();
+  const af=document.getElementById('alignFilter').value;
+  const bf=document.getElementById('blockFilter').value;
+  const mf=document.getElementById('memberFilter').value;
+  let rows=DATA.rows.filter(r=>{
+    if(q&&!((r.ln+' '+r.ds+' '+r.q).toLowerCase().includes(q)))return false;
+    if(af&&r.a!==af)return false; if(bf&&r.b!==bf)return false; if(mf&&r.m!==mf)return false; return true;});
+  rows.sort((a,b)=>{const av=a[sortKey],bv=b[sortKey];let cmp;
+    if(typeof av==='number')cmp=av-bv;else cmp=String(av).localeCompare(String(bv));
+    if(cmp===0&&sortKey!=='r')cmp=(a.y-b.y)||(a.r-b.r);return sortDesc?-cmp:cmp;});
+  const tb=document.getElementById('voteBody');
+  const aClass=a=>'badge '+({"Helped Republicans":"b-rep","Helped Democrats":"b-dem","Helped Both":"b-both","Helped Neither":"b-neither"}[a]||"b-na");
+  const mClass=m=>'v-'+(m==='Yea'||m==='Aye'?'yea':m==='Nay'||m==='No'?'nay':m==='Not Voting'?'nv':'present');
+  tb.innerHTML=rows.map(r=>`<tr><td>${r.y}</td><td>${r.r}</td><td>${r.d}</td><td>${r.ln||''}</td><td>${r.q||''}</td><td>${r.ds||''}</td><td>${r.rs}</td><td class="${mClass(r.m)}">${r.m||''}</td><td>${r.ry}</td><td>${r.rn}</td><td>${r.dy}</td><td>${r.dn}</td><td><span class="${aClass(r.a)}">${r.a}</span></td><td>${r.b?`<span class="badge ${r.b==='Democrat'?'b-dem':'b-rep'}">${r.b}</span>`:''}</td></tr>`).join('');
+  document.getElementById('row-count').textContent=rows.length;
+}
+document.querySelectorAll('#voteTable th').forEach(th=>{th.addEventListener('click',()=>{const k=th.dataset.k;if(sortKey===k)sortDesc=!sortDesc;else{sortKey=k;sortDesc=true;}renderTable();});});
+['search','alignFilter','blockFilter','memberFilter'].forEach(id=>document.getElementById(id).addEventListener('input',renderTable));
+renderTable();
+</script>
+</body></html>
+"""
+
+def render(bioguide, name, party, out_name, note=None):
+    votes = gather(bioguide, party)
+    if not votes:
+        print(f"  WARN: no votes parsed for {name}", file=sys.stderr)
+    data = aggregate(votes, party)
+    def pct(n, d): return f"{(n/d*100):.1f}" if d else "0.0"
+    partisan_r = data["voted_with_gop"] + data["voted_against_gop"]
+    partisan_d = data["voted_with_dem"] + data["voted_against_dem"]
+    color = "#d93b3b" if party == "R" else ("#3b6ed9" if party == "D" else "#888")
+
+    note_html = f'<div class="member-note"><strong>Note:</strong> {note}</div>' if note else ''
+    html = (HTML_TEMPLATE
+        .replace("__MEMBER_NOTE__", note_html)
+        .replace("__NAME__", name)
+        .replace("__BIOGUIDE__", bioguide)
+        .replace("__PARTY__", party)
+        .replace("__PARTY_COLOR__", color)
+        .replace("__TOTAL__", str(data["total"]))
+        .replace("__VOTING__", str(data["voting"]))
+        .replace("__VOTING_PCT__", pct(data["voting"], data["total"]))
+        .replace("__YEAS__", str(data["yeas"]))
+        .replace("__NAYS__", str(data["nays"]))
+        .replace("__NV__", str(data["nv"]))
+        .replace("__PRESENT__", str(data["present"]))
+        .replace("__VA_GOP__", str(data["voted_against_gop"]))
+        .replace("__VA_GOP_PCT__", pct(data["voted_against_gop"], partisan_r))
+        .replace("__VA_DEM__", str(data["voted_against_dem"]))
+        .replace("__VA_DEM_PCT__", pct(data["voted_against_dem"], partisan_d))
+        .replace("__PARTISAN_R__", str(partisan_r))
+        .replace("__PARTISAN_D__", str(partisan_d))
+        .replace("__BLK_DEM__", str(data["blocked_dem_count"]))
+        .replace("__BLK_DEM_PCT__", pct(data["blocked_dem_count"], data["total"]))
+        .replace("__BLK_REP__", str(data["blocked_rep_count"]))
+        .replace("__BLK_REP_PCT__", pct(data["blocked_rep_count"], data["total"]))
+        .replace("__LONE__", str(data["lone_wolf"]))
+        .replace("__LONE_PCT__", pct(data["lone_wolf"], data["voting"]))
+        .replace("__DATA__", json.dumps(data))
+    )
+    out = os.path.join(OUT_DIR, out_name)
+    with open(out, "w") as f: f.write(html)
+    print(f"  {name}: {data['voting']}/{data['total']} voted, "
+          f"alignment R:{data['alignment'].get('Helped Republicans',0)} "
+          f"D:{data['alignment'].get('Helped Democrats',0)} "
+          f"Both:{data['alignment'].get('Helped Both',0)} "
+          f"Neither:{data['alignment'].get('Helped Neither',0)} → {out}")
+
+if __name__ == "__main__":
+    os.makedirs(OUT_DIR, exist_ok=True)
+    if len(sys.argv) >= 5:
+        render(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])
+    else:
+        # Roster entries: (bioguide, name, party, output_filename, optional_note)
+        # See DOCUMENTATION.md §3.1 for when to add a note.
+        MTG_NOTE = ("Rep. Greene publicly announced her resignation from the House "
+                    "in late 2025, effective January 5, 2026. This explains her "
+                    "substantially lower participation count (325 of 553) versus "
+                    "other members analyzed. Votes after her departure date are "
+                    "necessarily recorded as Not Voting in clerk.house.gov data.")
+        ROSTER = [
+            ("K000389", "Ro Khanna",                  "D", "RoKhanna119.html"),
+            ("O000172", "Alexandria Ocasio-Cortez",   "D", "AlexandriaOcasioCortez119.html"),
+            ("O000173", "Ilhan Omar",                 "D", "IlhanOmar119.html"),
+            ("G000596", "Marjorie Taylor Greene",     "R", "MarjorieTaylorGreene119.html", MTG_NOTE),
+            ("J000289", "Jim Jordan",                 "R", "JimJordan119.html"),
+            ("D000032", "Byron Donalds",              "R", "ByronDonalds119.html"),
+        ]
+        for entry in ROSTER:
+            bg, nm, pt, fn = entry[:4]
+            note = entry[4] if len(entry) > 4 else None
+            render(bg, nm, pt, fn, note)

+ 433 - 0
legacy/build_senator.py

@@ -0,0 +1,433 @@
+#!/usr/bin/env python3
+"""Parameterized Senate dashboard builder. Parses cached senate.gov XMLs
+for a given senator (LIS ID) and emits a self-contained HTML dashboard.
+
+Usage:
+  python3 build_senator.py <lis_id> "<Full Name>" <party R|D|I> <output_filename>
+  python3 build_senator.py                                  # builds the default ROSTER
+"""
+import sys, os, json, glob, re
+import xml.etree.ElementTree as ET
+from collections import Counter, defaultdict
+
+CACHE_DIR = "/home/user/polisci/senate_vote_cache"
+OUT_DIR   = "/home/user/polisci/results"
+MONTHS_FULL = ["January","February","March","April","May","June",
+               "July","August","September","October","November","December"]
+
+# Independents who caucus with Democrats — treat as D for party-majority math.
+DEM_CAUCUSING_INDEPENDENTS = {"S313"}  # Sanders (VT); add King (S354) etc as needed.
+
+def parse_date(s):
+    """'January 9, 2025,  02:54 PM' -> ('2025-01', '2025-01-09')."""
+    if not s: return None, None
+    m = re.match(r"\s*(\w+)\s+(\d+),\s+(\d+)", s)
+    if not m: return None, None
+    mon, day, yr = m.groups()
+    try:
+        mi = MONTHS_FULL.index(mon) + 1
+        return f"{yr}-{mi:02d}", f"{yr}-{mi:02d}-{int(day):02d}"
+    except ValueError:
+        return None, None
+
+def parse_xml(path, session, vnum, lis_id):
+    try:
+        with open(path, "rb") as f:
+            root = ET.fromstring(f.read())
+    except Exception:
+        return None
+    def t(tag):
+        el = root.find(tag)
+        return (el.text or "").strip() if el is not None and el.text else ""
+    # Build party totals from members
+    R = {"yea":0,"nay":0,"present":0,"nv":0}
+    D = {"yea":0,"nay":0,"present":0,"nv":0}
+    member_vote = None
+    for mem in root.iter("member"):
+        party = (mem.findtext("party","") or "").strip()
+        vc    = (mem.findtext("vote_cast","") or "").strip()
+        lid   = (mem.findtext("lis_member_id","") or "").strip()
+        if lid == lis_id:
+            member_vote = vc
+        bucket = ("yea" if vc == "Yea"
+                  else "nay" if vc == "Nay"
+                  else "present" if vc == "Present"
+                  else "nv")
+        if party == "R":
+            R[bucket] += 1
+        elif party == "D" or lid in DEM_CAUCUSING_INDEPENDENTS:
+            D[bucket] += 1
+        # other independents (none currently caucus with R) ignored for party math
+    # Compose bill id
+    doc_type = root.findtext("document/document_type","") or ""
+    doc_num  = root.findtext("document/document_number","") or ""
+    legis = (doc_type + " " + doc_num).strip() if (doc_type or doc_num) else (root.findtext("vote_title","") or "")
+    date_raw = t("vote_date")
+    return {
+        "session": session, "vnum": vnum,
+        "date_raw": date_raw,
+        "month_key": parse_date(date_raw)[0],
+        "date_iso":  parse_date(date_raw)[1],
+        "question":  t("question") or t("vote_question_text"),
+        "result":    t("vote_result"),
+        "legis_num": legis,
+        "desc":      t("vote_title") or t("vote_document_text"),
+        "R": R, "D": D,
+        "member": member_vote,
+    }
+
+def classify(v):
+    r_yea, r_nay = v["R"]["yea"], v["R"]["nay"]
+    d_yea, d_nay = v["D"]["yea"], v["D"]["nay"]
+    r_pos = "Yea" if r_yea > r_nay else ("Nay" if r_nay > r_yea else "Split")
+    d_pos = "Yea" if d_yea > d_nay else ("Nay" if d_nay > d_yea else "Split")
+    m = v["member"]
+    if m not in ("Yea","Nay"):
+        return ("N/A: " + (m or "absent"), None, r_pos, d_pos)
+    helped_r = (r_pos != "Split" and m == r_pos)
+    helped_d = (d_pos != "Split" and m == d_pos)
+    if helped_r and helped_d: align = "Helped Both"
+    elif helped_r: align = "Helped Republicans"
+    elif helped_d: align = "Helped Democrats"
+    else: align = "Helped Neither"
+    result = v["result"].lower()
+    # Senate results often say "agreed to" / "rejected" / "passed" / "failed" / "not agreed to"
+    measure_failed = ("rejected" in result or "not agreed" in result
+                      or "failed" in result or "not passed" in result
+                      or "not invoked" in result)
+    blocked = None
+    if d_pos == "Yea" and measure_failed and m == "Nay" and r_pos != "Yea":
+        blocked = "Democrat"
+    if r_pos == "Yea" and measure_failed and m == "Nay" and d_pos != "Yea":
+        blocked = "Republican"
+    return (align, blocked, r_pos, d_pos)
+
+def gather(lis_id):
+    votes = []
+    for path in sorted(glob.glob(f"{CACHE_DIR}/*.xml")):
+        base = os.path.basename(path).replace(".xml","")
+        try:
+            session, vnum = base.split("_")
+            session, vnum = int(session), int(vnum)
+        except Exception:
+            continue
+        v = parse_xml(path, session, vnum, lis_id)
+        if not v: continue
+        align, blocked, r_pos, d_pos = classify(v)
+        v["alignment"] = align
+        v["blocked"]   = blocked
+        v["r_pos"]     = r_pos
+        v["d_pos"]     = d_pos
+        votes.append(v)
+    return votes
+
+def aggregate(votes, member_party):
+    align_counts = Counter(v["alignment"] for v in votes)
+    member_counts = Counter(v["member"] for v in votes)
+    blocked_counts = Counter(v["blocked"] for v in votes if v["blocked"])
+    month_align = defaultdict(Counter)
+    for v in votes:
+        if v["month_key"]:
+            month_align[v["month_key"]][v["alignment"]] += 1
+    months_sorted = sorted(month_align.keys())
+    align_labels = ["Helped Republicans","Helped Democrats","Helped Both","Helped Neither"]
+    monthly = {l: [month_align[m].get(l,0) for m in months_sorted] for l in align_labels}
+
+    def row(v):
+        return {"y": (v["date_iso"] or "")[:4] or "?", "r": v["vnum"],
+                "d": v["date_raw"] or "", "ln": v["legis_num"],
+                "q": v["question"], "ds": (v["desc"] or "")[:90],
+                "rs": v["result"], "m": v["member"],
+                "ry": v["R"]["yea"], "rn": v["R"]["nay"],
+                "dy": v["D"]["yea"], "dn": v["D"]["nay"],
+                "a": v["alignment"], "b": v["blocked"] or ""}
+    rows = [row(v) for v in votes]
+
+    total = len(votes)
+    voting = sum(1 for v in votes if v["member"] in ("Yea","Nay"))
+    yeas = sum(1 for v in votes if v["member"] == "Yea")
+    nays = sum(1 for v in votes if v["member"] == "Nay")
+    nv = sum(1 for v in votes if v["member"] == "Not Voting")
+    present = sum(1 for v in votes if v["member"] == "Present")
+
+    va_gop = vw_gop = va_dem = vw_dem = 0
+    for v in votes:
+        if v["member"] not in ("Yea","Nay"): continue
+        r_pos = v["r_pos"]; d_pos = v["d_pos"]
+        if r_pos != "Split":
+            if v["member"] == r_pos: vw_gop += 1
+            else: va_gop += 1
+        if d_pos != "Split":
+            if v["member"] == d_pos: vw_dem += 1
+            else: va_dem += 1
+
+    own = member_party
+    lone_wolf = 0
+    for v in votes:
+        if v["member"] not in ("Yea","Nay"): continue
+        own_pos = v["r_pos"] if own == "R" else v["d_pos"]
+        if own_pos == "Split" or v["member"] == own_pos: continue
+        own_totals = v["R"] if own == "R" else v["D"]
+        defectors = own_totals["nay"] if own_pos == "Yea" else own_totals["yea"]
+        if defectors <= 3:  # Senate threshold tighter (smaller chamber)
+            lone_wolf += 1
+
+    return {
+        "total":total,"voting":voting,"yeas":yeas,"nays":nays,"nv":nv,"present":present,
+        "alignment":dict(align_counts),"member":dict(member_counts),
+        "blocked":dict(blocked_counts),"months":months_sorted,"monthly":monthly,
+        "rows":rows,
+        "blocked_dem_count":sum(1 for v in votes if v["blocked"] == "Democrat"),
+        "blocked_rep_count":sum(1 for v in votes if v["blocked"] == "Republican"),
+        "lone_wolf":lone_wolf,
+        "voted_against_gop":va_gop,"voted_against_dem":va_dem,
+        "voted_with_gop":vw_gop,"voted_with_dem":vw_dem,
+    }
+
+HTML_TEMPLATE = r"""<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>__NAME__ - 119th Congress Voting Dashboard</title>
+<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
+<style>
+  body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;margin:0;padding:24px;background:#f5f5f7;color:#1d1d1f;}
+  h1{margin:0 0 4px;font-size:28px;} .sub{color:#6e6e73;margin-bottom:24px;}
+  .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;margin-bottom:24px;}
+  .card{background:#fff;border-radius:12px;padding:20px;box-shadow:0 1px 3px rgba(0,0,0,.06);cursor:grab;transition:box-shadow .15s,transform .15s;}
+  .card:active{cursor:grabbing;} .card.sortable-ghost{opacity:.4;}
+  .card.sortable-chosen{box-shadow:0 4px 16px rgba(0,0,0,.15);transform:scale(1.02);}
+  .card.sortable-drag{box-shadow:0 8px 24px rgba(0,0,0,.2);}
+  .kpi{font-size:32px;font-weight:700;margin:6px 0 2px;} .pct{font-size:14px;font-weight:500;color:#6e6e73;margin-left:4px;}
+  .kpi-label{color:#6e6e73;font-size:13px;text-transform:uppercase;letter-spacing:.5px;}
+  .chart-row{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:24px;}
+  @media (max-width:900px){.chart-row{grid-template-columns:1fr;}}
+  .chart-card{background:#fff;border-radius:12px;padding:20px;box-shadow:0 1px 3px rgba(0,0,0,.06);min-width:0;}
+  .chart-box{position:relative;height:300px;width:100%;}
+  h2{font-size:18px;margin:0 0 12px;}
+  table{width:100%;border-collapse:collapse;font-size:13px;}
+  th,td{padding:6px 8px;text-align:left;border-bottom:1px solid #e8e8ed;}
+  th{background:#fafafa;position:sticky;top:0;cursor:pointer;user-select:none;}
+  th:hover{background:#f0f0f5;}
+  .table-wrap{max-height:600px;overflow:auto;border:1px solid #e8e8ed;border-radius:8px;}
+  .filter-bar{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;}
+  .filter-bar input,.filter-bar select{padding:8px 12px;border:1px solid #d2d2d7;border-radius:8px;font-size:14px;}
+  .filter-bar input{flex:1;min-width:200px;}
+  .badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;}
+  .b-rep{background:#fde2e2;color:#a30000;} .b-dem{background:#dde7fa;color:#1644a0;}
+  .b-both{background:#e0f0e0;color:#1e6b1e;} .b-neither{background:#f0e0f0;color:#6b1e6b;}
+  .b-na{background:#eee;color:#666;}
+  .v-yea{color:#1e6b1e;font-weight:600;} .v-nay{color:#a30000;font-weight:600;}
+  .v-nv,.v-present{color:#888;}
+  .note{font-size:12px;color:#6e6e73;line-height:1.5;margin-top:12px;}
+  .drag-hint{font-size:11px;color:#6e6e73;margin-bottom:8px;}
+  .drag-hint button{margin-left:8px;padding:2px 8px;font-size:11px;border:1px solid #d2d2d7;border-radius:4px;background:#fff;cursor:pointer;}
+  .party-pill{display:inline-block;padding:2px 8px;border-radius:10px;font-size:12px;font-weight:600;margin-left:8px;color:#fff;}
+  .chamber-pill{display:inline-block;padding:2px 8px;border-radius:10px;font-size:12px;font-weight:600;margin-left:6px;background:#5a3d8a;color:#fff;}
+  .footer{text-align:center;color:#6e6e73;font-size:12px;margin-top:32px;}
+  .member-note{background:#fff8e1;border-left:4px solid #f5b400;padding:12px 16px;border-radius:6px;margin:0 0 20px;font-size:14px;line-height:1.5;color:#5a4500;}
+  .member-note strong{color:#3d2f00;}
+</style>
+</head>
+<body>
+<h1>__NAME__<span class="party-pill" style="background:__PARTY_COLOR__">__PARTY__</span><span class="chamber-pill">SENATE</span></h1>
+<div class="sub">LIS __LIS__ · Senate roll-call votes, 119th Congress (Jan 2025 – present) · Source: senate.gov</div>
+__MEMBER_NOTE__
+
+<div class="drag-hint">↕ Drag any card to rearrange. <button id="resetOrder">Reset order</button></div>
+<div class="grid" id="cardGrid">
+  <div class="card" data-id="participation"><div class="kpi-label">Roll Calls / Participation</div><div class="kpi">__VOTING__ <span style="font-size:18px;color:#6e6e73">/ __TOTAL__</span> <span class="pct">__VOTING_PCT__%</span></div><div class="note">119th Congress · Yea: __YEAS__ · Nay: __NAYS__ · Not Voting: __NV__ · Present: __PRESENT__</div></div>
+  <div class="card" data-id="va_gop"><div class="kpi-label">Voted Against GOP Majority</div><div class="kpi" style="color:#a30000">__VA_GOP__ <span class="pct">__VA_GOP_PCT__%</span></div><div class="note">of __PARTISAN_R__ votes where R majority took a side</div></div>
+  <div class="card" data-id="va_dem"><div class="kpi-label">Voted Against Dem Majority</div><div class="kpi" style="color:#1644a0">__VA_DEM__ <span class="pct">__VA_DEM_PCT__%</span></div><div class="note">of __PARTISAN_D__ votes where D majority took a side</div></div>
+  <div class="card" data-id="blk_dem"><div class="kpi-label">Blocked Dem-Backed</div><div class="kpi" style="color:#1644a0">__BLK_DEM__ <span class="pct">__BLK_DEM_PCT__%</span></div><div class="note">Senator Nay + D wanted Yea + measure failed</div></div>
+  <div class="card" data-id="blk_rep"><div class="kpi-label">Blocked GOP-Backed</div><div class="kpi" style="color:#a30000">__BLK_REP__ <span class="pct">__BLK_REP_PCT__%</span></div><div class="note">Senator Nay + R wanted Yea + measure failed</div></div>
+  <div class="card" data-id="lone"><div class="kpi-label">Lone Wolf Defections</div><div class="kpi">__LONE__ <span class="pct">__LONE_PCT__%</span></div><div class="note">Defected from own (__PARTY__) party majority w/ ≤3 fellow defectors</div></div>
+</div>
+
+<div class="chart-row">
+  <div class="chart-card"><h2>Alignment Classification</h2><div class="chart-box"><canvas id="alignChart"></canvas></div></div>
+  <div class="chart-card"><h2>Vote Distribution</h2><div class="chart-box"><canvas id="voteChart"></canvas></div></div>
+</div>
+
+<div class="chart-row">
+  <div class="chart-card"><h2>Voted With vs. Against — by Party Majority</h2><div class="chart-box"><canvas id="partyChart"></canvas></div></div>
+  <div class="chart-card"><h2>Blocking Wins</h2><div class="chart-box"><canvas id="blockChart"></canvas></div></div>
+</div>
+
+<div class="chart-row">
+  <div class="chart-card" style="grid-column:1 / -1"><h2>Alignment Over Time (monthly)</h2><div class="chart-box" style="height:320px"><canvas id="trendChart"></canvas></div></div>
+</div>
+
+<div class="chart-row">
+  <div class="chart-card" style="grid-column:1 / -1">
+    <h2>All Votes (filterable)</h2>
+    <div class="filter-bar">
+      <input id="search" placeholder="Search bill/description…">
+      <select id="alignFilter"><option value="">All alignments</option><option>Helped Republicans</option><option>Helped Democrats</option><option>Helped Both</option><option>Helped Neither</option></select>
+      <select id="blockFilter"><option value="">All votes</option><option value="Democrat">Blocked Dem-backed</option><option value="Republican">Blocked GOP-backed</option></select>
+      <select id="memberFilter"><option value="">Any vote</option><option>Yea</option><option>Nay</option><option>Not Voting</option><option>Present</option></select>
+    </div>
+    <div class="table-wrap"><table id="voteTable">
+      <thead><tr><th data-k="y">Yr</th><th data-k="r">#</th><th data-k="d">Date</th><th data-k="ln">Bill</th><th data-k="q">Question</th><th data-k="ds">Description</th><th data-k="rs">Result</th><th data-k="m">Vote</th><th data-k="ry">R Yea</th><th data-k="rn">R Nay</th><th data-k="dy">D Yea</th><th data-k="dn">D Nay</th><th data-k="a">Alignment</th><th data-k="b">Blocked</th></tr></thead>
+      <tbody id="voteBody"></tbody></table></div>
+    <div class="note">Click headers to sort. Showing <span id="row-count">0</span> rows.</div>
+  </div>
+</div>
+<div class="footer">Source: senate.gov LIS XML rollcalls · Built 2026-05-24 · See DOCUMENTATION.md §12 for Senate methodology</div>
+
+<script>
+const DATA = __DATA__;
+const CARD_ORDER_KEY = 'dashCardOrder.__LIS__';
+const grid = document.getElementById('cardGrid');
+function applyOrder(order){if(!order)return;const byId={};Array.from(grid.children).forEach(c=>byId[c.dataset.id]=c);order.forEach(id=>{if(byId[id])grid.appendChild(byId[id]);});}
+try{applyOrder(JSON.parse(localStorage.getItem(CARD_ORDER_KEY)));}catch(e){}
+Sortable.create(grid,{animation:180,ghostClass:'sortable-ghost',chosenClass:'sortable-chosen',dragClass:'sortable-drag',
+  onEnd(){localStorage.setItem(CARD_ORDER_KEY,JSON.stringify(Array.from(grid.children).map(c=>c.dataset.id)));}});
+document.getElementById('resetOrder').addEventListener('click',()=>{localStorage.removeItem(CARD_ORDER_KEY);location.reload();});
+
+const ALIGN_COLORS={"Helped Republicans":"#d93b3b","Helped Democrats":"#3b6ed9","Helped Both":"#3ba85a","Helped Neither":"#a13bd9","N/A: Not Voting":"#999","N/A: Present":"#bbb"};
+const alignOrder=["Helped Republicans","Helped Democrats","Helped Both","Helped Neither","N/A: Not Voting","N/A: Present"];
+const alignLabels=alignOrder.filter(k=>DATA.alignment[k]);
+const alignData=alignLabels.map(k=>DATA.alignment[k]);
+const alignColors=alignLabels.map(k=>ALIGN_COLORS[k]||"#888");
+const alignTotal=alignData.reduce((a,b)=>a+b,0);
+const alignLabelsPct=alignLabels.map((l,i)=>`${l} — ${alignData[i]} (${(alignData[i]/alignTotal*100).toFixed(1)}%)`);
+new Chart(document.getElementById('alignChart'),{type:'doughnut',data:{labels:alignLabelsPct,datasets:[{data:alignData,backgroundColor:alignColors}]},
+  options:{plugins:{legend:{position:'right',labels:{boxWidth:14,font:{size:12}}},
+    tooltip:{callbacks:{label:ctx=>`${alignLabels[ctx.dataIndex]}: ${ctx.parsed} (${(ctx.parsed/alignTotal*100).toFixed(1)}%)`}}},
+    responsive:true,maintainAspectRatio:false}});
+
+const voteOrder=["Yea","Nay","Not Voting","Present"];
+const voteLabels=voteOrder.filter(k=>DATA.member[k]);
+const voteData=voteLabels.map(k=>DATA.member[k]);
+const voteTotal=voteData.reduce((a,b)=>a+b,0);
+const barLabelPlugin={id:'barLabel',afterDatasetsDraw(chart){const {ctx}=chart;
+  chart.data.datasets.forEach((ds,di)=>{chart.getDatasetMeta(di).data.forEach((bar,i)=>{
+    const v=ds.data[i],pct=(v/voteTotal*100).toFixed(1);
+    ctx.fillStyle='#333';ctx.font='600 11px -apple-system, sans-serif';ctx.textAlign='center';
+    ctx.fillText(`${v} (${pct}%)`,bar.x,bar.y-6);});});}};
+new Chart(document.getElementById('voteChart'),{type:'bar',
+  data:{labels:voteLabels,datasets:[{label:'Count',data:voteData,
+    backgroundColor:voteLabels.map(l=>l==='Yea'?'#1e8e3e':l==='Nay'?'#c5221f':'#888')}]},
+  options:{plugins:{legend:{display:false},tooltip:{callbacks:{label:ctx=>`${ctx.parsed.y} (${(ctx.parsed.y/voteTotal*100).toFixed(1)}%)`}}},
+    responsive:true,maintainAspectRatio:false,layout:{padding:{top:20}},scales:{y:{beginAtZero:true}}},
+  plugins:[barLabelPlugin]});
+
+const partyTotR=DATA.voted_with_gop+DATA.voted_against_gop;
+const partyTotD=DATA.voted_with_dem+DATA.voted_against_dem;
+new Chart(document.getElementById('partyChart'),{type:'bar',
+  data:{labels:['GOP majority','Dem majority'],datasets:[
+    {label:'Voted WITH',data:[DATA.voted_with_gop,DATA.voted_with_dem],backgroundColor:'#1e8e3e'},
+    {label:'Voted AGAINST',data:[DATA.voted_against_gop,DATA.voted_against_dem],backgroundColor:'#c5221f'}]},
+  options:{responsive:true,maintainAspectRatio:false,
+    plugins:{tooltip:{callbacks:{label:ctx=>{const tot=ctx.dataIndex===0?partyTotR:partyTotD;return `${ctx.dataset.label}: ${ctx.parsed.y} (${tot?(ctx.parsed.y/tot*100).toFixed(1):'0.0'}%)`;}}}},
+    scales:{x:{stacked:true},y:{stacked:true,beginAtZero:true}}},
+  plugins:[{id:'stackPct',afterDatasetsDraw(chart){const {ctx}=chart;
+    ctx.fillStyle='#fff';ctx.font='600 12px -apple-system, sans-serif';ctx.textAlign='center';
+    chart.data.datasets.forEach((ds,di)=>{chart.getDatasetMeta(di).data.forEach((bar,i)=>{
+      const tot=i===0?partyTotR:partyTotD,v=ds.data[i];
+      if(v>0&&tot)ctx.fillText(`${v} (${(v/tot*100).toFixed(1)}%)`,bar.x,(bar.y+bar.base)/2+4);});});}}]});
+
+const blkTot=DATA.blocked_dem_count+DATA.blocked_rep_count;
+new Chart(document.getElementById('blockChart'),{type:'bar',
+  data:{labels:['Blocked Dem-backed','Blocked GOP-backed'],
+    datasets:[{label:'Count',data:[DATA.blocked_dem_count,DATA.blocked_rep_count],backgroundColor:['#3b6ed9','#d93b3b']}]},
+  options:{indexAxis:'y',responsive:true,maintainAspectRatio:false,
+    plugins:{legend:{display:false},tooltip:{callbacks:{label:ctx=>`${ctx.parsed.x} (${blkTot?(ctx.parsed.x/blkTot*100).toFixed(1):'0.0'}%)`}}},
+    scales:{x:{beginAtZero:true}}},
+  plugins:[{id:'hbarLabel',afterDatasetsDraw(chart){const {ctx}=chart;
+    ctx.fillStyle='#333';ctx.font='600 12px -apple-system, sans-serif';ctx.textAlign='left';ctx.textBaseline='middle';
+    chart.getDatasetMeta(0).data.forEach((bar,i)=>{const v=chart.data.datasets[0].data[i];
+      ctx.fillText(`  ${v} (${blkTot?(v/blkTot*100).toFixed(1):'0.0'}%)`,bar.x,bar.y);});}}]});
+
+const trendLabels=DATA.months;
+const trendSets=["Helped Republicans","Helped Democrats","Helped Both","Helped Neither"].map(k=>({
+  label:k,data:DATA.monthly[k],borderColor:ALIGN_COLORS[k],backgroundColor:ALIGN_COLORS[k]+'33',tension:.3,fill:false}));
+new Chart(document.getElementById('trendChart'),{type:'line',data:{labels:trendLabels,datasets:trendSets},
+  options:{responsive:true,maintainAspectRatio:false,
+    scales:{y:{beginAtZero:true,title:{display:true,text:'Votes per month'}}},interaction:{mode:'index',intersect:false}}});
+
+let sortKey='r',sortDesc=true;
+function renderTable(){
+  const q=document.getElementById('search').value.toLowerCase();
+  const af=document.getElementById('alignFilter').value;
+  const bf=document.getElementById('blockFilter').value;
+  const mf=document.getElementById('memberFilter').value;
+  let rows=DATA.rows.filter(r=>{
+    if(q&&!((r.ln+' '+r.ds+' '+r.q).toLowerCase().includes(q)))return false;
+    if(af&&r.a!==af)return false; if(bf&&r.b!==bf)return false; if(mf&&r.m!==mf)return false; return true;});
+  rows.sort((a,b)=>{const av=a[sortKey],bv=b[sortKey];let cmp;
+    if(typeof av==='number')cmp=av-bv;else cmp=String(av).localeCompare(String(bv));
+    if(cmp===0&&sortKey!=='r')cmp=(String(a.y).localeCompare(String(b.y)))||(a.r-b.r);return sortDesc?-cmp:cmp;});
+  const tb=document.getElementById('voteBody');
+  const aClass=a=>'badge '+({"Helped Republicans":"b-rep","Helped Democrats":"b-dem","Helped Both":"b-both","Helped Neither":"b-neither"}[a]||"b-na");
+  const mClass=m=>'v-'+(m==='Yea'?'yea':m==='Nay'?'nay':m==='Not Voting'?'nv':'present');
+  tb.innerHTML=rows.map(r=>`<tr><td>${r.y}</td><td>${r.r}</td><td>${r.d}</td><td>${r.ln||''}</td><td>${r.q||''}</td><td>${r.ds||''}</td><td>${r.rs}</td><td class="${mClass(r.m)}">${r.m||''}</td><td>${r.ry}</td><td>${r.rn}</td><td>${r.dy}</td><td>${r.dn}</td><td><span class="${aClass(r.a)}">${r.a}</span></td><td>${r.b?`<span class="badge ${r.b==='Democrat'?'b-dem':'b-rep'}">${r.b}</span>`:''}</td></tr>`).join('');
+  document.getElementById('row-count').textContent=rows.length;
+}
+document.querySelectorAll('#voteTable th').forEach(th=>{th.addEventListener('click',()=>{const k=th.dataset.k;if(sortKey===k)sortDesc=!sortDesc;else{sortKey=k;sortDesc=true;}renderTable();});});
+['search','alignFilter','blockFilter','memberFilter'].forEach(id=>document.getElementById(id).addEventListener('input',renderTable));
+renderTable();
+</script>
+</body></html>
+"""
+
+def render(lis_id, name, party, out_name, note=None):
+    votes = gather(lis_id)
+    if not votes:
+        print(f"  WARN: no votes parsed for {name}", file=sys.stderr)
+    data = aggregate(votes, party)
+    def pct(n, d): return f"{(n/d*100):.1f}" if d else "0.0"
+    partisan_r = data["voted_with_gop"] + data["voted_against_gop"]
+    partisan_d = data["voted_with_dem"] + data["voted_against_dem"]
+    color = "#d93b3b" if party == "R" else ("#3b6ed9" if party == "D" else "#888")
+    note_html = f'<div class="member-note"><strong>Note:</strong> {note}</div>' if note else ''
+
+    html = (HTML_TEMPLATE
+        .replace("__MEMBER_NOTE__", note_html)
+        .replace("__NAME__", name)
+        .replace("__LIS__", lis_id)
+        .replace("__PARTY__", party)
+        .replace("__PARTY_COLOR__", color)
+        .replace("__TOTAL__", str(data["total"]))
+        .replace("__VOTING__", str(data["voting"]))
+        .replace("__VOTING_PCT__", pct(data["voting"], data["total"]))
+        .replace("__YEAS__", str(data["yeas"]))
+        .replace("__NAYS__", str(data["nays"]))
+        .replace("__NV__", str(data["nv"]))
+        .replace("__PRESENT__", str(data["present"]))
+        .replace("__VA_GOP__", str(data["voted_against_gop"]))
+        .replace("__VA_GOP_PCT__", pct(data["voted_against_gop"], partisan_r))
+        .replace("__VA_DEM__", str(data["voted_against_dem"]))
+        .replace("__VA_DEM_PCT__", pct(data["voted_against_dem"], partisan_d))
+        .replace("__PARTISAN_R__", str(partisan_r))
+        .replace("__PARTISAN_D__", str(partisan_d))
+        .replace("__BLK_DEM__", str(data["blocked_dem_count"]))
+        .replace("__BLK_DEM_PCT__", pct(data["blocked_dem_count"], data["total"]))
+        .replace("__BLK_REP__", str(data["blocked_rep_count"]))
+        .replace("__BLK_REP_PCT__", pct(data["blocked_rep_count"], data["total"]))
+        .replace("__LONE__", str(data["lone_wolf"]))
+        .replace("__LONE_PCT__", pct(data["lone_wolf"], data["voting"]))
+        .replace("__DATA__", json.dumps(data))
+    )
+    os.makedirs(OUT_DIR, exist_ok=True)
+    out = os.path.join(OUT_DIR, out_name)
+    with open(out, "w") as f: f.write(html)
+    print(f"  {name}: {data['voting']}/{data['total']} voted, "
+          f"R-aligned:{data['alignment'].get('Helped Republicans',0)} "
+          f"D-aligned:{data['alignment'].get('Helped Democrats',0)} "
+          f"Both:{data['alignment'].get('Helped Both',0)} "
+          f"Neither:{data['alignment'].get('Helped Neither',0)} → {out}")
+
+if __name__ == "__main__":
+    if len(sys.argv) >= 5:
+        render(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])
+    else:
+        ROSTER = [
+            ("S293", "Lindsey Graham", "R", "LindseyGraham119.html"),
+        ]
+        for entry in ROSTER:
+            lis, nm, pt, fn = entry[:4]
+            note = entry[4] if len(entry) > 4 else None
+            render(lis, nm, pt, fn, note)

+ 38 - 0
legacy/fetch_senate.py

@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+"""Fetch all Senate roll-call votes from the 119th Congress (sessions 1 + 2)."""
+import urllib.request, os, time, sys, re
+
+SESSIONS = {1: 659, 2: 130}  # session: max vote_number (snapshot 2026-05-24)
+CACHE = "/home/user/polisci/senate_vote_cache"
+os.makedirs(CACHE, exist_ok=True)
+UA = "Mozilla/5.0 (research; polisci-analysis)"
+
+def fetch(session, n):
+    path = f"{CACHE}/{session}_{n:05d}.xml"
+    if os.path.exists(path) and os.path.getsize(path) > 200:
+        return True
+    url = (f"https://www.senate.gov/legislative/LIS/roll_call_votes/"
+           f"vote119{session}/vote_119_{session}_{n:05d}.xml")
+    req = urllib.request.Request(url, headers={"User-Agent": UA})
+    try:
+        with urllib.request.urlopen(req, timeout=30) as r:
+            data = r.read()
+        with open(path, "wb") as f: f.write(data)
+        time.sleep(0.35)
+        return True
+    except Exception as e:
+        print(f"FAIL {session}/{n}: {e}", file=sys.stderr)
+        return False
+
+def main():
+    total = sum(SESSIONS.values()); done = 0
+    for s, m in SESSIONS.items():
+        for n in range(1, m+1):
+            fetch(s, n)
+            done += 1
+            if done % 50 == 0:
+                print(f"  {done}/{total}", file=sys.stderr)
+    print(f"Done: {sum(1 for f in os.listdir(CACHE) if f.endswith('.xml'))} files cached")
+
+if __name__ == "__main__":
+    main()

+ 145 - 0
legacy/fetch_votes.py

@@ -0,0 +1,145 @@
+#!/usr/bin/env python3
+"""Fetch all House roll call votes from 119th Congress and analyze Massie's record."""
+import urllib.request
+import xml.etree.ElementTree as ET
+import json
+import time
+import os
+import sys
+
+MASSIE_ID = "M001184"
+YEARS = {2025: 362, 2026: 191}
+CACHE_DIR = "/home/user/polisci/vote_cache"
+os.makedirs(CACHE_DIR, exist_ok=True)
+
+UA = "Mozilla/5.0 (research; polisci-analysis)"
+
+def fetch(year, roll):
+    path = f"{CACHE_DIR}/{year}_{roll:03d}.xml"
+    if os.path.exists(path) and os.path.getsize(path) > 200:
+        with open(path, "rb") as f:
+            return f.read()
+    url = f"https://clerk.house.gov/evs/{year}/roll{roll:03d}.xml"
+    req = urllib.request.Request(url, headers={"User-Agent": UA})
+    try:
+        with urllib.request.urlopen(req, timeout=30) as r:
+            data = r.read()
+        with open(path, "wb") as f:
+            f.write(data)
+        time.sleep(0.35)  # throttle
+        return data
+    except Exception as e:
+        print(f"FAIL {year}/{roll}: {e}", file=sys.stderr)
+        return None
+
+def parse(data, year, roll):
+    try:
+        root = ET.fromstring(data)
+    except Exception as e:
+        return None
+    meta = root.find("vote-metadata")
+    if meta is None:
+        return None
+    def t(tag):
+        el = meta.find(tag)
+        return (el.text or "").strip() if el is not None else ""
+    info = {
+        "year": year, "roll": roll,
+        "date": t("action-date"),
+        "question": t("vote-question"),
+        "result": t("vote-result"),
+        "legis_num": t("legis-num"),
+        "desc": t("vote-desc"),
+        "majority": t("majority"),
+    }
+    # party totals
+    party_totals = {}
+    for pt in meta.findall("vote-totals/totals-by-party"):
+        party = pt.findtext("party", "").strip()
+        party_totals[party] = {
+            "yea": int(pt.findtext("yea-total", "0") or 0),
+            "nay": int(pt.findtext("nay-total", "0") or 0),
+            "present": int(pt.findtext("present-total", "0") or 0),
+            "nv": int(pt.findtext("not-voting-total", "0") or 0),
+        }
+    info["R"] = party_totals.get("Republican", {"yea":0,"nay":0,"present":0,"nv":0})
+    info["D"] = party_totals.get("Democratic", {"yea":0,"nay":0,"present":0,"nv":0})
+    info["I"] = party_totals.get("Independent", {"yea":0,"nay":0,"present":0,"nv":0})
+    # Massie's vote
+    massie = None
+    for rv in root.iter("recorded-vote"):
+        leg = rv.find("legislator")
+        if leg is not None and leg.get("name-id") == MASSIE_ID:
+            v = rv.find("vote")
+            massie = (v.text or "").strip() if v is not None else None
+            break
+    info["massie"] = massie
+    return info
+
+def classify(v):
+    """Given parsed vote, return (alignment, blocked_side)."""
+    r_yea, r_nay = v["R"]["yea"], v["R"]["nay"]
+    d_yea, d_nay = v["D"]["yea"], v["D"]["nay"]
+    # Determine each party's majority position
+    r_pos = "Yea" if r_yea > r_nay else ("Nay" if r_nay > r_yea else "Split")
+    d_pos = "Yea" if d_yea > d_nay else ("Nay" if d_nay > d_yea else "Split")
+    m = v["massie"]
+    if m not in ("Yea", "Nay", "Aye", "No"):
+        return ("N/A: " + (m or "absent"), None, r_pos, d_pos)
+    # Normalize Aye/No to Yea/Nay
+    m_norm = "Yea" if m in ("Yea", "Aye") else "Nay"
+    helped_r = (r_pos != "Split" and m_norm == r_pos)
+    helped_d = (d_pos != "Split" and m_norm == d_pos)
+    if helped_r and helped_d:
+        align = "Helped Both"
+    elif helped_r:
+        align = "Helped Republicans"
+    elif helped_d:
+        align = "Helped Democrats"
+    else:
+        align = "Helped Neither"
+
+    # Blocking analysis: Massie voted against [side]'s majority position, AND that side lost the vote
+    result = v["result"].lower()
+    measure_passed = result in ("passed", "agreed to", "adopted")
+    measure_failed = "fail" in result or "reject" in result or "not agreed" in result or "not passed" in result
+
+    blocked = None
+    # "Dem-backed measure": D majority was Yea -> they wanted it to pass.
+    # If D wanted Yea, measure failed, and Massie voted Nay -> Massie helped block a Dem-backed measure.
+    if d_pos == "Yea" and measure_failed and m_norm == "Nay":
+        if r_pos != "Yea":  # only count if Rs didn't also back it -> actually a partisan block
+            blocked = "Democrat"
+    # Also if D wanted Nay (to defeat it) but it passed and Massie voted Yea... that's not blocking, that's helping pass
+    if r_pos == "Yea" and measure_failed and m_norm == "Nay":
+        if d_pos != "Yea":
+            blocked = "Republican"
+    return (align, blocked, r_pos, d_pos)
+
+def main():
+    all_votes = []
+    total = sum(YEARS.values())
+    done = 0
+    for year, max_roll in YEARS.items():
+        for roll in range(1, max_roll + 1):
+            data = fetch(year, roll)
+            done += 1
+            if done % 25 == 0:
+                print(f"  fetched {done}/{total}", file=sys.stderr)
+            if not data:
+                continue
+            v = parse(data, year, roll)
+            if not v:
+                continue
+            align, blocked, r_pos, d_pos = classify(v)
+            v["alignment"] = align
+            v["blocked"] = blocked
+            v["r_pos"] = r_pos
+            v["d_pos"] = d_pos
+            all_votes.append(v)
+    with open("/home/user/polisci/votes.json", "w") as f:
+        json.dump(all_votes, f)
+    print(f"Saved {len(all_votes)} votes", file=sys.stderr)
+
+if __name__ == "__main__":
+    main()

+ 106 - 0
legacy/index.html

@@ -0,0 +1,106 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>119th Congress Voting Dashboards — Index</title>
+<style>
+  body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
+       margin:0;padding:32px;background:#f5f5f7;color:#1d1d1f;max-width:1100px;margin-left:auto;margin-right:auto;}
+  h1{margin:0 0 6px;font-size:28px;}
+  .sub{color:#6e6e73;margin-bottom:28px;line-height:1.5;}
+  .sub a{color:#1644a0;}
+  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px;}
+  .card{background:#fff;border-radius:12px;padding:18px;box-shadow:0 1px 3px rgba(0,0,0,.06);
+        text-decoration:none;color:inherit;transition:transform .12s,box-shadow .12s;display:block;}
+  .card:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.1);}
+  .name{font-size:17px;font-weight:600;margin-bottom:8px;}
+  .pills{display:flex;align-items:center;gap:6px;margin-bottom:8px;flex-wrap:wrap;}
+  .pill{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;color:#fff;}
+  .loc{font-size:12px;color:#6e6e73;margin-left:4px;}
+  .file{font-size:11px;color:#9a9aa1;font-family:ui-monospace,SF Mono,Menlo,monospace;}
+  .footer{margin-top:32px;text-align:center;color:#6e6e73;font-size:12px;}
+</style>
+</head>
+<body>
+<h1>119th Congress — Voting Dashboards</h1>
+<div class="sub">
+  Per-member voting analysis for the 119th U.S. Congress (Jan 2025 – present).
+  Click any card to open its dashboard. See
+  <a href="../DOCUMENTATION.md">DOCUMENTATION.md</a> for methodology, data sources, and roster notes.
+</div>
+<div class="grid">
+    <a class="card" href="AlexandriaOcasioCortez119.html">
+      <div class="name">Alexandria Ocasio-Cortez</div>
+      <div class="pills">
+        <span class="pill" style="background:#3b6ed9">D</span>
+        <span class="pill" style="background:#1e6b1e">House</span>
+        <span class="loc">NY-14</span>
+      </div>
+      <div class="file">AlexandriaOcasioCortez119.html</div>
+    </a>
+    <a class="card" href="ByronDonalds119.html">
+      <div class="name">Byron Donalds</div>
+      <div class="pills">
+        <span class="pill" style="background:#d93b3b">R</span>
+        <span class="pill" style="background:#1e6b1e">House</span>
+        <span class="loc">FL-19</span>
+      </div>
+      <div class="file">ByronDonalds119.html</div>
+    </a>
+    <a class="card" href="IlhanOmar119.html">
+      <div class="name">Ilhan Omar</div>
+      <div class="pills">
+        <span class="pill" style="background:#3b6ed9">D</span>
+        <span class="pill" style="background:#1e6b1e">House</span>
+        <span class="loc">MN-5</span>
+      </div>
+      <div class="file">IlhanOmar119.html</div>
+    </a>
+    <a class="card" href="JimJordan119.html">
+      <div class="name">Jim Jordan</div>
+      <div class="pills">
+        <span class="pill" style="background:#d93b3b">R</span>
+        <span class="pill" style="background:#1e6b1e">House</span>
+        <span class="loc">OH-4</span>
+      </div>
+      <div class="file">JimJordan119.html</div>
+    </a>
+    <a class="card" href="LindseyGraham119.html">
+      <div class="name">Lindsey Graham</div>
+      <div class="pills">
+        <span class="pill" style="background:#d93b3b">R</span>
+        <span class="pill" style="background:#5a3d8a">Senate</span>
+        <span class="loc">SC</span>
+      </div>
+      <div class="file">LindseyGraham119.html</div>
+    </a>
+    <a class="card" href="MarjorieTaylorGreene119.html">
+      <div class="name">Marjorie Taylor Greene</div>
+      <div class="pills">
+        <span class="pill" style="background:#d93b3b">R</span>
+        <span class="pill" style="background:#1e6b1e">House</span>
+        <span class="loc">GA-14</span>
+      </div>
+      <div class="file">MarjorieTaylorGreene119.html</div>
+    </a>
+    <a class="card" href="RoKhanna119.html">
+      <div class="name">Ro Khanna</div>
+      <div class="pills">
+        <span class="pill" style="background:#3b6ed9">D</span>
+        <span class="pill" style="background:#1e6b1e">House</span>
+        <span class="loc">CA-17</span>
+      </div>
+      <div class="file">RoKhanna119.html</div>
+    </a>
+    <a class="card" href="ThomasMassie119.html">
+      <div class="name">Thomas Massie</div>
+      <div class="pills">
+        <span class="pill" style="background:#d93b3b">R</span>
+        <span class="pill" style="background:#1e6b1e">House</span>
+        <span class="loc">KY-4</span>
+      </div>
+      <div class="file">ThomasMassie119.html</div>
+    </a>
+</div>
+<div class="footer">8 dashboards · Sources: clerk.house.gov (House) · senate.gov LIS (Senate)</div>
+</body></html>

ファイルの差分が大きいため隠しています
+ 0 - 0
legacy/votes.json


+ 322 - 0
parse.py

@@ -0,0 +1,322 @@
+#!/usr/bin/env python3
+"""Parse cached XML rollcalls into a unified, chamber-agnostic schema.
+
+Outputs per (congress, chamber):
+  data/<congress>/<chamber>/votes.jsonl  — one JSON object per vote, schema:
+      {
+        "chamber": "house"|"senate",
+        "year": 2025, "session": 1, "num": 47,
+        "date": "2025-01-09",
+        "bill": "H R 1234" | "S 5" | "",
+        "question": "On Passage",
+        "result": "Passed",
+        "desc": "Short title",
+        "totals": {"R":{"yea":0,"nay":0,"present":0,"nv":0}, "D":{...}, "I":{...}},
+        "votes": {"M001184": "Yea", "K000389": "Nay", ...}    # id -> raw vote text
+      }
+  data/<congress>/<chamber>/roster.json — {id: {name, party, state, chamber}}
+
+Run:
+  python3 parse.py --congress 119 --chamber {house|senate|both}
+"""
+import argparse, os, sys, json, glob, re
+import xml.etree.ElementTree as ET
+
+DATA_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
+MONTHS_3 = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
+MONTHS_FULL = ["January","February","March","April","May","June",
+               "July","August","September","October","November","December"]
+
+# Independents who caucus with Democrats — counted as D for party totals.
+DEM_CAUCUSING_INDEPENDENTS = {
+    # Bernie Sanders (S313 in Senate XML), Angus King (S354), etc.
+    "S313", "S354",
+}
+
+# Reject characters that should never appear in government XML free-text fields.
+# Presence indicates injection, encoding corruption, or upstream tampering.
+_BAD_TEXT_RE = re.compile(r"[<>\x00-\x08\x0b\x0c\x0e-\x1f]")
+
+def _safe_text(s, field, source):
+    """Return s unchanged if clean; raise ValueError with context if it contains
+    angle brackets or ASCII control chars (excluding \\t, \\n, \\r)."""
+    if s and _BAD_TEXT_RE.search(s):
+        raise ValueError(
+            f"Unsafe characters in upstream field '{field}' from {source}: {s!r}")
+    return s
+
+# ---------- House parsing ----------
+
+def _t(el, tag):
+    sub = el.find(tag)
+    return (sub.text or "").strip() if sub is not None and sub.text else ""
+
+def _ts(el, tag, source):
+    """Like _t but validates the extracted text via _safe_text."""
+    return _safe_text(_t(el, tag), tag, source)
+
+def parse_house_vote(path, year, roll):
+    try:
+        with open(path, "rb") as f:
+            root = ET.fromstring(f.read())
+    except Exception:
+        return None, {}
+    meta = root.find("vote-metadata")
+    if meta is None: return None, {}
+    # Date: "3-Jan-2025"
+    raw = _t(meta, "action-date")
+    iso = None
+    try:
+        d, mo, y = raw.split("-")
+        iso = f"{y}-{MONTHS_3.index(mo)+1:02d}-{int(d):02d}"
+    except Exception:
+        pass
+    totals = {"R":{"yea":0,"nay":0,"present":0,"nv":0},
+              "D":{"yea":0,"nay":0,"present":0,"nv":0},
+              "I":{"yea":0,"nay":0,"present":0,"nv":0}}
+    for pt in meta.findall("vote-totals/totals-by-party"):
+        party = (pt.findtext("party","") or "").strip()
+        key = ("R" if party=="Republican" else "D" if party=="Democratic" else "I")
+        totals[key] = {
+            "yea": int(pt.findtext("yea-total","0") or 0),
+            "nay": int(pt.findtext("nay-total","0") or 0),
+            "present": int(pt.findtext("present-total","0") or 0),
+            "nv": int(pt.findtext("not-voting-total","0") or 0),
+        }
+    votes = {}
+    roster_seen = {}
+    source = f"house:{year}/{roll}"
+    for rv in root.iter("recorded-vote"):
+        leg = rv.find("legislator")
+        if leg is None: continue
+        bid = leg.get("name-id")
+        if not bid: continue
+        vote_el = rv.find("vote")
+        vote = (vote_el.text or "").strip() if vote_el is not None else ""
+        votes[bid] = _safe_text(vote, "vote", source)
+        # Roster: store name + party + state (last write wins; fine for stable identities)
+        roster_seen[bid] = {
+            "name": _safe_text((leg.get("unaccented-name") or leg.text or "").strip(),
+                               "legislator-name", source),
+            "party": leg.get("party") or "",
+            "state": leg.get("state") or "",
+            "chamber": "house",
+        }
+    record = {
+        "chamber": "house",
+        "year": year, "session": None, "num": roll,
+        "date": iso, "date_raw": raw,
+        "bill": _ts(meta, "legis-num", source),
+        "question": _ts(meta, "vote-question", source),
+        "result": _ts(meta, "vote-result", source),
+        "desc": _ts(meta, "vote-desc", source),
+        "totals": totals,
+        "votes": votes,
+    }
+    return record, roster_seen
+
+# ---------- Senate parsing ----------
+
+def parse_senate_vote(path, session, vnum):
+    try:
+        with open(path, "rb") as f:
+            root = ET.fromstring(f.read())
+    except Exception:
+        return None, {}
+    raw = _t(root, "vote_date")
+    iso = None
+    m = re.match(r"\s*(\w+)\s+(\d+),\s+(\d+)", raw)
+    if m:
+        mon, day, yr = m.groups()
+        try:
+            mi = MONTHS_FULL.index(mon) + 1
+            iso = f"{yr}-{mi:02d}-{int(day):02d}"
+        except Exception:
+            pass
+    year_int = int(iso[:4]) if iso else None
+    totals = {"R":{"yea":0,"nay":0,"present":0,"nv":0},
+              "D":{"yea":0,"nay":0,"present":0,"nv":0},
+              "I":{"yea":0,"nay":0,"present":0,"nv":0}}
+    votes = {}
+    roster_seen = {}
+    source = f"senate:{session}/{vnum}"
+    for mem in root.iter("member"):
+        lid = (mem.findtext("lis_member_id","") or "").strip()
+        if not lid: continue
+        party = (mem.findtext("party","") or "").strip()
+        vc = (mem.findtext("vote_cast","") or "").strip()
+        votes[lid] = _safe_text(vc, "vote_cast", source)
+        name = ((mem.findtext("first_name","") or "").strip() + " " +
+                (mem.findtext("last_name","") or "").strip()).strip()
+        roster_seen[lid] = {
+            "name": _safe_text(name, "member-name", source),
+            "party": party,
+            "state": (mem.findtext("state","") or "").strip(),
+            "chamber": "senate",
+        }
+        bucket = ("yea" if vc=="Yea" else "nay" if vc=="Nay"
+                  else "present" if vc=="Present" else "nv")
+        # Count for party totals — Dem-caucusing Independents go into D.
+        if party == "R":
+            totals["R"][bucket] += 1
+        elif party == "D" or lid in DEM_CAUCUSING_INDEPENDENTS:
+            totals["D"][bucket] += 1
+        else:
+            totals["I"][bucket] += 1
+    # Compose bill identifier
+    dt = root.findtext("document/document_type","") or ""
+    dn = root.findtext("document/document_number","") or ""
+    bill = _safe_text((dt + " " + dn).strip(), "bill", source)
+    record = {
+        "chamber": "senate",
+        "year": year_int, "session": session, "num": vnum,
+        "date": iso, "date_raw": raw,
+        "bill": bill,
+        "question": _ts(root, "question", source) or _ts(root, "vote_question_text", source),
+        "result": _ts(root, "vote_result", source),
+        "desc": _ts(root, "vote_title", source) or _ts(root, "vote_document_text", source),
+        "totals": totals,
+        "votes": votes,
+    }
+    return record, roster_seen
+
+# ---------- Orchestration ----------
+
+def parse_chamber(congress, chamber):
+    cache = os.path.join(DATA_ROOT, str(congress), chamber, "cache")
+    out_votes = os.path.join(DATA_ROOT, str(congress), chamber, "votes.jsonl")
+    out_roster = os.path.join(DATA_ROOT, str(congress), chamber, "roster.json")
+    if not os.path.isdir(cache):
+        print(f"  no cache at {cache}", file=sys.stderr); return 0
+    roster = {}
+    n = 0
+    with open(out_votes, "w") as out:
+        for path in sorted(glob.glob(os.path.join(cache, "*.xml"))):
+            base = os.path.splitext(os.path.basename(path))[0]
+            try:
+                a, b = base.split("_")
+                a, b = int(a), int(b)
+            except Exception:
+                continue
+            if chamber == "house":
+                rec, seen = parse_house_vote(path, a, b)
+            else:
+                rec, seen = parse_senate_vote(path, a, b)
+            if not rec: continue
+            out.write(json.dumps(rec, separators=(",", ":")) + "\n")
+            n += 1
+            for mid, info in seen.items():
+                # Don't overwrite a roster entry with a less-complete one
+                if mid not in roster or not roster[mid].get("name"):
+                    roster[mid] = info
+                else:
+                    # Update party/state in case of a switch — keep latest
+                    if info.get("party"): roster[mid]["party"] = info["party"]
+                    if info.get("state"): roster[mid]["state"] = info["state"]
+    # Merge with members_directory.json if available (Phase 0.5 enrichment).
+    vote_count = len(roster)
+    directory_only = 0
+    directory_path = os.path.join(DATA_ROOT, str(congress), "members_directory.json")
+    lis_xwalk_path = os.path.join(DATA_ROOT, str(congress), "lis_to_bioguide.json")
+    directory_present = os.path.isfile(directory_path)
+    if directory_present:
+        try:
+            with open(directory_path) as f:
+                directory = json.load(f)
+        except Exception as e:
+            print(f"  [{chamber}] WARNING: could not load {directory_path}: {e}",
+                  file=sys.stderr)
+            directory = {}
+        # For senate, map bioguide -> lis via reverse of lis_to_bioguide.json.
+        bioguide_to_lis = {}
+        if chamber == "senate" and os.path.isfile(lis_xwalk_path):
+            try:
+                with open(lis_xwalk_path) as f:
+                    lis_map = json.load(f)
+                bioguide_to_lis = {b: l for l, b in lis_map.items()}
+            except Exception as e:
+                print(f"  [{chamber}] WARNING: could not load {lis_xwalk_path}: {e}",
+                      file=sys.stderr)
+        for bioguide, entry in directory.items():
+            ec = (entry.get("chamber") or "").lower()
+            entry_chamber = "senate" if "senate" in ec else ("house" if "house" in ec else ec)
+            if entry_chamber != chamber:
+                continue
+            # Roster key: bioguide for house, LIS for senate.
+            if chamber == "house":
+                key = bioguide
+            else:
+                key = bioguide_to_lis.get(bioguide)
+                if not key:
+                    # Without LIS we cannot map to the per-chamber roster key;
+                    # still add under bioguide so the directory entry isn't lost.
+                    key = bioguide
+            enrichment = {
+                "full_name": entry.get("full_name"),
+                "district": entry.get("district"),
+                "served_from": entry.get("served_from"),
+                "served_to": entry.get("served_to"),
+                "photo_url": entry.get("photo_url"),
+                "bioguide": bioguide,
+                "lis": entry.get("lis"),
+            }
+            if key in roster:
+                roster[key].update({k: v for k, v in enrichment.items() if v is not None})
+                roster[key]["served_partial"] = False
+            else:
+                roster[key] = {
+                    "name": entry.get("full_name") or "",
+                    "party": entry.get("party") or "",
+                    "state": entry.get("state") or "",
+                    "chamber": chamber,
+                    "served_partial": True,
+                    **{k: v for k, v in enrichment.items() if v is not None},
+                }
+                directory_only += 1
+        total = len(roster)
+        print(f"  [{chamber}] merged: {vote_count} vote-derived + "
+              f"{directory_only} directory-only = {total} total roster entries",
+              file=sys.stderr)
+    else:
+        print(f"  [{chamber}] merged: {vote_count} vote-derived + 0 directory-only "
+              f"= {vote_count} total roster entries (no members_directory.json)",
+              file=sys.stderr)
+    with open(out_roster, "w") as f:
+        json.dump(roster, f, indent=2, sort_keys=True)
+    print(f"  [{chamber}] parsed {n} votes, {len(roster)} roster entries → {out_votes}", file=sys.stderr)
+    return n
+
+def main():
+    ap = argparse.ArgumentParser()
+    ap.add_argument("--congress", type=int, required=True)
+    ap.add_argument("--chamber", choices=["house","senate","both"], default="both")
+    args = ap.parse_args()
+    if args.chamber in ("house","both"): parse_chamber(args.congress, "house")
+    if args.chamber in ("senate","both"): parse_chamber(args.congress, "senate")
+
+    # Validation gate: total combined unique-member count across chambers.
+    directory_path = os.path.join(DATA_ROOT, str(args.congress), "members_directory.json")
+    if not os.path.isfile(directory_path):
+        print(f"WARNING: {directory_path} missing — roster may be incomplete "
+              f"(Phase 0.5 enrichment was skipped). Run enrich_roster.py.",
+              file=sys.stderr)
+    if args.chamber == "both":
+        seen = set()
+        for ch in ("house", "senate"):
+            rp = os.path.join(DATA_ROOT, str(args.congress), ch, "roster.json")
+            if not os.path.isfile(rp): continue
+            try:
+                with open(rp) as f:
+                    r = json.load(f)
+                for k, v in r.items():
+                    bid = v.get("bioguide") or k
+                    seen.add(bid)
+            except Exception:
+                pass
+        if len(seen) < 535:
+            print(f"WARNING: combined roster has only {len(seen)} unique members "
+                  f"(<535 expected). Congress.gov API may be down or "
+                  f"enrich_roster.py was not run.", file=sys.stderr)
+
+if __name__ == "__main__":
+    main()

+ 47 - 0
research/PHASE3_PARITY.md

@@ -0,0 +1,47 @@
+# Phase 3 — KPI Parity Gate Report
+
+Compared legacy/*.html DATA blocks vs data/119/members/<id>.json metrics.
+
+## Per-member results
+
+M001184 Thomas Massie — PASS  (all 12+1 scalar KPIs + alignment4 + blocked + monthly + rows)
+
+K000389 Ro Khanna — PASS  (all 12+1 scalar KPIs + alignment4 + blocked + monthly + rows)
+
+O000172 Alexandria Ocasio-Cortez — PASS  (all 12+1 scalar KPIs + alignment4 + blocked + monthly + rows)
+
+O000173 Ilhan Omar — PASS  (all 12+1 scalar KPIs + alignment4 + blocked + monthly + rows)
+
+G000596 Marjorie Taylor Greene — PASS  (all 12+1 scalar KPIs + alignment4 + blocked + monthly + rows)
+
+J000289 Jim Jordan — PASS  (all 12+1 scalar KPIs + alignment4 + blocked + monthly + rows)
+
+D000032 Byron Donalds — PASS  (all 12+1 scalar KPIs + alignment4 + blocked + monthly + rows)
+
+S293 Lindsey Graham — PASS  (all 12+1 scalar KPIs + alignment4 + blocked + monthly + rows)
+
+## Summary
+
+Phase 3 gate: 8/8 members PASS
+
+## MTG (G000596) banner check
+
+- served_partial: False
+- metrics.total: 553
+- served_from: 2021-01-03, served_to: 2026-01-03
+- No banner needed (full term, has votes).
+
+## Deep-link URL behavior (template/app.js)
+
+- pushState present: True
+- popstate present: True
+
+## CDN traffic check (template/)
+
+- PASS — no runtime CDN references (vendored-file documentation comments ignored).
+  Note: documentation/comment-only mentions inside template/vendor/ were ignored:
+    /home/user/polisci/template/vendor/chart.umd.min.js: * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
+
+## Decision
+
+**GATE PASSED — safe to proceed to Phase 4.**

+ 23 - 0
research/PHASE6_CSS_AUDIT.md

@@ -0,0 +1,23 @@
+# Phase 6 CSS Namespace Audit
+
+**File audited:** `template/app.css` (399 lines, 60 top-level rule blocks, 68 effective rules counting nested `@media`).
+
+**Audit method:** Python brace-scanner that walks every top-level rule and every rule nested inside `@media`/`@supports`. Each selector is split on commas; every comma-part must either contain `#polisci-root` or be in the allowed bare set (`html`, `body`, `*`, `:root`). At-rules (`@media`, `@keyframes`, `@font-face`, `@supports`, `@import`, `@charset`, `@page`) are allowed at the top level.
+
+## Result
+
+| Metric | Count |
+|---|---|
+| Top-level rule blocks | 60 |
+| Scoped under `#polisci-root` | 68 |
+| Allowed at-rules (`@media`) | 1 |
+| **Violations (unscoped)** | **0** |
+
+No fixes required. The stylesheet is fully namespaced under `#polisci-root`, including the comparison styles appended by Phase 5 and the responsive `@media (max-width: 768px)` block whose inner rules are all prefixed.
+
+## Embed contract status
+
+- `template/app.css` — passes (0 unscoped rules)
+- `template/app.js` line 37 — `state.BASE = root.dataset.base || './data/'` honors host-provided base path
+- `template/compare.js` line 30 — same pattern honored
+- `results/119/_embed_test.html` — smoke-test host page written for manual visual inspection

+ 70 - 0
research/PHASE6_EMBED_TEST.md

@@ -0,0 +1,70 @@
+# Phase 6 — Embedding & Cross-Browser Smoke Test
+
+_Date: 2026-05-24_
+
+## Iframe test files
+
+Created two host pages that load the built artifacts inside a sandboxed iframe:
+
+- `/home/user/polisci/results/119/_iframe_test.html` — loads `app.html`
+- `/home/user/polisci/results/119/_iframe_compare_test.html` — loads `compare.html?ids=M001184,K000389,O000172`
+
+Both use:
+
+```
+sandbox="allow-scripts allow-same-origin"
+referrerpolicy="no-referrer"
+```
+
+`allow-same-origin` is required because the app uses `fetch()` to load sibling JSON files via relative paths — without same-origin, the iframe would have an opaque origin and same-origin fetches would fail. `allow-scripts` is required to run the dashboard JS. No `allow-forms`, `allow-popups`, `allow-top-navigation`, etc.
+
+## HTTP smoke results
+
+Served `results/119/` via `python3 -m http.server 8765`. All endpoints responded as expected:
+
+| Endpoint | Result |
+|---|---|
+| `app.html` | 200 OK |
+| `compare.html` | 200 OK |
+| `app.js` | 200 OK |
+| `compare.js` | 200 OK |
+| `app.css` | 200 OK |
+| `vendor/chart.umd.min.js` | 200 OK |
+| `vendor/sortable.min.js` | 200 OK |
+| `data/manifest.json` | parsed: 552 members, version=1.0.0+2026-05-24 |
+| `data/members/M001184.json` | parsed: Massie total=553 |
+| `data/members/INVALID.json` | 404 (expected) |
+
+Server cleanly shut down after the smoke run; port 8765 no longer listening.
+
+## Headless browser smoke (if any)
+
+No headless browser available on this host. `chromium`, `chromium-browser`, `google-chrome`, and `firefox` are all absent from `PATH`. **Manual cross-browser smoke deferred to the user.**
+
+Recommended manual checks for the user:
+1. Open `results/119/_iframe_test.html` in Chromium/Firefox/Safari from a local web server. Confirm dashboard renders inside the iframe and DevTools Network panel shows zero requests outside the directory.
+2. Same for `_iframe_compare_test.html` (three members preloaded).
+3. Verify console is clean (no CSP / mixed-content errors).
+
+## External-URL grep audit
+
+Audited `template/` and `results/119/` for `https?://` URLs and inspected every runtime-code occurrence.
+
+**Runtime network calls in built JS** (`app.js`, `compare.js`):
+
+```
+compare.js:46    fetch(state.BASE + 'manifest.json', { credentials: 'omit' })
+compare.js:251   fetch(url, { credentials: 'omit' })
+compare.js:635   fetch(url, { credentials: 'omit' })
+compare.js:671   fetch(url, { credentials: 'omit' })
+app.js:56        fetch(url, { credentials: 'omit' })
+app.js:243       fetch(url, { credentials: 'omit' })
+```
+
+All `url` values are constructed from `state.BASE + relative_path`, where `BASE` is derived from the document's own location. **Zero absolute external URLs** in `app.js`, `compare.js`, `app.html`, `compare.html`, or `app.css`.
+
+The `https://*.senate.gov` / `http://history.house.gov` URLs found in the grep are **member website / photo URLs inside `data/members/*.json`** — these are rendered as `<a href=…>` links for the user to click, never auto-fetched. The `Image.src` / `<img src>` audit returned no hits assigning external URLs.
+
+The remaining `https://` strings in `vendor/chart.umd.min.js` and `vendor/sortable.min.js` are inside license / banner comments and do not trigger network activity.
+
+**Conclusion: no runtime external network calls.** Embedding contract satisfied.

+ 384 - 0
research/PLAN.md

@@ -0,0 +1,384 @@
+# 119th Congress Voting Dashboard — Interactive SPA Rewrite (FINAL v1)
+
+## Context
+
+The user has iteratively built a working voting-dashboard pipeline in this session: `fetch.py`, `parse.py`, and `analyze.py` are written and produce a unified schema (1,342 cached XML rollcalls — 553 House + 789 Senate) for the 119th Congress. Eight standalone dashboard HTML files were previously generated (Massie, Khanna, AOC, Omar, MTG, Jordan, Donalds, Graham) and now live in `legacy/`.
+
+The user now wants:
+
+1. A dashboard for **every member** of the 119th Congress (~535 House + Senate).
+2. A **single interactive web page** with a member picker that re-renders without a page reload.
+3. A **comparison page** for overlaying multiple representatives on shared charts.
+4. **Framework-free** so it can be embedded into a third-party site (no React/Vue/Next).
+5. **Static-file data hosting** — per-member JSON served alongside the page.
+6. Member selector: **searchable typeahead dropdown PLUS sidebar filters** (chamber, party, state).
+7. Structure must generalize to future Congresses via a `--congress N` CLI arg.
+8. Cheap re-analysis: change `analyze.py`, re-render all member JSON in seconds; no re-fetch.
+
+This plan incorporates feedback from PM + programmer (Consult) + security (Consult) + compliance (Consult) agents. Decisions locked by the user:
+- **Labels:** keep current "Helped Republicans / Helped Democrats / Blocked Dem-Backed / Blocked GOP-Backed" wording. Editorial-bias concern flagged by compliance will be captured in `NOTES.md` (not on dashboards).
+- **Comparison page:** ship all 5 overlay charts in MVP. Programmer/PM concern about derivative charts will be captured in `NOTES.md`.
+- **Congress.gov API key:** redact from `DOCUMENTATION.md`, move to `.env` (gitignored) with a rotation note. The key has never been used by the pipeline.
+
+---
+
+## Target File Layout
+
+```
+polisci/
+├── DOCUMENTATION.md            # existing — UPDATE §2 (redact key), §8 (file layout), §9 (regeneration)
+├── NOTES.md                    # NEW — captures known concerns we chose not to fix in MVP
+├── PROJECT_SCOPE.md            # NEW — written by PM agent (only it may edit)
+├── .env                        # NEW — gitignored — holds CONGRESS_GOV_API_KEY
+├── .gitignore                  # NEW — at least: .env, __pycache__/, *.pyc, data/*/cache/
+├── fetch.py                    # existing — unchanged
+├── parse.py                    # existing — minor: validate upstream strings (reject <, >, NUL)
+├── analyze.py                  # existing — unchanged (output shape stable)
+├── enrich_roster.py            # NEW — Congress.gov API → data/<C>/members_directory.json (complete roster)
+├── build_members.py            # NEW — write data/<C>/members/<id>.json + manifest.json (parallelized)
+├── build_app.py                # NEW — copy template/ → results/<C>/; embed manifest version; copy data/
+├── build_all.py                # NEW — orchestrator: fetch → parse → build_members → build_app
+├── tests/                      # NEW — pytest unit tests for analyze.py with frozen fixtures
+│   ├── fixtures/
+│   │   ├── partisan_house.xml
+│   │   ├── bipartisan_house.xml
+│   │   ├── partisan_senate.xml
+│   │   └── failed_blocking_senate.xml
+│   └── test_analyze.py
+├── template/                   # NEW — input templates copied at build time
+│   ├── app.html                # single-member dashboard shell
+│   ├── compare.html            # comparison shell
+│   ├── app.js                  # shared frontend logic
+│   ├── app.css                 # shared styles, all selectors namespaced under #polisci-root
+│   └── vendor/                 # NEW — pinned local copies (no CDN)
+│       ├── chart.umd.min.js    # Chart.js 4.4.0
+│       └── sortable.min.js     # SortableJS 1.15.2
+├── data/119/
+│   ├── house/{cache/, votes.jsonl, roster.json}
+│   ├── senate/{cache/, votes.jsonl, roster.json}
+│   ├── members/<id>.json       # NEW — per-member metrics (~80 KB each)
+│   ├── manifest.json           # NEW — array of {id,n,p,s,c,district,served_partial} for picker
+│   ├── members_directory.json  # NEW — complete 119th roster from Congress.gov
+│   ├── lis_to_bioguide.json    # NEW — Senate ID crosswalk
+│   ├── api_cache/              # NEW — cached Congress.gov responses (idempotent)
+│   └── build_report.json       # NEW — per-build success/failure log
+├── results/119/                # output — entire dir is the embeddable artifact
+│   ├── app.html
+│   ├── compare.html
+│   ├── app.js
+│   ├── app.css
+│   ├── vendor/{chart…, sortable…}
+│   ├── data/
+│   │   ├── manifest.json
+│   │   └── members/<id>.json
+│   └── README.md               # NEW — embedding instructions + recommended CSP/sandbox snippet
+└── legacy/                     # existing — archived after Milestone 0 validation passes
+```
+
+---
+
+## Frontend Architecture
+
+### `app.html` — single-member view
+
+- URL: `app.html?id=M001184` (no `?c=` — Congress is implicit in the deploy path; programmer recommendation)
+- **Sidebar** (collapsible on mobile): Chamber checkboxes (House / Senate), Party checkboxes (R / D / I), State multi-select populated from manifest.
+- **Searchable typeahead**: case-insensitive substring + initials match ("AOC" → Ocasio-Cortez). Iterates `manifest` as an **array** (not object) for sort/filter speed. Filtered live by sidebar.
+- **On selection**: fetch `data/members/<id>.json?v={manifest.version}` (cache-busted on classifier changes), then **mutate existing Chart.js datasets in place** and call `chart.update('none')` — no teardown/rebuild per switch. All 5 charts created once at page init.
+- **State persistence**: URL via `history.replaceState` for filter typing, `history.pushState` only on member selection. localStorage as Could-Have (per PM), namespaced as `polisci:v119:lastMember`, validated against manifest on read.
+
+### `compare.html` — overlay view
+
+- URL: `compare.html?ids=M001184,K000389,O000172` (shareable; cap parse at 6 IDs; validate each against manifest)
+- Same sidebar + typeahead. Multi-select pills (color-coded by member-assigned color).
+- **All 5 overlay charts** per user decision:
+  1. Alignment-over-time (line, one per member; switcher for which alignment class)
+  2. Voted-against-own-party rate over time (line)
+  3. Side-by-side KPI grouped bar (% against GOP, % against Dem, Lone Wolf %, Participation %, Blocked counts)
+  4. Defection scatter (X: % against GOP, Y: % against Dem; one dot per member, party color)
+  5. Vote-distribution grouped bar (Yea/Nay/Present/Not Voting per member)
+- Pill click opens member's `app.html?id=<id>` in a new tab.
+
+### Embedding contract
+
+Three modes, all supported by the same artifact in `results/119/`:
+1. **Standalone**: open `app.html` directly.
+2. **Iframe**: `<iframe src="…/app.html" sandbox="allow-scripts allow-same-origin" referrerpolicy="no-referrer">` (snippet documented in `results/119/README.md`).
+3. **Inline**: copy `<div id="polisci-root" data-base="./data/">…</div>` + `<link>`/`<script>` tags into a host page. All CSS namespaced under `#polisci-root`; `data-base` attribute makes the data path host-configurable.
+
+No external requests after page load (Chart.js + SortableJS pinned locally). Recommended host CSP documented in `results/119/README.md`.
+
+### Security posture
+
+- **All upstream strings rendered via `textContent`**, never `innerHTML`. Bill links built via `createElement('a')` + `textContent` + validated href.
+- **`parse.py`** rejects any upstream string containing `<`, `>`, or control characters with a build-report warning (gov XML should never legitimately contain these).
+- **localStorage**: namespaced (`polisci:v119:*`); values regex-validated against the manifest allowlist on read.
+- **Query strings**: `id` matched against `^[A-Z]\d{6}$|^S\d{3}$` (House bioguide or Senate LIS) AND verified to be a manifest key before any fetch or DOM use; `ids` capped at 6.
+- **No `postMessage` API in v1** — frame-boundary attack surface stays closed.
+- **No CDN, no SRI question** — vendored locally.
+
+---
+
+## Build Pipeline
+
+```bash
+python3 fetch.py          --congress 119                   # idempotent network fetch (clerk.house.gov + senate.gov)
+python3 parse.py          --congress 119                   # XML → votes.jsonl + roster.json
+python3 enrich_roster.py  --congress 119                   # Congress.gov API → complete roster + LIS crosswalk (NEW)
+pytest tests/                                              # gate: classifier behavior frozen
+python3 build_members.py  --congress 119                   # parallel; writes per-member JSON + manifest.json + build_report.json
+python3 build_app.py      --congress 119                   # template + vendor → results/<C>/
+# OR one command:
+python3 build_all.py      --congress 119
+```
+
+### `build_members.py` requirements
+
+- Loads `votes.jsonl` + `roster.json` once in the parent process.
+- Uses `multiprocessing.Pool(min(8, os.cpu_count()))` to run `analyze.aggregate()` per member.
+- Per-member JSON includes a `_meta` block (compliance-required reproducibility metadata):
+  ```json
+  "_meta": {
+    "schema_version": 1,
+    "pipeline_version": "1.0.0",
+    "classifier_hash": "<sha256 of analyze.py>",
+    "data_snapshot_date": "2026-05-24",
+    "source_xml_count": {"house": 553, "senate": 789}
+  }
+  ```
+- Atomic writes: write to `*.tmp`, then `os.replace`. Per-member failures log to `build_report.json` and continue, never abort the batch.
+- `manifest.json` is shipped as an **array of objects** (programmer recommendation), with a top-level `version` field for client-side cache-busting:
+  ```json
+  {
+    "version": "<pipeline_version>+<data_snapshot_date>",
+    "members": [{"id":"M001184","n":"Thomas Massie","p":"R","s":"KY","c":"H"}, …]
+  }
+  ```
+
+### `build_app.py` requirements
+
+- More than a `cp`: copies `template/` into `results/<C>/`, stamps the manifest version into `app.html` / `compare.html` as inline JSON (saves a fetch), copies `data/<C>/members/` and `data/<C>/manifest.json` into `results/<C>/data/`, and writes the recommended-CSP `README.md`.
+
+### `analyze.py` test suite (compliance-required)
+
+Frozen fixture XMLs covering: partisan-line vote, bipartisan vote, tied/Split party position, member absent, Aye/No vs Yea/Nay normalization, failed-measure blocking case (House and Senate). Each test asserts the metrics dict for a known member.
+
+---
+
+## NOTES.md (NEW)
+
+This file captures concerns we considered but chose NOT to address in MVP:
+
+1. **Editorial label wording (compliance Finding 1, High).** Current labels ("Helped Republicans", "Blocked Dem-Backed") attribute intent/agency the math doesn't measure. Neutral alternatives proposed ("Aligned with R majority", "Voted Nay on failed D-backed measures"). Decision: keep current wording per user preference; revisit before any third-party publication.
+2. **compare.html chart scope (PM + programmer, Medium).** Voted-against-own-party overlay and vote-distribution comparison are derivative of single-member data. Decision: ship all 5 charts per user preference; trim if user-research shows redundancy.
+3. **Visible caveats panel (compliance Finding 2).** §10 of DOCUMENTATION.md lists material limitations (procedural vs substantive votes treated equally, blocking is per-share-not-marginal, lone-wolf ≤5 is heuristic, voice votes invisible). Recommend adding a collapsible "How to read this" panel on dashboards. Deferred — surface in v1.1.
+4. **`localStorage` as MVP feature.** Listed as Could-Have; cut if Phase 3 runs long.
+5. **120th Congress dry-run.** No live data exists yet; pipeline generalization tested by code review only until then.
+
+---
+
+## Implementation Phases & Verification
+
+### Phase 0 — Reset (immediate)
+- Mark superseded TaskCreate items 14, 15, 16 as deleted (they targeted the pre-pivot plan).
+- Create `.env` with the existing API key; create `.gitignore` listing `.env` + `__pycache__/` + `*.pyc`; redact `DOCUMENTATION.md` §2 to reference `<see .env>` and add rotation guidance.
+- Create `NOTES.md` with the items above.
+
+### Phase 0.5 — Complete-roster enrichment (NEW)
+
+Building a dashboard for **every** member of the 119th Congress requires a
+complete roster, not just members who appear in vote XMLs. Members who died,
+resigned, or were sworn in mid-term but never cast a vote (rare but possible)
+would otherwise be missing.
+
+**Source:** Congress.gov API `/member/congress/119` (uses the `CONGRESS_GOV_API_KEY`
+now in `.env`). This is the first justified use of the API key since the
+project began; the Clerk XML alone cannot answer "who served in the 119th."
+
+**New script: `enrich_roster.py`**
+- Paginates `https://api.congress.gov/v3/member/congress/119?currentMember=false&limit=250&format=json&api_key=<key>` (≈540 members across ≈3 pages).
+- Throttled 350 ms between requests; reads key from `.env` (never from CLI).
+- Writes `data/119/members_directory.json` keyed by bioguide:
+  ```json
+  {
+    "M001184": {
+      "bioguide": "M001184",
+      "lis": null,                       # populated for senators in a second pass
+      "full_name": "Thomas Massie",
+      "party": "R",
+      "state": "KY",
+      "district": "4",
+      "chamber": "House",
+      "served_from": "2012-11-13",
+      "served_to": null,                 # null if currently serving
+      "photo_url": "...",                # if available
+      "source": "congress.gov/v3"
+    },
+    ...
+  }
+  ```
+- Second pass: for each Senate member, fetch
+  `https://api.congress.gov/v3/member/<bioguide>?api_key=<key>` to read the
+  LIS member ID (needed to join with the Senate vote cache that uses LIS IDs,
+  not bioguide). Cache results to `data/119/lis_to_bioguide.json` for the
+  reverse map.
+
+**Merge step in `parse.py`** (modification, not new file): when writing
+`roster.json`, union the vote-derived roster with `members_directory.json` so
+every 119th-Congress member is represented, even if `votes` is empty for them.
+Members with zero votes get a `served_partial: true` flag so `app.js` can
+auto-render a member note like
+*"This member did not cast any roll-call votes during the period analyzed
+(served {served_from} – {served_to}). Dashboards reflect that absence."*
+
+**Validation:** assert `len(roster.json) >= 535` and that every entry has
+`bioguide` + `chamber` + `party` + `state`. Fail the build if not.
+
+**Failure mode:** if the Congress.gov API is unreachable or rate-limited,
+the build falls back to the vote-derived roster with a warning logged to
+`build_report.json`. `build_members.py` still produces dashboards for every
+member found; readers see a banner explaining the roster may be incomplete.
+
+This phase runs after `parse.py` and before `build_members.py`. Throttled
+fetch takes ≈3 min cold; idempotent (cached in `data/119/api_cache/`).
+
+### Phase 1 — Foundation (PM Phase 1 = MVP enabler)
+- `build_members.py` with parallel pool + atomic writes + `_meta` block + `build_report.json`.
+- `manifest.json` array format with `version` field.
+- Skeleton `template/app.html` / `app.js` / `app.css` (structure only, no charts yet).
+- Vendor Chart.js 4.4.0 + SortableJS 1.15.2 into `template/vendor/`.
+- `pytest tests/test_analyze.py` passes against frozen fixtures.
+
+### Phase 2 — Single-member view
+- Sidebar filters + typeahead (vanilla JS, ~300 LOC).
+- Member JSON fetch + in-place Chart.js dataset updates for all 5 charts (vote distribution, alignment doughnut, blocking bars, alignment-over-time line, with/against stacked).
+- Sortable/filterable vote table (port logic from `legacy/`, switch all `innerHTML` of upstream strings to `textContent`).
+- URL deep-linking (`pushState` on selection, `replaceState` on filter typing).
+- Member-note banner support (MTG case still needs to surface).
+
+### Phase 3 — Milestone 0 validation gate (HARD GATE)
+- Regenerate the 8 legacy members through the new pipeline.
+- Diff KPI numbers against `legacy/*.html` (extract via `grep` on KPI placeholders).
+- Confirm member-note banner shows for MTG, deep-links work, vendor scripts load locally with no CDN traffic.
+- If parity fails: fix `analyze.py` / `build_members.py`, do NOT proceed.
+
+### Phase 4 — Full member build
+- `build_members.py --congress 119` for all ~535. Confirm completion in <60 s.
+- Smoke-test 10 randomly chosen members across both chambers + all parties.
+
+### Phase 5 — Comparison view
+- `compare.html` + multi-select pills (reuse sidebar/typeahead from `app.js`).
+- All 5 overlay charts implemented.
+- Shareable URL (`?ids=…`, validated + capped at 6).
+
+### Phase 6 — Embedding, security hardening, polish
+- Namespace audit of all CSS under `#polisci-root`; test inline-div embed in a throwaway host page.
+- `data-base` attribute support; iframe embed test.
+- `results/119/README.md` with recommended CSP + sandbox snippet:
+  ```
+  Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';
+    img-src 'self' data:; connect-src 'self'; frame-ancestors <host-domain>;
+    base-uri 'none'; form-action 'none'
+  <iframe src="…/app.html" sandbox="allow-scripts allow-same-origin" referrerpolicy="no-referrer">
+  ```
+- Cross-browser smoke test (Chrome, Firefox, Safari).
+
+### Phase 7 — Documentation & close-out
+- Update `DOCUMENTATION.md` §8 (new file layout), §9 (new regeneration commands), §11 (change-log entries).
+- Verify `PROJECT_SCOPE.md` reflects shipped reality (have PM agent revise once if needed).
+- Delete `legacy/` after Phase 3 gate passes AND user confirms.
+
+---
+
+## Verification
+
+End-to-end success means:
+
+```bash
+python3 fetch.py --congress 119     # exits 0; cache already populated; near-zero fetch
+python3 parse.py --congress 119     # writes votes.jsonl + roster.json
+pytest tests/                       # all green
+python3 build_members.py --congress 119  # writes 535 JSONs in <60 s; build_report.json shows 0 failures
+python3 build_app.py --congress 119      # writes results/119/{app.html, compare.html, data/, vendor/, README.md}
+# Open results/119/app.html in a browser:
+#   - Pick a member from dropdown → dashboard re-renders without page load
+#   - Filter by Senate / Party R / State KY → typeahead shows Rand Paul, McConnell
+#   - URL updates on selection, shareable; reload restores state
+#   - Network panel shows ONLY local requests (vendor + manifest + selected member JSON)
+# Open results/119/compare.html:
+#   - Select Massie + Khanna + AOC → 5 charts overlay with color-coded lines/dots
+#   - URL updates with ?ids=…; reload restores state
+# Open results/119/README.md → recommended CSP + iframe sandbox snippet visible
+```
+
+Run-once additional checks:
+- `grep -RE "innerHTML\s*=" template/ results/` returns no matches against upstream strings.
+- `grep -RE "cdn\.|cdnjs|jsdelivr|unpkg" template/ results/` returns nothing.
+- `grep "g9axyby" DOCUMENTATION.md` returns nothing (key redacted).
+- `cat .gitignore | grep -E '^\.env$'` returns the line.
+- `python3 -c "import json; m=json.load(open('results/119/data/manifest.json')); print(len(m['members']))"` reports ≥535.
+
+---
+
+## Execution Strategy (parallel programmer agents)
+
+After plan approval, the very first action is to copy this plan to
+`/home/user/polisci/research/PLAN.md` so a fresh context can pick it up.
+The user will then clear context. The next session should orchestrate
+implementation with **multiple `programmer` agents in parallel** wherever
+the work is independent. Dependent steps must run sequentially.
+
+### Parallelization map
+
+**Phase 0 (3 parallel programmer agents):**
+- Agent A: create `.env` + `.gitignore`; redact API key in `DOCUMENTATION.md` §2.
+- Agent B: write `NOTES.md` with the 5 deferred concerns.
+- Agent C: download Chart.js 4.4.0 + SortableJS 1.15.2 into `template/vendor/`.
+
+**Phase 1 (4 parallel programmer agents + 1 sequential gate):**
+- Agent A: write `build_members.py` (parallel pool, atomic writes, `_meta`, `build_report.json`, manifest array+version).
+- Agent B: write `tests/fixtures/*.xml` + `tests/test_analyze.py`. Synthesize fixtures from real cached XML.
+- Agent C: write skeleton `template/app.html` + `template/app.css` (structure only, namespaced under `#polisci-root`, no chart logic yet).
+- Agent D: write `enrich_roster.py` (Phase 0.5) + modify `parse.py` to merge `members_directory.json` into `roster.json`.
+- Sequential gate after all four: run `enrich_roster.py`, run `parse.py`, run `pytest tests/` and confirm green; run `build_members.py` and confirm ≥535 JSONs + manifest emitted.
+
+**Phase 2 (1 programmer agent — sequential, deeply interlinked frontend logic):**
+- Single agent writes all of `template/app.js`: manifest loader, sidebar filters, typeahead, member-fetch + in-place Chart.js updates for all 5 charts, sortable/filterable table, URL deep-linking, member-note banner. Splitting risks state-management drift.
+
+**Phase 3 (1 programmer agent — sequential validation gate):**
+- Agent runs `build_members.py` for the 8 legacy member IDs, opens the new dashboards, extracts KPI numbers from `legacy/*.html`, diffs, reports.
+
+**Phase 4 (1 programmer agent — sequential, bulk build):**
+- Full `build_members.py --congress 119` + 10-random smoke test.
+
+**Phase 5 (5 parallel programmer agents — one per overlay chart):**
+- Each agent implements one of the 5 comparison charts in `template/compare.html` + adds its render function to `app.js`. Shared scaffolding (multi-select pills, URL state) implemented by one designated agent first, then the 5 chart agents fork off.
+
+**Phase 6 (3 parallel programmer agents):**
+- Agent A: `build_app.py` orchestrator + `results/119/README.md` with CSP + sandbox snippets.
+- Agent B: CSS namespacing audit + inline-div embed test in a throwaway host page.
+- Agent C: `data-base` attribute support + iframe embed test + cross-browser smoke (Chrome, Firefox, Safari via headless if available).
+
+**Phase 7 (1 programmer agent + 1 PM agent):**
+- Programmer: update `DOCUMENTATION.md` §8, §9, §11.
+- PM (sequential, after programmer): revise `PROJECT_SCOPE.md` to reflect shipped reality.
+
+### Coordination rules
+
+- Each parallel batch is a single message with multiple `Agent` tool calls.
+- Each agent prompt MUST cite the plan path (`/home/user/polisci/research/PLAN.md`) and the specific section/files it owns.
+- Trust-but-verify: after each parallel batch completes, read the actual diffs (not just the agent summaries) before launching the next phase.
+- If a parallel agent's work depends on another's output, escalate to sequential rather than guess.
+
+## Critical Files (to be created or modified)
+
+- CREATE: `enrich_roster.py`, `build_members.py`, `build_app.py`, `build_all.py`, `NOTES.md`, `.env`, `.gitignore`, `template/app.html`, `template/compare.html`, `template/app.js`, `template/app.css`, `template/vendor/{chart,sortable}.min.js`, `tests/test_analyze.py`, `tests/fixtures/*.xml`, `results/119/README.md`, `research/PLAN.md` (copy of this plan)
+- MODIFY: `DOCUMENTATION.md` (§2 redact key, §8 file layout, §9 regen commands, §11 change-log), `parse.py` (add upstream-string validation + merge with `members_directory.json`), `PROJECT_SCOPE.md` (PM agent owns this)
+- DELETE (after Phase 3 gate): `legacy/`
+
+## Reused Existing Code
+
+- `analyze.py` (`aggregate`, `classify_vote`, `_norm_vote`, `_majority_position`, `LONE_WOLF_THRESHOLD`) — unchanged; consumed by `build_members.py` in worker processes.
+- `parse.py` schema (chamber-unified `votes.jsonl` + `roster.json`) — consumed unchanged.
+- `legacy/*.html` rendering logic for KPI cards, the 5 chart types, the sortable/filterable table — port to vanilla JS in `template/app.js` with `innerHTML` → `textContent` substitution for upstream strings.
+- `DOCUMENTATION.md` §6 (Classification) and §10 (Limitations) — referenced from dashboards; not duplicated.

+ 38 - 0
research/TEST-PLAN.md

@@ -0,0 +1,38 @@
+# Test Plan — analyze.py classifier suite (compliance-required)
+
+_Generated: 2026-05-24. Mode: Author. Caller: Phase 1 Agent B._
+
+## Purpose
+Freeze classifier behavior in `analyze.py` so downstream `build_members.py` cannot
+silently regress KPI semantics. Compliance-required per PLAN.md §"analyze.py test suite".
+
+## Scope
+**In:** `_norm_vote`, `_majority_position`, `classify_vote`, `aggregate` from analyze.py;
+end-to-end parse→classify→aggregate via parse.py.
+**Out:** parse.py exhaustive tests, render code, fetch code.
+
+## Framework
+- **Chosen:** pytest 9.0.3 (installed via `python3 -m pip install --user --break-system-packages pytest`)
+- **Source of pinning:** project has no manifest; pytest is the standard Python runner
+
+## Sandbox / Layout
+- **Path:** `./tests/` (in-tree, per the user-specified PLAN.md layout)
+- **Sandbox exception:** PLAN.md §"Target File Layout" mandates in-tree `tests/`.
+
+## Mock Boundaries
+- **Mocked:** none. Fixtures are real XML on disk.
+- **Not mocked:** analyze.py, parse.py.
+
+## Test Files
+| # | File | Covers | Test count (est.) |
+|---|---|---|---|
+| 1 | tests/test_analyze.py | normalization, classification, aggregation | ~12 |
+
+Fixtures: 4 required + 1 optional (split-party tied).
+
+## CI visibility
+This suite lives under `./tests/` (in-tree, NOT gitignored — per PLAN.md). It runs
+when explicitly invoked via `python3 -m pytest tests/`.
+
+## Approval token
+APPROVED-TEST-PLAN was provided by Phase 1 Agent B caller.

+ 472 - 0
template/app.css

@@ -0,0 +1,472 @@
+/* Base reset scoped to root */
+#polisci-root, #polisci-root *, #polisci-root *::before, #polisci-root *::after {
+  box-sizing: border-box;
+}
+
+#polisci-root {
+  --ps-party-r: #d9534f;
+  --ps-party-d: #337ab7;
+  --ps-party-i: #5cb85c;
+  --ps-accent: #2c3e50;
+  --ps-bg: #f5f7fa;
+  --ps-surface: #ffffff;
+  --ps-text: #1f2933;
+  --ps-text-muted: #5b6471;
+  --ps-border: #d8dde4;
+  --ps-row-alt: #f9fafc;
+  --ps-hover: #eef3f9;
+  --ps-radius: 6px;
+  --ps-shadow: 0 1px 2px rgba(0,0,0,0.04);
+
+  font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+  font-size: 15px;
+  line-height: 1.45;
+  color: var(--ps-text);
+  background: var(--ps-bg);
+  padding: 16px;
+  max-width: 1400px;
+  margin: 0 auto;
+}
+
+#polisci-root .is-hidden { display: none !important; }
+
+#polisci-root .visually-hidden {
+  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
+  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
+}
+
+/* Header */
+#polisci-root .ps-header { margin-bottom: 16px; }
+#polisci-root .ps-header h1 { font-size: 1.6rem; margin: 0 0 4px; color: var(--ps-accent); }
+#polisci-root .ps-subtitle { margin: 0; color: var(--ps-text-muted); font-size: 0.95rem; }
+#polisci-root .member-note {
+  margin-top: 10px;
+  padding: 10px 14px;
+  background: #fff8e1;
+  border: 1px solid #f0d97a;
+  border-radius: var(--ps-radius);
+  font-size: 0.92rem;
+}
+
+/* Layout */
+#polisci-root .ps-layout {
+  display: grid;
+  grid-template-columns: 240px 1fr;
+  gap: 20px;
+  align-items: start;
+}
+
+/* Sidebar */
+#polisci-root .sidebar {
+  background: var(--ps-surface);
+  border: 1px solid var(--ps-border);
+  border-radius: var(--ps-radius);
+  padding: 14px;
+  box-shadow: var(--ps-shadow);
+  position: sticky;
+  top: 12px;
+}
+#polisci-root .sidebar-toggle {
+  display: none;
+  width: 100%;
+  padding: 8px 12px;
+  background: var(--ps-accent);
+  color: #fff;
+  border: none;
+  border-radius: var(--ps-radius);
+  font-size: 0.95rem;
+  cursor: pointer;
+}
+#polisci-root .filter-group {
+  border: none;
+  padding: 0;
+  margin: 0 0 14px;
+}
+#polisci-root .filter-group legend {
+  font-weight: 600;
+  font-size: 0.85rem;
+  text-transform: uppercase;
+  letter-spacing: 0.04em;
+  color: var(--ps-text-muted);
+  margin-bottom: 6px;
+  padding: 0;
+}
+#polisci-root .filter-group label {
+  display: block;
+  padding: 3px 0;
+  font-size: 0.93rem;
+  cursor: pointer;
+}
+#polisci-root .filter-group input[type="checkbox"] { margin-right: 6px; }
+#polisci-root #filter-state-select {
+  width: 100%;
+  font-size: 0.9rem;
+  border: 1px solid var(--ps-border);
+  border-radius: var(--ps-radius);
+  padding: 4px;
+}
+#polisci-root .btn-reset {
+  width: 100%;
+  padding: 7px 10px;
+  background: var(--ps-surface);
+  color: var(--ps-text);
+  border: 1px solid var(--ps-border);
+  border-radius: var(--ps-radius);
+  cursor: pointer;
+  font-size: 0.9rem;
+}
+#polisci-root .btn-reset:hover { background: var(--ps-hover); }
+#polisci-root .hint {
+  margin: 10px 0 0;
+  font-size: 0.82rem;
+  color: var(--ps-text-muted);
+}
+
+/* Main */
+#polisci-root .ps-main { min-width: 0; }
+
+/* Search */
+#polisci-root .search-wrap {
+  position: relative;
+  margin-bottom: 16px;
+}
+#polisci-root #member-search {
+  width: 100%;
+  padding: 10px 14px;
+  font-size: 1rem;
+  border: 1px solid var(--ps-border);
+  border-radius: var(--ps-radius);
+  background: var(--ps-surface);
+}
+#polisci-root #member-search:focus {
+  outline: 2px solid var(--ps-party-d);
+  outline-offset: -1px;
+}
+#polisci-root .member-results {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  right: 0;
+  margin: 4px 0 0;
+  padding: 0;
+  list-style: none;
+  background: var(--ps-surface);
+  border: 1px solid var(--ps-border);
+  border-radius: var(--ps-radius);
+  box-shadow: 0 4px 12px rgba(0,0,0,0.08);
+  max-height: 320px;
+  overflow-y: auto;
+  z-index: 20;
+}
+#polisci-root .member-results li {
+  padding: 8px 12px;
+  cursor: pointer;
+  font-size: 0.94rem;
+  border-bottom: 1px solid var(--ps-border);
+}
+#polisci-root .member-results li:last-child { border-bottom: none; }
+#polisci-root .member-results li:hover,
+#polisci-root .member-results li[aria-selected="true"] {
+  background: var(--ps-hover);
+}
+
+/* Member summary */
+#polisci-root .member-summary {
+  background: var(--ps-surface);
+  border: 1px solid var(--ps-border);
+  border-radius: var(--ps-radius);
+  padding: 14px;
+  margin-bottom: 16px;
+  min-height: 60px;
+  box-shadow: var(--ps-shadow);
+}
+#polisci-root .member-summary:empty { display: none; }
+
+/* KPI cards */
+#polisci-root .kpi-cards {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+  gap: 12px;
+  margin-bottom: 20px;
+}
+#polisci-root .kpi-card {
+  background: var(--ps-surface);
+  border: 1px solid var(--ps-border);
+  border-radius: var(--ps-radius);
+  padding: 12px 14px;
+  box-shadow: var(--ps-shadow);
+}
+#polisci-root .kpi-label {
+  font-size: 0.78rem;
+  text-transform: uppercase;
+  letter-spacing: 0.04em;
+  color: var(--ps-text-muted);
+  margin-bottom: 4px;
+}
+#polisci-root .kpi-value {
+  font-size: 1.6rem;
+  font-weight: 600;
+  color: var(--ps-accent);
+  font-variant-numeric: tabular-nums;
+}
+
+/* Charts grid */
+#polisci-root .charts-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
+  gap: 16px;
+  margin-bottom: 24px;
+}
+#polisci-root .chart-frame {
+  background: var(--ps-surface);
+  border: 1px solid var(--ps-border);
+  border-radius: var(--ps-radius);
+  padding: 16px;
+  margin: 0;
+  height: 340px;
+  box-shadow: var(--ps-shadow);
+  display: flex;
+  flex-direction: column;
+}
+#polisci-root .chart-frame figcaption {
+  font-size: 0.95rem;
+  font-weight: 600;
+  color: var(--ps-accent);
+  margin-bottom: 10px;
+  flex: 0 0 auto;
+}
+#polisci-root .chart-canvas-wrap {
+  position: relative;
+  flex: 1 1 auto;
+  min-height: 0;
+  width: 100%;
+}
+#polisci-root .chart-frame canvas {
+  position: absolute;
+  top: 0; left: 0;
+  width: 100% !important;
+  height: 100% !important;
+}
+#polisci-root .chart-wide { grid-column: 1 / -1; }
+
+/* Vote table */
+#polisci-root .vote-table-section {
+  background: var(--ps-surface);
+  border: 1px solid var(--ps-border);
+  border-radius: var(--ps-radius);
+  padding: 14px;
+  box-shadow: var(--ps-shadow);
+  margin-bottom: 20px;
+}
+#polisci-root .vote-table-section h2 {
+  font-size: 1.1rem;
+  margin: 0 0 12px;
+  color: var(--ps-accent);
+}
+#polisci-root .table-scroll {
+  overflow-x: auto;
+  max-height: 600px;
+  overflow-y: auto;
+}
+#polisci-root #vote-table {
+  width: 100%;
+  border-collapse: collapse;
+  font-size: 0.88rem;
+}
+#polisci-root #vote-table thead th {
+  position: sticky;
+  top: 0;
+  background: var(--ps-accent);
+  color: #fff;
+  text-align: left;
+  padding: 8px 10px;
+  font-weight: 600;
+  font-size: 0.82rem;
+  white-space: nowrap;
+  z-index: 1;
+}
+#polisci-root #vote-table tbody td {
+  padding: 6px 10px;
+  border-bottom: 1px solid var(--ps-border);
+  vertical-align: top;
+}
+#polisci-root #vote-table tbody tr:nth-child(even) { background: var(--ps-row-alt); }
+#polisci-root #vote-table tbody tr:hover { background: var(--ps-hover); }
+#polisci-root #vote-table td:nth-child(1),
+#polisci-root #vote-table td:nth-child(2),
+#polisci-root #vote-table td:nth-child(3),
+#polisci-root #vote-table td:nth-child(9),
+#polisci-root #vote-table td:nth-child(10) {
+  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
+  font-variant-numeric: tabular-nums;
+  white-space: nowrap;
+}
+
+/* Footer */
+#polisci-root .ps-footer {
+  margin-top: 16px;
+  padding-top: 12px;
+  border-top: 1px solid var(--ps-border);
+  font-size: 0.82rem;
+  color: var(--ps-text-muted);
+}
+
+/* Comparison view (compare.html) */
+#polisci-root .compare-pills-section {
+  margin: 12px 0 8px;
+  padding: 10px 12px;
+  border: 1px solid var(--ps-border);
+  border-radius: 6px;
+  background: var(--ps-surface, #fff);
+}
+#polisci-root .compare-pills-title {
+  font-size: 0.95rem;
+  margin: 0 0 6px;
+  color: var(--ps-text-muted);
+  font-weight: 600;
+  text-transform: uppercase;
+  letter-spacing: 0.04em;
+}
+#polisci-root .selected-pills {
+  list-style: none;
+  padding: 0;
+  margin: 0 0 4px;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+}
+#polisci-root .compare-pill {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  padding: 4px 8px;
+  border: 1px solid #ccc;
+  border-radius: 999px;
+  font-size: 0.9rem;
+  line-height: 1.1;
+}
+#polisci-root .compare-pill-swatch {
+  display: inline-block;
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+}
+#polisci-root .compare-pill-name {
+  background: none;
+  border: 0;
+  padding: 0;
+  font: inherit;
+  color: inherit;
+  cursor: pointer;
+  text-decoration: underline;
+  text-underline-offset: 2px;
+}
+#polisci-root .compare-pill-name:hover { color: var(--ps-accent); }
+#polisci-root .compare-pill-remove {
+  background: none;
+  border: 0;
+  padding: 0 2px;
+  margin-left: 2px;
+  font-size: 1.1rem;
+  line-height: 1;
+  color: var(--ps-text-muted);
+  cursor: pointer;
+}
+#polisci-root .compare-pill-remove:hover { color: #c0392b; }
+#polisci-root .align-switcher-wrap {
+  margin: 10px 0;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 0.9rem;
+  color: var(--ps-text-muted);
+}
+#polisci-root .align-switcher-wrap select {
+  padding: 4px 6px;
+  border: 1px solid var(--ps-border);
+  border-radius: 4px;
+  font: inherit;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+  #polisci-root { padding: 10px; }
+  #polisci-root .ps-layout {
+    grid-template-columns: 1fr;
+  }
+  #polisci-root .sidebar {
+    position: static;
+    padding: 10px;
+  }
+  #polisci-root .sidebar-toggle { display: block; margin-bottom: 10px; }
+  #polisci-root .sidebar-body { display: none; }
+  #polisci-root .sidebar.is-open .sidebar-body { display: block; }
+  #polisci-root .ps-header h1 { font-size: 1.3rem; }
+  #polisci-root .kpi-value { font-size: 1.3rem; }
+  #polisci-root .chart-frame { height: 280px; }
+}
+
+/* Top-level nav between Member / Compare / Rankings pages */
+#polisci-root .ps-nav {
+  margin: 4px 0 8px;
+  font-size: 0.95rem;
+}
+#polisci-root .ps-nav a {
+  color: var(--ps-accent);
+  text-decoration: none;
+  padding: 2px 6px;
+  border-radius: 4px;
+}
+#polisci-root .ps-nav a:hover { background: var(--ps-hover); }
+#polisci-root .ps-nav a[aria-current="page"] {
+  background: var(--ps-accent);
+  color: #fff;
+  font-weight: 600;
+}
+
+/* Ranking page */
+#polisci-root .rank-summary {
+  margin-bottom: 12px;
+  color: var(--ps-text-muted, #555);
+  font-size: 0.95rem;
+}
+#polisci-root .rank-summary-line { margin: 0; }
+#polisci-root #rank-table {
+  width: 100%;
+  border-collapse: collapse;
+  font-size: 0.92rem;
+}
+#polisci-root #rank-table thead th {
+  background: var(--ps-row-alt);
+  text-align: left;
+  padding: 8px 10px;
+  border-bottom: 2px solid var(--ps-border);
+  position: sticky;
+  top: 0;
+  z-index: 1;
+}
+#polisci-root #rank-table tbody td {
+  padding: 6px 10px;
+  border-bottom: 1px solid var(--ps-border);
+}
+#polisci-root #rank-table tbody tr.rank-row { cursor: pointer; }
+#polisci-root #rank-table tbody tr:nth-child(even) { background: var(--ps-row-alt); }
+#polisci-root #rank-table tbody tr:hover { background: var(--ps-hover); }
+#polisci-root #rank-table td:first-child {
+  width: 60px;
+  font-variant-numeric: tabular-nums;
+  color: var(--ps-text-muted, #555);
+}
+#polisci-root #rank-table .rank-value {
+  text-align: right;
+  font-variant-numeric: tabular-nums;
+  font-weight: 600;
+}
+#polisci-root #rank-table .party-cell {
+  font-weight: 600;
+  width: 40px;
+  text-align: center;
+}
+#polisci-root #rank-table .party-R { color: #d9534f; }
+#polisci-root #rank-table .party-D { color: #337ab7; }
+#polisci-root #rank-table .party-I { color: #5cb85c; }

+ 135 - 0
template/app.html

@@ -0,0 +1,135 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>119th Congress — Member Voting Dashboard</title>
+<link rel="stylesheet" href="app.css">
+<script src="vendor/chart.umd.min.js" defer></script>
+<script src="vendor/sortable.min.js" defer></script>
+<script src="app.js" defer></script>
+</head>
+<body>
+<noscript>This dashboard requires JavaScript.</noscript>
+
+<div id="polisci-root" data-base="./data/">
+  <!-- Header -->
+  <header class="ps-header">
+    <h1>119th Congress — Member Voting Dashboard</h1>
+    <nav class="ps-nav"><a href="app.html" aria-current="page">Member</a> · <a href="compare.html">Compare</a> · <a href="ranking.html">Rankings</a></nav>
+    <p class="ps-subtitle">Roll-call voting record, alignment, and blocking activity for any member of the 119th Congress.</p>
+    <div id="member-note" class="member-note is-hidden" role="status"></div>
+  </header>
+
+  <div class="ps-layout">
+    <!-- Sidebar -->
+    <aside class="sidebar" id="sidebar">
+      <button type="button" class="sidebar-toggle" id="sidebar-toggle" aria-expanded="true" aria-controls="sidebar-body">Filters</button>
+      <div class="sidebar-body" id="sidebar-body">
+        <fieldset class="filter-group" id="filter-chamber">
+          <legend>Chamber</legend>
+          <label><input type="checkbox" name="chamber" value="H" checked> House</label>
+          <label><input type="checkbox" name="chamber" value="S" checked> Senate</label>
+        </fieldset>
+
+        <fieldset class="filter-group" id="filter-party">
+          <legend>Party</legend>
+          <label><input type="checkbox" name="party" value="R" checked> Republican</label>
+          <label><input type="checkbox" name="party" value="D" checked> Democrat</label>
+          <label><input type="checkbox" name="party" value="I" checked> Independent</label>
+        </fieldset>
+
+        <fieldset class="filter-group" id="filter-state">
+          <legend>State</legend>
+          <select multiple id="filter-state-select" size="8" aria-label="State filter"></select>
+        </fieldset>
+
+        <button type="button" id="filter-reset" class="btn-reset">Reset filters</button>
+        <p class="hint">Filters apply to the member dropdown.</p>
+      </div>
+    </aside>
+
+    <!-- Main -->
+    <main class="ps-main">
+      <div class="search-wrap">
+        <label for="member-search" class="visually-hidden">Search members</label>
+        <input type="search" id="member-search" placeholder="Search by name or initials (e.g., AOC)…" autocomplete="off" role="combobox" aria-controls="member-results" aria-expanded="false">
+        <ul id="member-results" role="listbox" class="member-results is-hidden"></ul>
+      </div>
+
+      <section id="member-summary" class="member-summary"></section>
+
+      <section id="kpi-cards" class="kpi-cards">
+        <div class="kpi-card"><div class="kpi-label"></div><div class="kpi-value"></div></div>
+        <div class="kpi-card"><div class="kpi-label"></div><div class="kpi-value"></div></div>
+        <div class="kpi-card"><div class="kpi-label"></div><div class="kpi-value"></div></div>
+        <div class="kpi-card"><div class="kpi-label"></div><div class="kpi-value"></div></div>
+        <div class="kpi-card"><div class="kpi-label"></div><div class="kpi-value"></div></div>
+        <div class="kpi-card"><div class="kpi-label"></div><div class="kpi-value"></div></div>
+        <div class="kpi-card"><div class="kpi-label"></div><div class="kpi-value"></div></div>
+        <div class="kpi-card"><div class="kpi-label"></div><div class="kpi-value"></div></div>
+      </section>
+
+      <!-- Charts grid -->
+      <section class="charts-grid">
+        <figure class="chart-frame">
+          <figcaption>Vote distribution</figcaption>
+          <div class="chart-canvas-wrap"><canvas id="chart-vote-dist"></canvas></div>
+        </figure>
+        <figure class="chart-frame">
+          <figcaption>Party alignment</figcaption>
+          <div class="chart-canvas-wrap"><canvas id="chart-alignment"></canvas></div>
+        </figure>
+        <figure class="chart-frame">
+          <figcaption>Blocked measures</figcaption>
+          <div class="chart-canvas-wrap"><canvas id="chart-blocked"></canvas></div>
+        </figure>
+        <figure class="chart-frame chart-wide">
+          <figcaption>Alignment over time</figcaption>
+          <div class="chart-canvas-wrap"><canvas id="chart-alignment-time"></canvas></div>
+        </figure>
+        <figure class="chart-frame chart-wide">
+          <figcaption>With / Against own party</figcaption>
+          <div class="chart-canvas-wrap"><canvas id="chart-with-against"></canvas></div>
+        </figure>
+      </section>
+
+      <!-- Vote table -->
+      <section id="vote-table-section" class="vote-table-section">
+        <h2>Roll-call votes</h2>
+        <div class="table-scroll">
+          <table id="vote-table">
+            <thead>
+              <tr>
+                <th>Year</th>
+                <th>Roll</th>
+                <th>Date</th>
+                <th>Bill</th>
+                <th>Question</th>
+                <th>Description</th>
+                <th>Result</th>
+                <th>Member Vote</th>
+                <th>R Yea/Nay</th>
+                <th>D Yea/Nay</th>
+                <th>Alignment</th>
+                <th>Blocked</th>
+              </tr>
+            </thead>
+            <tbody></tbody>
+          </table>
+        </div>
+      </section>
+
+      <footer class="ps-footer">
+        <p>Source data:
+          <a href="https://clerk.house.gov/Votes" target="_blank" rel="noopener noreferrer">House Clerk roll-call XML</a> ·
+          <a href="https://www.senate.gov/legislative/votes_new.htm" target="_blank" rel="noopener noreferrer">Senate roll-call XML</a> ·
+          <a href="Methodology.md" target="_blank" rel="noopener">Methodology</a>
+        </p>
+        <p>Last regenerated: <span id="last-generated">(populated)</span></p>
+      </footer>
+    </main>
+  </div>
+</div>
+</body>
+</html>

+ 622 - 0
template/app.js

@@ -0,0 +1,622 @@
+(function () {
+  'use strict';
+
+  // Security: validate any member id before fetch / DOM use.
+  var ID_RE = /^[A-Z]\d{6}$|^S\d{3,4}$/;
+  var LS_KEY = 'polisci:v119:lastMember';
+  var COLORS = {
+    R: '#d9534f', D: '#337ab7', I: '#5cb85c',
+    helpedR: '#d9534f', helpedD: '#337ab7',
+    helpedBoth: '#8e44ad', helpedNeither: '#7f8c8d',
+    green: '#27ae60', red: '#c0392b'
+  };
+  var ALIGN_KEYS = ['Helped Republicans', 'Helped Democrats', 'Helped Both', 'Helped Neither'];
+  var ALIGN_COLORS = [COLORS.helpedR, COLORS.helpedD, COLORS.helpedBoth, COLORS.helpedNeither];
+
+  var state = {
+    manifest: null,
+    membersById: {},          // id -> manifest entry
+    BASE: './data/',
+    currentId: null,
+    currentMember: null,
+    sortCol: null,
+    sortAsc: true,
+    selectedStates: null,     // null => all
+    skipPush: false,
+    urlReplaceTimer: 0
+  };
+
+  var els = {};
+  var charts = {};
+
+  document.addEventListener('DOMContentLoaded', init);
+
+  function init() {
+    var root = document.getElementById('polisci-root');
+    if (!root) return;
+    state.BASE = root.dataset.base || './data/';
+
+    els.root = root;
+    els.note = document.getElementById('member-note');
+    els.summary = document.getElementById('member-summary');
+    els.kpis = document.getElementById('kpi-cards');
+    els.search = document.getElementById('member-search');
+    els.results = document.getElementById('member-results');
+    els.lastGen = document.getElementById('last-generated');
+    els.filterChamber = document.getElementById('filter-chamber');
+    els.filterParty = document.getElementById('filter-party');
+    els.filterState = document.getElementById('filter-state-select');
+    els.filterReset = document.getElementById('filter-reset');
+    els.sidebarToggle = document.getElementById('sidebar-toggle');
+    els.sidebar = document.getElementById('sidebar');
+    els.tableBody = document.querySelector('#vote-table tbody');
+    els.tableHead = document.querySelector('#vote-table thead');
+
+    var inline = document.getElementById('polisci-manifest');
+    if (inline && inline.textContent) {
+      try { onManifest(JSON.parse(inline.textContent)); return; }
+      catch (e) { /* fall through to fetch */ }
+    }
+    var url = state.BASE + 'manifest.json';
+    fetch(url, { credentials: 'omit' })
+      .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
+      .then(onManifest)
+      .catch(function (e) { showFatal('Failed to load manifest: ' + e.message + ' — if you opened this file directly, serve it over HTTP (e.g., `cd results/119 && python3 -m http.server`).'); });
+  }
+
+  function showFatal(msg) {
+    els.root.replaceChildren();
+    var p = document.createElement('p');
+    p.style.padding = '20px';
+    p.style.color = '#c0392b';
+    p.textContent = msg;
+    els.root.appendChild(p);
+  }
+
+  function onManifest(m) {
+    state.manifest = m;
+    var members = m.members || [];
+    for (var i = 0; i < members.length; i++) {
+      state.membersById[members[i].id] = members[i];
+    }
+    els.lastGen.textContent = m.generated_at || '';
+
+    populateStateOptions(members);
+    wireEvents();
+    buildCharts();
+    initSortHeaders();
+    restoreFiltersFromURL();
+    initialSelect();
+    window.addEventListener('popstate', onPopState);
+  }
+
+  function populateStateOptions(members) {
+    var states = {};
+    for (var i = 0; i < members.length; i++) states[members[i].s] = true;
+    var keys = Object.keys(states).sort();
+    els.filterState.replaceChildren();
+    for (var j = 0; j < keys.length; j++) {
+      var opt = document.createElement('option');
+      opt.value = keys[j];
+      opt.textContent = keys[j];
+      opt.selected = true;
+      els.filterState.appendChild(opt);
+    }
+  }
+
+  function wireEvents() {
+    var chamberInputs = els.filterChamber.querySelectorAll('input[type=checkbox]');
+    var partyInputs = els.filterParty.querySelectorAll('input[type=checkbox]');
+    chamberInputs.forEach(function (i) { i.addEventListener('change', onFilterChange); });
+    partyInputs.forEach(function (i) { i.addEventListener('change', onFilterChange); });
+    els.filterState.addEventListener('change', onFilterChange);
+    els.filterReset.addEventListener('click', resetFilters);
+
+    els.search.addEventListener('input', onFilterChange);
+    els.search.addEventListener('focus', refreshFilter);
+    document.addEventListener('click', function (e) {
+      if (!els.results.contains(e.target) && e.target !== els.search) {
+        els.results.classList.add('is-hidden');
+        els.search.setAttribute('aria-expanded', 'false');
+      }
+    });
+
+    if (els.sidebarToggle) {
+      els.sidebarToggle.addEventListener('click', function () {
+        var open = els.sidebar.classList.toggle('is-open');
+        els.sidebarToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
+      });
+    }
+  }
+
+  function onFilterChange() {
+    refreshFilter();
+    debounceUrlReplace();
+  }
+
+  function resetFilters() {
+    var inputs = els.filterChamber.querySelectorAll('input[type=checkbox]');
+    inputs.forEach(function (i) { i.checked = true; });
+    inputs = els.filterParty.querySelectorAll('input[type=checkbox]');
+    inputs.forEach(function (i) { i.checked = true; });
+    for (var j = 0; j < els.filterState.options.length; j++) {
+      els.filterState.options[j].selected = true;
+    }
+    els.search.value = '';
+    refreshFilter();
+    debounceUrlReplace();
+  }
+
+  function getCheckedValues(group) {
+    var out = [];
+    var inputs = group.querySelectorAll('input[type=checkbox]:checked');
+    inputs.forEach(function (i) { out.push(i.value); });
+    return out;
+  }
+
+  function getSelectedStates() {
+    var out = [];
+    for (var i = 0; i < els.filterState.options.length; i++) {
+      if (els.filterState.options[i].selected) out.push(els.filterState.options[i].value);
+    }
+    return out;
+  }
+
+  function initialsOf(name) {
+    var parts = name.split(/\s+/);
+    var out = '';
+    for (var i = 0; i < parts.length; i++) {
+      if (parts[i]) out += parts[i].charAt(0).toUpperCase();
+    }
+    return out;
+  }
+
+  function refreshFilter() {
+    var chambers = getCheckedValues(els.filterChamber);
+    var parties = getCheckedValues(els.filterParty);
+    var states = getSelectedStates();
+    var q = (els.search.value || '').trim();
+    var qLower = q.toLowerCase();
+    var qUpper = q.toUpperCase();
+    var initialsMode = q.length > 0 && q.length <= 4 && /^[A-Za-z]+$/.test(q);
+
+    var members = state.manifest.members;
+    var matches = [];
+    for (var i = 0; i < members.length && matches.length < 50; i++) {
+      var m = members[i];
+      if (chambers.indexOf(m.c) < 0) continue;
+      if (parties.indexOf(m.p) < 0) continue;
+      if (states.indexOf(m.s) < 0) continue;
+      if (q) {
+        var nameLower = m.n.toLowerCase();
+        var hit = nameLower.indexOf(qLower) >= 0;
+        if (!hit && initialsMode) {
+          if (initialsOf(m.n) === qUpper) hit = true;
+        }
+        if (!hit) continue;
+      }
+      matches.push(m);
+    }
+
+    matches.sort(function (a, b) { return a.n.localeCompare(b.n); });
+    renderResults(matches, q);
+  }
+
+  function renderResults(matches, q) {
+    els.results.replaceChildren();
+    if (!q && matches.length === state.manifest.members.length) {
+      els.results.classList.add('is-hidden');
+      els.search.setAttribute('aria-expanded', 'false');
+      return;
+    }
+    if (!matches.length) {
+      var li0 = document.createElement('li');
+      li0.textContent = 'No members match.';
+      li0.setAttribute('aria-disabled', 'true');
+      els.results.appendChild(li0);
+    } else {
+      for (var i = 0; i < matches.length; i++) {
+        var m = matches[i];
+        var li = document.createElement('li');
+        li.setAttribute('role', 'option');
+        li.setAttribute('data-id', m.id);
+        li.textContent = m.n + ' (' + m.p + '-' + m.s + ', ' + (m.c === 'H' ? 'House' : 'Senate') + ')';
+        li.addEventListener('click', onResultClick);
+        els.results.appendChild(li);
+      }
+    }
+    els.results.classList.remove('is-hidden');
+    els.search.setAttribute('aria-expanded', 'true');
+  }
+
+  function onResultClick(e) {
+    var id = e.currentTarget.getAttribute('data-id');
+    if (!isValidId(id)) return;
+    els.results.classList.add('is-hidden');
+    els.search.setAttribute('aria-expanded', 'false');
+    selectMember(id);
+  }
+
+  function isValidId(id) {
+    return typeof id === 'string' && ID_RE.test(id) && Object.prototype.hasOwnProperty.call(state.membersById, id);
+  }
+
+  function selectMember(id) {
+    if (!isValidId(id)) return;
+    var version = state.manifest.version || '';
+    var url = state.BASE + 'members/' + encodeURIComponent(id) + '.json?v=' + encodeURIComponent(version);
+    fetch(url, { credentials: 'omit' })
+      .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
+      .then(function (member) {
+        state.currentId = id;
+        state.currentMember = member;
+        renderMember(member);
+        try { localStorage.setItem(LS_KEY, id); } catch (_) {}
+        if (!state.skipPush) {
+          var u = buildShareUrl(id);
+          history.pushState({ id: id }, '', u);
+        }
+        state.skipPush = false;
+        document.title = member.name + ' — 119th Congress Voting Dashboard';
+      })
+      .catch(function (e) { showError('Failed to load member ' + id + ': ' + e.message); });
+  }
+
+  function showError(msg) {
+    els.summary.replaceChildren();
+    var p = document.createElement('p');
+    p.style.color = '#c0392b';
+    p.textContent = msg;
+    els.summary.appendChild(p);
+  }
+
+  function renderMember(member) {
+    renderSummary(member);
+    renderNote(member);
+    renderKPIs(member);
+    updateCharts(member);
+    renderTable(member.metrics.rows || []);
+  }
+
+  function renderSummary(m) {
+    els.summary.replaceChildren();
+    var h = document.createElement('h2');
+    h.textContent = m.name;
+    h.style.margin = '0 0 6px';
+    els.summary.appendChild(h);
+    var sub = document.createElement('div');
+    sub.style.color = 'var(--ps-text-muted)';
+    sub.style.fontSize = '0.92rem';
+    var chamberLabel = m.chamber === 'house' ? 'House' : 'Senate';
+    var distSuffix = (m.district && m.chamber === 'house') ? '-' + m.district : '';
+    var endDate = m.served_to || 'present';
+    sub.textContent = chamberLabel + ' · ' + m.party + '-' + m.state + distSuffix +
+      ' · served ' + (m.served_from || '?') + ' – ' + endDate;
+    els.summary.appendChild(sub);
+  }
+
+  function renderNote(m) {
+    var partial = m.served_partial === true;
+    var noVotes = !m.metrics || m.metrics.total === 0;
+    if (partial || noVotes) {
+      var endDate = m.served_to || 'present';
+      els.note.textContent = 'This member did not cast roll-call votes during the period analyzed (served ' +
+        (m.served_from || '?') + ' – ' + endDate + '). The dashboards below reflect that absence.';
+      els.note.classList.remove('is-hidden');
+    } else {
+      els.note.textContent = '';
+      els.note.classList.add('is-hidden');
+    }
+  }
+
+  function pct(n, d) {
+    if (!d) return '0%';
+    return ((n / d) * 100).toFixed(1) + '%';
+  }
+
+  function renderKPIs(m) {
+    var x = m.metrics || {};
+    var voting = x.voting || 0;
+    var kpis = [
+      ['Total Votes', String(x.total || 0)],
+      ['Yea / Nay', (x.yeas || 0) + ' / ' + (x.nays || 0)],
+      ['Participation', pct(voting, x.total || 0)],
+      ['Voted With GOP', (x.voted_with_gop || 0) + ' (' + pct(x.voted_with_gop || 0, voting) + ')'],
+      ['Voted With Dem', (x.voted_with_dem || 0) + ' (' + pct(x.voted_with_dem || 0, voting) + ')'],
+      ['Voted Against GOP', (x.voted_against_gop || 0) + ' (' + pct(x.voted_against_gop || 0, voting) + ')'],
+      ['Voted Against Dem', (x.voted_against_dem || 0) + ' (' + pct(x.voted_against_dem || 0, voting) + ')'],
+      ['Lone Wolf Votes', String(x.lone_wolf || 0)]
+    ];
+    var cards = els.kpis.querySelectorAll('.kpi-card');
+    for (var i = 0; i < cards.length; i++) {
+      var label = cards[i].querySelector('.kpi-label');
+      var value = cards[i].querySelector('.kpi-value');
+      if (i < kpis.length) {
+        label.textContent = kpis[i][0];
+        value.textContent = kpis[i][1];
+      } else {
+        label.textContent = '';
+        value.textContent = '';
+      }
+    }
+  }
+
+  function buildCharts() {
+    if (typeof Chart === 'undefined') return;
+    var common = { responsive: true, maintainAspectRatio: false };
+
+    charts.voteDist = new Chart(document.getElementById('chart-vote-dist'), {
+      type: 'bar',
+      data: { labels: [], datasets: [{ label: 'Member vote', data: [], backgroundColor: ['#27ae60', '#c0392b', '#f39c12', '#7f8c8d'] }] },
+      options: Object.assign({}, common, { plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true } } })
+    });
+
+    charts.alignment = new Chart(document.getElementById('chart-alignment'), {
+      type: 'doughnut',
+      data: { labels: ALIGN_KEYS.slice(), datasets: [{ data: [0, 0, 0, 0], backgroundColor: ALIGN_COLORS.slice() }] },
+      options: Object.assign({}, common, {
+        plugins: {
+          legend: { position: 'bottom' },
+          tooltip: {
+            callbacks: {
+              label: function (ctx) {
+                var dataArr = ctx.dataset.data || [];
+                var sum = 0;
+                for (var i = 0; i < dataArr.length; i++) sum += Number(dataArr[i]) || 0;
+                var v = Number(ctx.parsed) || 0;
+                var p = sum > 0 ? ((v / sum) * 100).toFixed(1) + '%' : '0%';
+                return String(ctx.label) + ': ' + v + ' (' + p + ')';
+              }
+            }
+          }
+        }
+      })
+    });
+
+    charts.blocked = new Chart(document.getElementById('chart-blocked'), {
+      type: 'bar',
+      data: {
+        labels: ['Dem-Backed', 'GOP-Backed'],
+        datasets: [{ label: 'Blocked', data: [0, 0], backgroundColor: [COLORS.helpedD, COLORS.helpedR] }]
+      },
+      options: Object.assign({}, common, { indexAxis: 'y', plugins: { legend: { display: false } }, scales: { x: { beginAtZero: true } } })
+    });
+
+    charts.alignmentTime = new Chart(document.getElementById('chart-alignment-time'), {
+      type: 'line',
+      data: {
+        labels: [],
+        datasets: ALIGN_KEYS.map(function (k, i) {
+          return {
+            label: k, data: [], borderColor: ALIGN_COLORS[i], backgroundColor: ALIGN_COLORS[i],
+            tension: 0.2, fill: false
+          };
+        })
+      },
+      options: Object.assign({}, common, { plugins: { legend: { position: 'bottom' } }, scales: { y: { beginAtZero: true } } })
+    });
+
+    charts.withAgainst = new Chart(document.getElementById('chart-with-against'), {
+      type: 'bar',
+      data: {
+        labels: ['GOP majority', 'Dem majority'],
+        datasets: [
+          { label: 'Voted With', data: [0, 0], backgroundColor: COLORS.green },
+          { label: 'Voted Against', data: [0, 0], backgroundColor: COLORS.red }
+        ]
+      },
+      options: Object.assign({}, common, { plugins: { legend: { position: 'bottom' } }, scales: { y: { beginAtZero: true } } })
+    });
+  }
+
+  function updateCharts(member) {
+    if (typeof Chart === 'undefined') return;
+    var x = member.metrics || {};
+
+    var mv = x.member || {};
+    var mvKeys = ['Yea', 'Nay', 'Present', 'Not Voting'];
+    charts.voteDist.data.labels = mvKeys;
+    charts.voteDist.data.datasets[0].data = mvKeys.map(function (k) { return Number(mv[k]) || 0; });
+    charts.voteDist.update('none');
+
+    var al = x.alignment || {};
+    charts.alignment.data.datasets[0].data = ALIGN_KEYS.map(function (k) { return Number(al[k]) || 0; });
+    charts.alignment.update('none');
+
+    charts.blocked.data.datasets[0].data = [Number(x.blocked_dem_count) || 0, Number(x.blocked_rep_count) || 0];
+    charts.blocked.update('none');
+
+    var months = Array.isArray(x.months) ? x.months : [];
+    var monthly = x.monthly || {};
+    charts.alignmentTime.data.labels = months.slice();
+    for (var i = 0; i < ALIGN_KEYS.length; i++) {
+      var arr = monthly[ALIGN_KEYS[i]];
+      charts.alignmentTime.data.datasets[i].data = Array.isArray(arr) ? arr.slice() : months.map(function () { return 0; });
+    }
+    charts.alignmentTime.update('none');
+
+    charts.withAgainst.data.datasets[0].data = [Number(x.voted_with_gop) || 0, Number(x.voted_with_dem) || 0];
+    charts.withAgainst.data.datasets[1].data = [Number(x.voted_against_gop) || 0, Number(x.voted_against_dem) || 0];
+    charts.withAgainst.update('none');
+  }
+
+  // ---- Table ----
+
+  var COL_DEFS = [
+    { key: 'y', numeric: true },
+    { key: 'r', numeric: true },
+    { key: 'd', numeric: false },
+    { key: 'ln', numeric: false },
+    { key: 'q', numeric: false },
+    { key: 'ds', numeric: false },
+    { key: 'rs', numeric: false },
+    { key: 'm', numeric: false },
+    { key: 'ry_rn', numeric: false },
+    { key: 'dy_dn', numeric: false },
+    { key: 'a', numeric: false },
+    { key: 'b', numeric: false }
+  ];
+
+  function initSortHeaders() {
+    var ths = els.tableHead.querySelectorAll('th');
+    for (var i = 0; i < ths.length; i++) {
+      ths[i].style.cursor = 'pointer';
+      ths[i].setAttribute('data-col-index', String(i));
+      ths[i].addEventListener('click', onSortClick);
+    }
+  }
+
+  function onSortClick(e) {
+    var idx = Number(e.currentTarget.getAttribute('data-col-index'));
+    if (isNaN(idx)) return;
+    if (state.sortCol === idx) {
+      state.sortAsc = !state.sortAsc;
+    } else {
+      state.sortCol = idx;
+      state.sortAsc = true;
+    }
+    updateSortIndicators();
+    if (state.currentMember) renderTable(state.currentMember.metrics.rows || []);
+  }
+
+  function updateSortIndicators() {
+    var ths = els.tableHead.querySelectorAll('th');
+    for (var i = 0; i < ths.length; i++) {
+      var base = ths[i].textContent.replace(/[▲▼]\s*$/, '').trim();
+      ths[i].textContent = base + (i === state.sortCol ? (state.sortAsc ? ' ▲' : ' ▼') : '');
+    }
+  }
+
+  function renderTable(rows) {
+    var sorted = rows.slice();
+    if (state.sortCol != null && state.sortCol >= 0 && state.sortCol < COL_DEFS.length) {
+      var col = COL_DEFS[state.sortCol];
+      var dir = state.sortAsc ? 1 : -1;
+      sorted.sort(function (a, b) {
+        var va, vb;
+        if (col.key === 'ry_rn') { va = (a.ry || 0) - (a.rn || 0); vb = (b.ry || 0) - (b.rn || 0); }
+        else if (col.key === 'dy_dn') { va = (a.dy || 0) - (a.dn || 0); vb = (b.dy || 0) - (b.dn || 0); }
+        else { va = a[col.key]; vb = b[col.key]; }
+        if (col.numeric) { va = Number(va) || 0; vb = Number(vb) || 0; return (va - vb) * dir; }
+        va = String(va == null ? '' : va);
+        vb = String(vb == null ? '' : vb);
+        return va.localeCompare(vb) * dir;
+      });
+    }
+    els.tableBody.replaceChildren();
+    var frag = document.createDocumentFragment();
+    for (var i = 0; i < sorted.length; i++) {
+      var r = sorted[i];
+      var tr = document.createElement('tr');
+      appendCell(tr, r.y);
+      appendCell(tr, r.r);
+      appendCell(tr, r.d);
+      appendCell(tr, r.ln);
+      appendCell(tr, r.q);
+      appendCell(tr, r.ds);
+      appendCell(tr, r.rs);
+      appendCell(tr, r.m);
+      appendCell(tr, (r.ry || 0) + ' / ' + (r.rn || 0));
+      appendCell(tr, (r.dy || 0) + ' / ' + (r.dn || 0));
+      appendCell(tr, r.a);
+      appendCell(tr, r.b);
+      frag.appendChild(tr);
+    }
+    els.tableBody.appendChild(frag);
+  }
+
+  function appendCell(tr, val) {
+    var td = document.createElement('td');
+    td.textContent = val == null ? '' : String(val);
+    tr.appendChild(td);
+  }
+
+  // ---- URL / history ----
+
+  function buildShareUrl(id) {
+    var params = new URLSearchParams();
+    if (id) params.set('id', id);
+    var q = (els.search.value || '').trim();
+    if (q) params.set('q', q);
+
+    var parties = getCheckedValues(els.filterParty);
+    if (parties.length > 0 && parties.length < 3) params.set('p', parties.join(','));
+    var chambers = getCheckedValues(els.filterChamber);
+    if (chambers.length > 0 && chambers.length < 2) params.set('c', chambers.join(','));
+    var statesSel = getSelectedStates();
+    if (statesSel.length > 0 && statesSel.length < els.filterState.options.length) {
+      params.set('s', statesSel.join(','));
+    }
+    var s = params.toString();
+    return s ? ('?' + s) : window.location.pathname;
+  }
+
+  function debounceUrlReplace() {
+    if (state.urlReplaceTimer) clearTimeout(state.urlReplaceTimer);
+    // Debounce to avoid hammering history during keystrokes.
+    state.urlReplaceTimer = setTimeout(function () {
+      var u = buildShareUrl(state.currentId);
+      history.replaceState({ id: state.currentId }, '', u);
+    }, 250);
+  }
+
+  function restoreFiltersFromURL() {
+    var params = new URLSearchParams(window.location.search);
+    var p = params.get('p');
+    var c = params.get('c');
+    var s = params.get('s');
+    var q = params.get('q');
+    if (p) {
+      var allowed = p.split(',');
+      els.filterParty.querySelectorAll('input[type=checkbox]').forEach(function (i) {
+        i.checked = allowed.indexOf(i.value) >= 0;
+      });
+    }
+    if (c) {
+      var allowedC = c.split(',');
+      els.filterChamber.querySelectorAll('input[type=checkbox]').forEach(function (i) {
+        i.checked = allowedC.indexOf(i.value) >= 0;
+      });
+    }
+    if (s) {
+      var allowedS = s.split(',');
+      for (var i = 0; i < els.filterState.options.length; i++) {
+        els.filterState.options[i].selected = allowedS.indexOf(els.filterState.options[i].value) >= 0;
+      }
+    }
+    if (q) els.search.value = q;
+  }
+
+  function onPopState(e) {
+    var params = new URLSearchParams(window.location.search);
+    var id = params.get('id');
+    if (id && isValidId(id)) {
+      state.skipPush = true;
+      selectMember(id);
+    }
+  }
+
+  function initialSelect() {
+    var params = new URLSearchParams(window.location.search);
+    var urlId = params.get('id');
+    if (urlId && isValidId(urlId)) {
+      state.skipPush = true;
+      selectMember(urlId);
+      return;
+    }
+    var stored = null;
+    try { stored = localStorage.getItem(LS_KEY); } catch (_) {}
+    if (stored && isValidId(stored)) {
+      state.skipPush = true;
+      selectMember(stored);
+      return;
+    }
+    // No selection — leave summary empty with a hint.
+    els.summary.replaceChildren();
+    var p = document.createElement('p');
+    p.style.color = 'var(--ps-text-muted)';
+    p.textContent = 'Pick a member to begin — type a name, set of initials (e.g., AOC), or use the filters at left.';
+    els.summary.appendChild(p);
+  }
+
+})();

+ 110 - 0
template/compare.html

@@ -0,0 +1,110 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Compare Members — 119th Congress</title>
+<link rel="stylesheet" href="app.css">
+<script src="vendor/chart.umd.min.js" defer></script>
+<script src="compare.js" defer></script>
+</head>
+<body>
+<noscript>This dashboard requires JavaScript.</noscript>
+
+<div id="polisci-root" data-base="./data/">
+  <!-- Header -->
+  <header class="ps-header">
+    <h1>Compare Members — 119th Congress</h1>
+    <nav class="ps-nav"><a href="app.html">Member</a> · <a href="compare.html" aria-current="page">Compare</a> · <a href="ranking.html">Rankings</a></nav>
+    <p class="ps-subtitle">Overlay roll-call voting metrics for up to six members of the 119th Congress.</p>
+  </header>
+
+  <div class="ps-layout">
+    <!-- Sidebar -->
+    <aside class="sidebar" id="sidebar">
+      <button type="button" class="sidebar-toggle" id="sidebar-toggle" aria-expanded="true" aria-controls="sidebar-body">Filters</button>
+      <div class="sidebar-body" id="sidebar-body">
+        <fieldset class="filter-group" id="filter-chamber">
+          <legend>Chamber</legend>
+          <label><input type="checkbox" name="chamber" value="H" checked> House</label>
+          <label><input type="checkbox" name="chamber" value="S" checked> Senate</label>
+        </fieldset>
+
+        <fieldset class="filter-group" id="filter-party">
+          <legend>Party</legend>
+          <label><input type="checkbox" name="party" value="R" checked> Republican</label>
+          <label><input type="checkbox" name="party" value="D" checked> Democrat</label>
+          <label><input type="checkbox" name="party" value="I" checked> Independent</label>
+        </fieldset>
+
+        <fieldset class="filter-group" id="filter-state">
+          <legend>State</legend>
+          <select multiple id="filter-state-select" size="8" aria-label="State filter"></select>
+        </fieldset>
+
+        <button type="button" id="filter-reset" class="btn-reset">Reset filters</button>
+        <p class="hint">Filters apply to the member search.</p>
+      </div>
+    </aside>
+
+    <!-- Main -->
+    <main class="ps-main">
+      <div class="search-wrap">
+        <label for="member-search" class="visually-hidden">Search members</label>
+        <input type="search" id="member-search" placeholder="Search by name or initials (e.g., AOC)…" autocomplete="off" role="combobox" aria-controls="member-results" aria-expanded="false">
+        <ul id="member-results" role="listbox" class="member-results is-hidden"></ul>
+      </div>
+
+      <section class="compare-pills-section">
+        <h2 class="compare-pills-title">Selected members</h2>
+        <ul id="selected-pills" class="selected-pills" aria-label="Selected members"></ul>
+        <p id="compare-empty-hint" class="hint">Pick up to 6 members to compare. Click a pill to open the member's full dashboard in a new tab.</p>
+      </section>
+
+      <div class="align-switcher-wrap">
+        <label for="align-class-switcher">Alignment class:</label>
+        <select id="align-class-switcher">
+          <option value="Helped Republicans" selected>Helped Republicans</option>
+          <option value="Helped Democrats">Helped Democrats</option>
+          <option value="Helped Both">Helped Both</option>
+          <option value="Helped Neither">Helped Neither</option>
+        </select>
+      </div>
+
+      <!-- Charts grid -->
+      <section class="charts-grid">
+        <figure class="chart-frame chart-wide">
+          <figcaption>Alignment over time (selected class)</figcaption>
+          <div class="chart-canvas-wrap"><canvas id="cmp-chart-alignment-time"></canvas></div>
+        </figure>
+        <figure class="chart-frame chart-wide">
+          <figcaption>Voted against own party — monthly (proxy: "Helped Neither" rate; see NOTES.md item 6)</figcaption>
+          <div class="chart-canvas-wrap"><canvas id="cmp-chart-against-own"></canvas></div>
+        </figure>
+        <figure class="chart-frame chart-wide">
+          <figcaption>KPI comparison</figcaption>
+          <div class="chart-canvas-wrap"><canvas id="cmp-chart-kpi"></canvas></div>
+        </figure>
+        <figure class="chart-frame">
+          <figcaption>Defection scatter (% against GOP vs % against Dem; color = party)</figcaption>
+          <div class="chart-canvas-wrap"><canvas id="cmp-chart-defection"></canvas></div>
+        </figure>
+        <figure class="chart-frame">
+          <figcaption>Vote distribution</figcaption>
+          <div class="chart-canvas-wrap"><canvas id="cmp-chart-vote-dist"></canvas></div>
+        </figure>
+      </section>
+
+      <footer class="ps-footer">
+        <p>Source data:
+          <a href="https://clerk.house.gov/Votes" target="_blank" rel="noopener noreferrer">House Clerk roll-call XML</a> ·
+          <a href="https://www.senate.gov/legislative/votes_new.htm" target="_blank" rel="noopener noreferrer">Senate roll-call XML</a> ·
+          <a href="Methodology.md" target="_blank" rel="noopener">Methodology</a>
+        </p>
+        <p>Last regenerated: <span id="last-generated">(populated)</span></p>
+      </footer>
+    </main>
+  </div>
+</div>
+</body>
+</html>

+ 689 - 0
template/compare.js

@@ -0,0 +1,689 @@
+(function () {
+  'use strict';
+
+  // Security: validate any member id before fetch / DOM use.
+  var ID_RE = /^[A-Z]\d{6}$|^S\d{3,4}$/;
+  var MAX_SELECTED = 6;
+  var PARTY_COLORS = { R: '#d9534f', D: '#337ab7', I: '#5cb85c' };
+  var PALETTE = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b'];
+  var ALIGN_KEYS = ['Helped Republicans', 'Helped Democrats', 'Helped Both', 'Helped Neither'];
+
+  var state = {
+    manifest: null,
+    membersById: {},
+    BASE: './data/',
+    selectedIds: [],
+    memberCache: {},
+    colorById: {},
+    currentAlignClass: 'Helped Republicans',
+    skipPush: false
+  };
+
+  var els = {};
+  var charts = {};
+
+  document.addEventListener('DOMContentLoaded', init);
+
+  function init() {
+    var root = document.getElementById('polisci-root');
+    if (!root) return;
+    state.BASE = root.dataset.base || './data/';
+
+    els.root = root;
+    els.search = document.getElementById('member-search');
+    els.results = document.getElementById('member-results');
+    els.pills = document.getElementById('selected-pills');
+    els.emptyHint = document.getElementById('compare-empty-hint');
+    els.lastGen = document.getElementById('last-generated');
+    els.filterChamber = document.getElementById('filter-chamber');
+    els.filterParty = document.getElementById('filter-party');
+    els.filterState = document.getElementById('filter-state-select');
+    els.filterReset = document.getElementById('filter-reset');
+    els.sidebarToggle = document.getElementById('sidebar-toggle');
+    els.sidebar = document.getElementById('sidebar');
+    els.alignSwitcher = document.getElementById('align-class-switcher');
+
+    var inline = document.getElementById('polisci-manifest');
+    if (inline && inline.textContent) {
+      try { onManifest(JSON.parse(inline.textContent)); return; }
+      catch (e) { /* fall through */ }
+    }
+    fetch(state.BASE + 'manifest.json', { credentials: 'omit' })
+      .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
+      .then(onManifest)
+      .catch(function (e) { showFatal('Failed to load manifest: ' + e.message + ' — if you opened this file directly, serve it over HTTP.'); });
+  }
+
+  function showFatal(msg) {
+    els.root.replaceChildren();
+    var p = document.createElement('p');
+    p.style.padding = '20px';
+    p.style.color = '#c0392b';
+    p.textContent = msg;
+    els.root.appendChild(p);
+  }
+
+  function onManifest(m) {
+    state.manifest = m;
+    var members = m.members || [];
+    for (var i = 0; i < members.length; i++) {
+      state.membersById[members[i].id] = members[i];
+    }
+    els.lastGen.textContent = m.generated_at || '';
+
+    populateStateOptions(members);
+    wireEvents();
+    buildCharts();
+    initFromURL();
+    window.addEventListener('popstate', onPopState);
+  }
+
+  function populateStateOptions(members) {
+    var states = {};
+    for (var i = 0; i < members.length; i++) states[members[i].s] = true;
+    var keys = Object.keys(states).sort();
+    els.filterState.replaceChildren();
+    for (var j = 0; j < keys.length; j++) {
+      var opt = document.createElement('option');
+      opt.value = keys[j];
+      opt.textContent = keys[j];
+      opt.selected = true;
+      els.filterState.appendChild(opt);
+    }
+  }
+
+  function wireEvents() {
+    var chamberInputs = els.filterChamber.querySelectorAll('input[type=checkbox]');
+    var partyInputs = els.filterParty.querySelectorAll('input[type=checkbox]');
+    chamberInputs.forEach(function (i) { i.addEventListener('change', refreshFilter); });
+    partyInputs.forEach(function (i) { i.addEventListener('change', refreshFilter); });
+    els.filterState.addEventListener('change', refreshFilter);
+    els.filterReset.addEventListener('click', resetFilters);
+
+    els.search.addEventListener('input', refreshFilter);
+    els.search.addEventListener('focus', refreshFilter);
+    document.addEventListener('click', function (e) {
+      if (!els.results.contains(e.target) && e.target !== els.search) {
+        els.results.classList.add('is-hidden');
+        els.search.setAttribute('aria-expanded', 'false');
+      }
+    });
+
+    if (els.sidebarToggle) {
+      els.sidebarToggle.addEventListener('click', function () {
+        var open = els.sidebar.classList.toggle('is-open');
+        els.sidebarToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
+      });
+    }
+
+    els.alignSwitcher.addEventListener('change', function () {
+      state.currentAlignClass = els.alignSwitcher.value;
+      renderAlignmentTime();
+    });
+  }
+
+  function resetFilters() {
+    var inputs = els.filterChamber.querySelectorAll('input[type=checkbox]');
+    inputs.forEach(function (i) { i.checked = true; });
+    inputs = els.filterParty.querySelectorAll('input[type=checkbox]');
+    inputs.forEach(function (i) { i.checked = true; });
+    for (var j = 0; j < els.filterState.options.length; j++) {
+      els.filterState.options[j].selected = true;
+    }
+    els.search.value = '';
+    refreshFilter();
+  }
+
+  function getCheckedValues(group) {
+    var out = [];
+    group.querySelectorAll('input[type=checkbox]:checked').forEach(function (i) { out.push(i.value); });
+    return out;
+  }
+
+  function getSelectedStates() {
+    var out = [];
+    for (var i = 0; i < els.filterState.options.length; i++) {
+      if (els.filterState.options[i].selected) out.push(els.filterState.options[i].value);
+    }
+    return out;
+  }
+
+  function initialsOf(name) {
+    var parts = name.split(/\s+/);
+    var out = '';
+    for (var i = 0; i < parts.length; i++) {
+      if (parts[i]) out += parts[i].charAt(0).toUpperCase();
+    }
+    return out;
+  }
+
+  function refreshFilter() {
+    var chambers = getCheckedValues(els.filterChamber);
+    var parties = getCheckedValues(els.filterParty);
+    var states = getSelectedStates();
+    var q = (els.search.value || '').trim();
+    var qLower = q.toLowerCase();
+    var qUpper = q.toUpperCase();
+    var initialsMode = q.length > 0 && q.length <= 4 && /^[A-Za-z]+$/.test(q);
+
+    var members = state.manifest.members;
+    var matches = [];
+    for (var i = 0; i < members.length && matches.length < 50; i++) {
+      var m = members[i];
+      if (chambers.indexOf(m.c) < 0) continue;
+      if (parties.indexOf(m.p) < 0) continue;
+      if (states.indexOf(m.s) < 0) continue;
+      if (q) {
+        var nameLower = m.n.toLowerCase();
+        var hit = nameLower.indexOf(qLower) >= 0;
+        if (!hit && initialsMode) {
+          if (initialsOf(m.n) === qUpper) hit = true;
+        }
+        if (!hit) continue;
+      }
+      matches.push(m);
+    }
+    matches.sort(function (a, b) { return a.n.localeCompare(b.n); });
+    renderResults(matches, q);
+  }
+
+  function renderResults(matches, q) {
+    els.results.replaceChildren();
+    if (!q && matches.length === state.manifest.members.length) {
+      els.results.classList.add('is-hidden');
+      els.search.setAttribute('aria-expanded', 'false');
+      return;
+    }
+    if (!matches.length) {
+      var li0 = document.createElement('li');
+      li0.textContent = 'No members match.';
+      li0.setAttribute('aria-disabled', 'true');
+      els.results.appendChild(li0);
+    } else {
+      for (var i = 0; i < matches.length; i++) {
+        var m = matches[i];
+        var li = document.createElement('li');
+        li.setAttribute('role', 'option');
+        li.setAttribute('data-id', m.id);
+        var alreadyIn = state.selectedIds.indexOf(m.id) >= 0;
+        var atCap = state.selectedIds.length >= MAX_SELECTED;
+        li.textContent = m.n + ' (' + m.p + '-' + m.s + ', ' + (m.c === 'H' ? 'House' : 'Senate') + ')' +
+          (alreadyIn ? ' — selected' : (atCap ? ' — max 6' : ''));
+        if (alreadyIn || atCap) {
+          li.setAttribute('aria-disabled', 'true');
+        } else {
+          li.addEventListener('click', onResultClick);
+        }
+        els.results.appendChild(li);
+      }
+    }
+    els.results.classList.remove('is-hidden');
+    els.search.setAttribute('aria-expanded', 'true');
+  }
+
+  function onResultClick(e) {
+    var id = e.currentTarget.getAttribute('data-id');
+    if (!isValidId(id)) return;
+    els.results.classList.add('is-hidden');
+    els.search.setAttribute('aria-expanded', 'false');
+    els.search.value = '';
+    addMember(id);
+  }
+
+  function isValidId(id) {
+    return typeof id === 'string' && ID_RE.test(id) &&
+      Object.prototype.hasOwnProperty.call(state.membersById, id);
+  }
+
+  function pickColor() {
+    var used = {};
+    for (var k in state.colorById) {
+      if (Object.prototype.hasOwnProperty.call(state.colorById, k)) used[state.colorById[k]] = true;
+    }
+    for (var i = 0; i < PALETTE.length; i++) {
+      if (!used[PALETTE[i]]) return PALETTE[i];
+    }
+    return PALETTE[0];
+  }
+
+  function addMember(id) {
+    if (!isValidId(id)) return;
+    if (state.selectedIds.indexOf(id) >= 0) return;
+    if (state.selectedIds.length >= MAX_SELECTED) return;
+
+    var version = state.manifest.version || '';
+    var url = state.BASE + 'members/' + encodeURIComponent(id) + '.json?v=' + encodeURIComponent(version);
+    fetch(url, { credentials: 'omit' })
+      .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
+      .then(function (member) {
+        if (state.selectedIds.indexOf(id) >= 0) return;
+        if (state.selectedIds.length >= MAX_SELECTED) return;
+        state.memberCache[id] = member;
+        state.colorById[id] = pickColor();
+        state.selectedIds.push(id);
+        renderPills();
+        pushUrlState();
+        renderAllCharts();
+      })
+      .catch(function (e) {
+        // Silent for one member load failure — surface via empty hint area.
+        els.emptyHint.textContent = 'Failed to load member ' + id + ': ' + e.message;
+      });
+  }
+
+  function removeMember(id) {
+    var idx = state.selectedIds.indexOf(id);
+    if (idx < 0) return;
+    state.selectedIds.splice(idx, 1);
+    delete state.memberCache[id];
+    delete state.colorById[id];
+    renderPills();
+    pushUrlState();
+    renderAllCharts();
+  }
+
+  function renderPills() {
+    els.pills.replaceChildren();
+    for (var i = 0; i < state.selectedIds.length; i++) {
+      var id = state.selectedIds[i];
+      var member = state.memberCache[id];
+      if (!member) continue;
+      var color = state.colorById[id] || '#888';
+
+      var li = document.createElement('li');
+      li.className = 'compare-pill';
+      li.style.setProperty('--pill-color', color);
+      li.style.borderColor = color;
+      li.style.backgroundColor = color + '22';
+
+      var swatch = document.createElement('span');
+      swatch.className = 'compare-pill-swatch';
+      swatch.style.backgroundColor = color;
+      swatch.setAttribute('aria-hidden', 'true');
+      li.appendChild(swatch);
+
+      var nameBtn = document.createElement('button');
+      nameBtn.type = 'button';
+      nameBtn.className = 'compare-pill-name';
+      nameBtn.textContent = member.name + ' (' + member.party + '-' + member.state + ')';
+      nameBtn.title = 'Open ' + member.name + ' dashboard in new tab';
+      (function (mid) {
+        nameBtn.addEventListener('click', function () {
+          window.open('app.html?id=' + encodeURIComponent(mid), '_blank', 'noopener');
+        });
+      })(id);
+      li.appendChild(nameBtn);
+
+      var rm = document.createElement('button');
+      rm.type = 'button';
+      rm.className = 'compare-pill-remove';
+      rm.setAttribute('aria-label', 'Remove ' + member.name);
+      rm.textContent = '×';
+      (function (mid) {
+        rm.addEventListener('click', function () { removeMember(mid); });
+      })(id);
+      li.appendChild(rm);
+
+      els.pills.appendChild(li);
+    }
+
+    if (state.selectedIds.length === 0) {
+      els.emptyHint.textContent = 'Pick up to 6 members to compare. Click a pill to open the member’s full dashboard in a new tab.';
+    } else {
+      els.emptyHint.textContent = state.selectedIds.length + ' of ' + MAX_SELECTED +
+        ' selected. Click a pill name to open that member’s full dashboard in a new tab.';
+    }
+  }
+
+  // ---- Charts ----
+
+  function buildCharts() {
+    if (typeof Chart === 'undefined') return;
+    var common = { responsive: true, maintainAspectRatio: false };
+
+    charts.alignmentTime = new Chart(document.getElementById('cmp-chart-alignment-time'), {
+      type: 'line',
+      data: { labels: [], datasets: [] },
+      options: Object.assign({}, common, {
+        plugins: { legend: { position: 'bottom' } },
+        scales: { y: { beginAtZero: true } }
+      })
+    });
+
+    charts.againstOwn = new Chart(document.getElementById('cmp-chart-against-own'), {
+      type: 'line',
+      data: { labels: [], datasets: [] },
+      options: Object.assign({}, common, {
+        plugins: { legend: { position: 'bottom' } },
+        scales: { y: { beginAtZero: true } }
+      })
+    });
+
+    charts.kpi = new Chart(document.getElementById('cmp-chart-kpi'), {
+      type: 'bar',
+      data: {
+        labels: ['% against GOP', '% against Dem', 'Lone Wolf %', 'Participation %', 'Blocked Dem', 'Blocked GOP'],
+        datasets: []
+      },
+      options: Object.assign({}, common, {
+        plugins: {
+          legend: { position: 'bottom' },
+          tooltip: {
+            callbacks: {
+              label: function (ctx) {
+                var v = Number(ctx.parsed.y);
+                if (isNaN(v)) v = Number(ctx.parsed) || 0;
+                return String(ctx.dataset.label) + ': ' + v.toFixed(1);
+              }
+            }
+          }
+        },
+        scales: { y: { beginAtZero: true } }
+      })
+    });
+
+    charts.defection = new Chart(document.getElementById('cmp-chart-defection'), {
+      type: 'scatter',
+      data: { datasets: [] },
+      options: Object.assign({}, common, {
+        plugins: {
+          legend: { display: false },
+          tooltip: {
+            callbacks: {
+              label: function (ctx) {
+                var p = ctx.raw || {};
+                return String(ctx.dataset.label) + ': (' +
+                  (Number(p.x) || 0).toFixed(1) + '% R, ' +
+                  (Number(p.y) || 0).toFixed(1) + '% D)';
+              }
+            }
+          }
+        },
+        scales: {
+          x: { beginAtZero: true, title: { display: true, text: '% against GOP majority' } },
+          y: { beginAtZero: true, title: { display: true, text: '% against Dem majority' } }
+        }
+      })
+    });
+
+    charts.voteDist = new Chart(document.getElementById('cmp-chart-vote-dist'), {
+      type: 'bar',
+      data: { labels: ['Yea', 'Nay', 'Present', 'Not Voting'], datasets: [] },
+      options: Object.assign({}, common, {
+        plugins: { legend: { position: 'bottom' } },
+        scales: { y: { beginAtZero: true } }
+      })
+    });
+  }
+
+  function renderAllCharts() {
+    renderAlignmentTime();
+    renderAgainstOwn();
+    renderKpi();
+    renderDefection();
+    renderVoteDist();
+  }
+
+  function unionMonths() {
+    var set = {};
+    for (var i = 0; i < state.selectedIds.length; i++) {
+      var m = state.memberCache[state.selectedIds[i]];
+      var months = m && m.metrics && Array.isArray(m.metrics.months) ? m.metrics.months : [];
+      for (var j = 0; j < months.length; j++) set[months[j]] = true;
+    }
+    return Object.keys(set).sort();
+  }
+
+  function replaceArray(target, source) {
+    target.length = 0;
+    for (var i = 0; i < source.length; i++) target.push(source[i]);
+  }
+
+  function monthlySeries(member, alignClass, months) {
+    var mMonths = (member.metrics && member.metrics.months) || [];
+    var arr = (member.metrics && member.metrics.monthly && member.metrics.monthly[alignClass]) || [];
+    var idx = {};
+    for (var i = 0; i < mMonths.length; i++) idx[mMonths[i]] = i;
+    return months.map(function (mo) {
+      var k = idx[mo];
+      return (k != null && arr[k] != null) ? Number(arr[k]) : 0;
+    });
+  }
+
+  function renderAlignmentTime() {
+    if (!charts.alignmentTime) return;
+    var months = unionMonths();
+    replaceArray(charts.alignmentTime.data.labels, months);
+    var datasets = [];
+    for (var i = 0; i < state.selectedIds.length; i++) {
+      var id = state.selectedIds[i];
+      var member = state.memberCache[id];
+      if (!member) continue;
+      var color = state.colorById[id];
+      datasets.push({
+        label: member.name,
+        data: monthlySeries(member, state.currentAlignClass, months),
+        borderColor: color,
+        backgroundColor: color + '33',
+        tension: 0.2,
+        fill: false
+      });
+    }
+    replaceArray(charts.alignmentTime.data.datasets, datasets);
+    charts.alignmentTime.update('none');
+  }
+
+  // "Voted against own party" per month is NOT directly emitted by analyze.py.
+  // We use monthly['Helped Neither'] as a proxy; caveat noted in NOTES.md item 6.
+  function renderAgainstOwn() {
+    if (!charts.againstOwn) return;
+    var months = unionMonths();
+    replaceArray(charts.againstOwn.data.labels, months);
+    var datasets = [];
+    for (var i = 0; i < state.selectedIds.length; i++) {
+      var id = state.selectedIds[i];
+      var member = state.memberCache[id];
+      if (!member) continue;
+      var color = state.colorById[id];
+      datasets.push({
+        label: member.name,
+        data: monthlySeries(member, 'Helped Neither', months),
+        borderColor: color,
+        backgroundColor: color + '33',
+        tension: 0.2,
+        fill: false
+      });
+    }
+    replaceArray(charts.againstOwn.data.datasets, datasets);
+    charts.againstOwn.update('none');
+  }
+
+  function kpiValues(member) {
+    var x = (member && member.metrics) || {};
+    var voting = Number(x.voting) || 0;
+    var total = Number(x.total) || 0;
+    var againstGop = Number(x.voted_against_gop) || 0;
+    var withGop = Number(x.voted_with_gop) || 0;
+    var againstDem = Number(x.voted_against_dem) || 0;
+    var withDem = Number(x.voted_with_dem) || 0;
+    var lone = Number(x.lone_wolf) || 0;
+    var denomGop = againstGop + withGop;
+    var denomDem = againstDem + withDem;
+    return [
+      denomGop > 0 ? (100 * againstGop / denomGop) : 0,
+      denomDem > 0 ? (100 * againstDem / denomDem) : 0,
+      voting > 0 ? (100 * lone / voting) : 0,
+      total > 0 ? (100 * voting / total) : 0,
+      Number(x.blocked_dem_count) || 0,
+      Number(x.blocked_rep_count) || 0
+    ];
+  }
+
+  function renderKpi() {
+    if (!charts.kpi) return;
+    var datasets = [];
+    for (var i = 0; i < state.selectedIds.length; i++) {
+      var id = state.selectedIds[i];
+      var member = state.memberCache[id];
+      if (!member) continue;
+      var color = state.colorById[id];
+      datasets.push({
+        label: member.name,
+        data: kpiValues(member),
+        backgroundColor: color,
+        borderColor: color
+      });
+    }
+    replaceArray(charts.kpi.data.datasets, datasets);
+    charts.kpi.update('none');
+  }
+
+  function renderDefection() {
+    if (!charts.defection) return;
+    var datasets = [];
+    for (var i = 0; i < state.selectedIds.length; i++) {
+      var id = state.selectedIds[i];
+      var member = state.memberCache[id];
+      if (!member) continue;
+      var vals = kpiValues(member);
+      var partyColor = PARTY_COLORS[member.party] || '#888';
+      datasets.push({
+        label: member.name,
+        data: [{ x: vals[0], y: vals[1] }],
+        backgroundColor: partyColor,
+        borderColor: partyColor,
+        pointBackgroundColor: partyColor,
+        pointRadius: 8,
+        pointHoverRadius: 10
+      });
+    }
+    replaceArray(charts.defection.data.datasets, datasets);
+    charts.defection.update('none');
+  }
+
+  function voteDistValues(member) {
+    var mv = (member && member.metrics && member.metrics.member) || {};
+    // Some procedural votes use Aye/No instead of Yea/Nay — sum them for display.
+    var yea = (Number(mv.Yea) || 0) + (Number(mv.Aye) || 0);
+    var nay = (Number(mv.Nay) || 0) + (Number(mv.No) || 0);
+    var present = Number(mv.Present) || 0;
+    var nv = Number(mv['Not Voting']) || 0;
+    return [yea, nay, present, nv];
+  }
+
+  function renderVoteDist() {
+    if (!charts.voteDist) return;
+    var datasets = [];
+    for (var i = 0; i < state.selectedIds.length; i++) {
+      var id = state.selectedIds[i];
+      var member = state.memberCache[id];
+      if (!member) continue;
+      var color = state.colorById[id];
+      datasets.push({
+        label: member.name,
+        data: voteDistValues(member),
+        backgroundColor: color,
+        borderColor: color
+      });
+    }
+    replaceArray(charts.voteDist.data.datasets, datasets);
+    charts.voteDist.update('none');
+  }
+
+  // ---- URL state ----
+
+  function parseIdsParam() {
+    var params = new URLSearchParams(window.location.search);
+    var raw = params.get('ids');
+    if (!raw) return [];
+    var parts = raw.split(',');
+    var out = [];
+    for (var i = 0; i < parts.length && out.length < MAX_SELECTED; i++) {
+      var id = parts[i];
+      if (isValidId(id) && out.indexOf(id) < 0) out.push(id);
+    }
+    return out;
+  }
+
+  function pushUrlState() {
+    if (state.skipPush) { state.skipPush = false; return; }
+    var u;
+    if (state.selectedIds.length === 0) {
+      u = window.location.pathname;
+    } else {
+      u = '?ids=' + state.selectedIds.join(',');
+    }
+    history.pushState({ ids: state.selectedIds.slice() }, '', u);
+  }
+
+  function initFromURL() {
+    refreshFilter();
+    var ids = parseIdsParam();
+    if (ids.length === 0) {
+      renderPills();
+      renderAllCharts();
+      return;
+    }
+    // Sequentially add (each fetch independent; order preserved via URL parse).
+    state.skipPush = true;
+    var pending = ids.slice();
+    var loaded = [];
+    function next() {
+      if (pending.length === 0) {
+        renderPills();
+        renderAllCharts();
+        return;
+      }
+      var id = pending.shift();
+      var version = state.manifest.version || '';
+      var url = state.BASE + 'members/' + encodeURIComponent(id) + '.json?v=' + encodeURIComponent(version);
+      fetch(url, { credentials: 'omit' })
+        .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
+        .then(function (member) {
+          state.memberCache[id] = member;
+          state.colorById[id] = pickColor();
+          state.selectedIds.push(id);
+          loaded.push(id);
+        })
+        .catch(function () { /* skip broken id */ })
+        .then(next);
+    }
+    next();
+  }
+
+  function onPopState() {
+    var ids = parseIdsParam();
+    // Reset selection and reload from URL.
+    state.selectedIds = [];
+    state.memberCache = {};
+    state.colorById = {};
+    state.skipPush = true;
+    if (ids.length === 0) {
+      renderPills();
+      renderAllCharts();
+      return;
+    }
+    var pending = ids.slice();
+    function next() {
+      if (pending.length === 0) {
+        renderPills();
+        renderAllCharts();
+        return;
+      }
+      var id = pending.shift();
+      var version = state.manifest.version || '';
+      var url = state.BASE + 'members/' + encodeURIComponent(id) + '.json?v=' + encodeURIComponent(version);
+      fetch(url, { credentials: 'omit' })
+        .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
+        .then(function (member) {
+          state.memberCache[id] = member;
+          state.colorById[id] = pickColor();
+          state.selectedIds.push(id);
+        })
+        .catch(function () {})
+        .then(next);
+    }
+    next();
+  }
+
+})();

+ 101 - 0
template/ranking.html

@@ -0,0 +1,101 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>119th Congress — Member Rankings</title>
+<link rel="stylesheet" href="app.css">
+<script src="ranking.js" defer></script>
+</head>
+<body>
+<noscript>This dashboard requires JavaScript.</noscript>
+
+<div id="polisci-root" data-base="./data/">
+  <header class="ps-header">
+    <h1>119th Congress — Member Rankings</h1>
+    <nav class="ps-nav"><a href="app.html">Member</a> · <a href="compare.html">Compare</a> · <a href="ranking.html" aria-current="page">Rankings</a></nav>
+    <p class="ps-subtitle">Rank every House or Senate member of the 119th Congress by any of the dashboard metrics. Click a row to open that member's full dashboard.</p>
+  </header>
+
+  <div class="ps-layout">
+    <aside class="sidebar" id="sidebar">
+      <button type="button" class="sidebar-toggle" id="sidebar-toggle" aria-expanded="true" aria-controls="sidebar-body">Controls</button>
+      <div class="sidebar-body" id="sidebar-body">
+        <fieldset class="filter-group" id="filter-chamber">
+          <legend>Chamber</legend>
+          <label><input type="radio" name="chamber" value="H" checked> House</label>
+          <label><input type="radio" name="chamber" value="S"> Senate</label>
+        </fieldset>
+
+        <fieldset class="filter-group" id="filter-party">
+          <legend>Party</legend>
+          <label><input type="checkbox" name="party" value="R" checked> Republican</label>
+          <label><input type="checkbox" name="party" value="D" checked> Democrat</label>
+          <label><input type="checkbox" name="party" value="I" checked> Independent</label>
+        </fieldset>
+
+        <fieldset class="filter-group">
+          <legend>Metric</legend>
+          <label for="rank-metric" class="sr-only">Metric</label>
+          <select id="rank-metric">
+            <option value="total">Total Votes</option>
+            <option value="yeas">Yeas</option>
+            <option value="nays">Nays</option>
+            <option value="participation_pct">Participation %</option>
+            <option value="voted_with_gop">Voted With GOP (count)</option>
+            <option value="voted_with_gop_pct">Voted With GOP %</option>
+            <option value="voted_with_dem">Voted With Dem (count)</option>
+            <option value="voted_with_dem_pct">Voted With Dem %</option>
+            <option value="voted_against_gop">Voted Against GOP (count)</option>
+            <option value="voted_against_gop_pct">Voted Against GOP %</option>
+            <option value="voted_against_dem">Voted Against Dem (count)</option>
+            <option value="voted_against_dem_pct">Voted Against Dem %</option>
+            <option value="lone_wolf">Lone Wolf Votes</option>
+            <option value="lone_wolf_pct">Lone Wolf %</option>
+          </select>
+        </fieldset>
+
+        <fieldset class="filter-group">
+          <legend>Order</legend>
+          <label><input type="radio" name="order" value="desc" checked> Highest first</label>
+          <label><input type="radio" name="order" value="asc"> Lowest first</label>
+        </fieldset>
+
+        <button type="button" id="rank-reset" class="btn-reset">Reset</button>
+        <p class="filter-hint">% metrics are computed as a share of votes cast.</p>
+      </div>
+    </aside>
+
+    <main class="main-content">
+      <section class="rank-summary" id="rank-summary"></section>
+      <section class="vote-table-section">
+        <div class="table-scroll">
+          <table id="rank-table">
+            <thead>
+              <tr>
+                <th>Rank</th>
+                <th>Member</th>
+                <th>Party</th>
+                <th>State</th>
+                <th id="rank-metric-header">Metric</th>
+              </tr>
+            </thead>
+            <tbody></tbody>
+          </table>
+        </div>
+      </section>
+
+      <footer class="ps-footer">
+        <p>Rankings exclude members with zero votes cast (denominators undefined).</p>
+        <p>Source data:
+          <a href="https://clerk.house.gov/Votes" target="_blank" rel="noopener noreferrer">House Clerk roll-call XML</a> ·
+          <a href="https://www.senate.gov/legislative/votes_new.htm" target="_blank" rel="noopener noreferrer">Senate roll-call XML</a> ·
+          <a href="Methodology.md" target="_blank" rel="noopener">Methodology</a>
+        </p>
+        <p>Last regenerated: <span id="last-generated">(populated)</span></p>
+      </footer>
+    </main>
+  </div>
+</div>
+</body>
+</html>

+ 269 - 0
template/ranking.js

@@ -0,0 +1,269 @@
+(function () {
+  'use strict';
+
+  var ID_RE = /^[A-Z]\d{6}$|^S\d{3,4}$/;
+
+  var METRIC_LABELS = {
+    total: 'Total Votes',
+    yeas: 'Yeas',
+    nays: 'Nays',
+    participation_pct: 'Participation %',
+    voted_with_gop: 'Voted With GOP',
+    voted_with_gop_pct: 'Voted With GOP %',
+    voted_with_dem: 'Voted With Dem',
+    voted_with_dem_pct: 'Voted With Dem %',
+    voted_against_gop: 'Voted Against GOP',
+    voted_against_gop_pct: 'Voted Against GOP %',
+    voted_against_dem: 'Voted Against Dem',
+    voted_against_dem_pct: 'Voted Against Dem %',
+    lone_wolf: 'Lone Wolf Votes',
+    lone_wolf_pct: 'Lone Wolf %'
+  };
+
+  var state = {
+    BASE: './data/',
+    manifest: null,
+    chamber: 'H',
+    parties: { R: true, D: true, I: true },
+    metric: 'total',
+    order: 'desc'
+  };
+  var els = {};
+
+  document.addEventListener('DOMContentLoaded', init);
+
+  function init() {
+    els.root = document.getElementById('polisci-root');
+    state.BASE = els.root.dataset.base || './data/';
+    els.tbody = document.querySelector('#rank-table tbody');
+    els.thead = document.querySelector('#rank-table thead');
+    els.metricHeader = document.getElementById('rank-metric-header');
+    els.summary = document.getElementById('rank-summary');
+    els.metricSelect = document.getElementById('rank-metric');
+    els.lastGenerated = document.getElementById('last-generated');
+
+    var inline = document.getElementById('polisci-manifest');
+    if (inline && inline.textContent) {
+      try { onManifest(JSON.parse(inline.textContent)); return; }
+      catch (e) { /* fall through */ }
+    }
+    fetch(state.BASE + 'manifest.json', { credentials: 'omit' })
+      .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
+      .then(onManifest)
+      .catch(function (e) { showFatal('Failed to load manifest: ' + e.message + ' — if you opened this file directly, serve it over HTTP.'); });
+  }
+
+  function showFatal(msg) {
+    els.root.replaceChildren();
+    var p = document.createElement('p');
+    p.style.padding = '20px';
+    p.style.color = '#c0392b';
+    p.textContent = msg;
+    els.root.appendChild(p);
+  }
+
+  function onManifest(m) {
+    state.manifest = m;
+    if (els.lastGenerated) els.lastGenerated.textContent = m.generated_at || '';
+
+    var chamberInputs = document.querySelectorAll('input[name="chamber"]');
+    for (var i = 0; i < chamberInputs.length; i++) {
+      chamberInputs[i].addEventListener('change', onChamberChange);
+    }
+    var partyInputs = document.querySelectorAll('input[name="party"]');
+    for (var j = 0; j < partyInputs.length; j++) {
+      partyInputs[j].addEventListener('change', onPartyChange);
+    }
+    els.metricSelect.addEventListener('change', function () {
+      state.metric = els.metricSelect.value;
+      syncURL();
+      render();
+    });
+    var orderInputs = document.querySelectorAll('input[name="order"]');
+    for (var k = 0; k < orderInputs.length; k++) {
+      orderInputs[k].addEventListener('change', onOrderChange);
+    }
+    document.getElementById('rank-reset').addEventListener('click', resetControls);
+
+    var toggle = document.getElementById('sidebar-toggle');
+    if (toggle) toggle.addEventListener('click', function () {
+      var body = document.getElementById('sidebar-body');
+      var open = body.classList.toggle('is-collapsed') ? 'false' : 'true';
+      toggle.setAttribute('aria-expanded', open);
+    });
+
+    window.addEventListener('popstate', function () { restoreFromURL(); render(); });
+    restoreFromURL();
+    render();
+  }
+
+  function onChamberChange(e) {
+    state.chamber = e.target.value === 'S' ? 'S' : 'H';
+    syncURL();
+    render();
+  }
+  function onPartyChange(e) {
+    state.parties[e.target.value] = e.target.checked;
+    syncURL();
+    render();
+  }
+  function onOrderChange(e) {
+    state.order = e.target.value === 'asc' ? 'asc' : 'desc';
+    syncURL();
+    render();
+  }
+
+  function resetControls() {
+    state.chamber = 'H';
+    state.parties = { R: true, D: true, I: true };
+    state.metric = 'total';
+    state.order = 'desc';
+    document.querySelector('input[name="chamber"][value="H"]').checked = true;
+    document.querySelector('input[name="party"][value="R"]').checked = true;
+    document.querySelector('input[name="party"][value="D"]').checked = true;
+    document.querySelector('input[name="party"][value="I"]').checked = true;
+    els.metricSelect.value = 'total';
+    document.querySelector('input[name="order"][value="desc"]').checked = true;
+    syncURL();
+    render();
+  }
+
+  function computeMetric(k, key) {
+    var voting = k.voting || 0;
+    switch (key) {
+      case 'total': return k.total || 0;
+      case 'yeas': return k.yeas || 0;
+      case 'nays': return k.nays || 0;
+      case 'participation_pct': return k.total ? (100 * voting / k.total) : null;
+      case 'voted_with_gop': return k.voted_with_gop || 0;
+      case 'voted_with_gop_pct': return voting ? (100 * (k.voted_with_gop || 0) / voting) : null;
+      case 'voted_with_dem': return k.voted_with_dem || 0;
+      case 'voted_with_dem_pct': return voting ? (100 * (k.voted_with_dem || 0) / voting) : null;
+      case 'voted_against_gop': return k.voted_against_gop || 0;
+      case 'voted_against_gop_pct': return voting ? (100 * (k.voted_against_gop || 0) / voting) : null;
+      case 'voted_against_dem': return k.voted_against_dem || 0;
+      case 'voted_against_dem_pct': return voting ? (100 * (k.voted_against_dem || 0) / voting) : null;
+      case 'lone_wolf': return k.lone_wolf || 0;
+      case 'lone_wolf_pct': return voting ? (100 * (k.lone_wolf || 0) / voting) : null;
+    }
+    return null;
+  }
+
+  function isPct(key) { return /_pct$/.test(key); }
+
+  function render() {
+    var members = (state.manifest && state.manifest.members) || [];
+    var rows = [];
+    for (var i = 0; i < members.length; i++) {
+      var m = members[i];
+      if (m.c !== state.chamber) continue;
+      if (!state.parties[m.p]) continue;
+      if (!m.k) continue;
+      var v = computeMetric(m.k, state.metric);
+      if (v === null) continue;
+      rows.push({ m: m, v: v });
+    }
+
+    rows.sort(function (a, b) {
+      if (a.v === b.v) return (a.m.n || '').localeCompare(b.m.n || '');
+      return state.order === 'asc' ? a.v - b.v : b.v - a.v;
+    });
+
+    var label = METRIC_LABELS[state.metric] || state.metric;
+    els.metricHeader.textContent = label;
+
+    els.summary.replaceChildren();
+    var p = document.createElement('p');
+    p.className = 'rank-summary-line';
+    var chamberName = state.chamber === 'H' ? 'House' : 'Senate';
+    var partyList = ['R', 'D', 'I'].filter(function (x) { return state.parties[x]; }).join('/') || 'none';
+    p.textContent = rows.length + ' ' + chamberName + ' members (' + partyList + ') ranked by ' + label
+      + ' (' + (state.order === 'desc' ? 'highest first' : 'lowest first') + ')';
+    els.summary.appendChild(p);
+
+    els.tbody.replaceChildren();
+    var pct = isPct(state.metric);
+    for (var r = 0; r < rows.length; r++) {
+      var row = rows[r];
+      var tr = document.createElement('tr');
+      tr.className = 'rank-row';
+      tr.tabIndex = 0;
+      tr.dataset.id = row.m.id;
+      tr.addEventListener('click', openMember);
+      tr.addEventListener('keydown', function (e) {
+        if (e.key === 'Enter') openMember.call(e.currentTarget, e);
+      });
+
+      var rank = document.createElement('td');
+      rank.textContent = String(r + 1);
+      tr.appendChild(rank);
+
+      var name = document.createElement('td');
+      name.textContent = row.m.n || row.m.id;
+      tr.appendChild(name);
+
+      var party = document.createElement('td');
+      party.textContent = row.m.p || '';
+      party.className = 'party-cell party-' + (row.m.p || 'X');
+      tr.appendChild(party);
+
+      var st = document.createElement('td');
+      st.textContent = row.m.s || '';
+      tr.appendChild(st);
+
+      var val = document.createElement('td');
+      val.className = 'rank-value';
+      val.textContent = pct ? row.v.toFixed(1) + '%' : String(row.v);
+      tr.appendChild(val);
+
+      els.tbody.appendChild(tr);
+    }
+  }
+
+  function openMember(e) {
+    var tr = e.currentTarget;
+    var id = tr.dataset.id;
+    if (!id || !ID_RE.test(id)) return;
+    window.open('app.html?id=' + encodeURIComponent(id), '_blank', 'noopener');
+  }
+
+  function syncURL() {
+    var params = new URLSearchParams();
+    if (state.chamber !== 'H') params.set('c', state.chamber);
+    if (state.metric !== 'total') params.set('m', state.metric);
+    if (state.order !== 'desc') params.set('o', state.order);
+    var pset = ['R', 'D', 'I'].filter(function (x) { return state.parties[x]; });
+    if (pset.length !== 3) params.set('p', pset.join(''));
+    var q = params.toString();
+    var url = q ? ('?' + q) : location.pathname;
+    history.replaceState(null, '', url);
+  }
+
+  function restoreFromURL() {
+    var params = new URLSearchParams(location.search);
+    var c = params.get('c');
+    if (c === 'S' || c === 'H') state.chamber = c;
+    var m = params.get('m');
+    if (m && METRIC_LABELS[m]) state.metric = m;
+    var o = params.get('o');
+    if (o === 'asc' || o === 'desc') state.order = o;
+    var p = params.get('p');
+    if (p) {
+      state.parties = { R: false, D: false, I: false };
+      for (var i = 0; i < p.length; i++) {
+        var ch = p.charAt(i);
+        if (ch === 'R' || ch === 'D' || ch === 'I') state.parties[ch] = true;
+      }
+    }
+    // Reflect in controls
+    var ci = document.querySelector('input[name="chamber"][value="' + state.chamber + '"]');
+    if (ci) ci.checked = true;
+    var pInputs = document.querySelectorAll('input[name="party"]');
+    for (var j = 0; j < pInputs.length; j++) {
+      pInputs[j].checked = !!state.parties[pInputs[j].value];
+    }
+    els.metricSelect.value = state.metric;
+    var oi = document.querySelector('input[name="order"][value="' + state.order + '"]');
+    if (oi) oi.checked = true;
+  }
+})();

ファイルの差分が大きいため隠しています
+ 12 - 0
template/vendor/chart.umd.min.js


ファイルの差分が大きいため隠しています
+ 1 - 0
template/vendor/sortable.min.js


+ 0 - 0
tests/__init__.py


+ 3 - 0
tests/conftest.py

@@ -0,0 +1,3 @@
+"""Make project root importable so `import analyze, parse` works."""
+import os, sys
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

+ 50 - 0
tests/fixtures/bipartisan_house.xml

@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- derived from data/119/house/cache/2025_001.xml; member votes and totals
+     edited for test. Scenario: BOTH parties majority Yea (bipartisan), Passed.
+     Also exercises Aye/No procedural normalization (vote-type=Aye/No).
+     Members: J000289 (Jordan, R) Aye, K000389 (Khanna, D) Aye,
+              D000032 (Donalds, R) No (cross-pressure: voted against both majorities). -->
+<rollcall-vote>
+<vote-metadata>
+<majority>R</majority>
+<congress>119</congress>
+<session>1st</session>
+<chamber>U.S. House of Representatives</chamber>
+<rollcall-num>901</rollcall-num>
+<legis-num>H R 9002</legis-num>
+<vote-question>On Motion to Suspend the Rules and Pass</vote-question>
+<vote-type>2/3 YEA-AND-NAY</vote-type>
+<vote-result>Passed</vote-result>
+<action-date>16-May-2025</action-date>
+<action-time time-etz="15:00">3:00 PM</action-time>
+<vote-desc>Test bipartisan passage (procedural Aye/No)</vote-desc>
+<vote-totals>
+<totals-by-party>
+<party>Republican</party>
+<yea-total>200</yea-total>
+<nay-total>15</nay-total>
+<present-total>0</present-total>
+<not-voting-total>0</not-voting-total>
+</totals-by-party>
+<totals-by-party>
+<party>Democratic</party>
+<yea-total>180</yea-total>
+<nay-total>20</nay-total>
+<present-total>0</present-total>
+<not-voting-total>0</not-voting-total>
+</totals-by-party>
+<totals-by-party>
+<party>Independent</party>
+<yea-total>0</yea-total>
+<nay-total>0</nay-total>
+<present-total>0</present-total>
+<not-voting-total>0</not-voting-total>
+</totals-by-party>
+</vote-totals>
+</vote-metadata>
+<vote-data>
+<recorded-vote><legislator name-id="J000289" sort-field="Jordan" unaccented-name="Jordan" party="R" state="OH" role="legislator">Jordan</legislator><vote>Aye</vote></recorded-vote>
+<recorded-vote><legislator name-id="K000389" sort-field="Khanna" unaccented-name="Khanna" party="D" state="CA" role="legislator">Khanna</legislator><vote>Aye</vote></recorded-vote>
+<recorded-vote><legislator name-id="D000032" sort-field="Donalds" unaccented-name="Donalds" party="R" state="FL" role="legislator">Donalds</legislator><vote>No</vote></recorded-vote>
+</vote-data>
+</rollcall-vote>

+ 49 - 0
tests/fixtures/failed_blocking_house.xml

@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- derived from data/119/house/cache/2025_001.xml; member votes/totals edited.
+     Scenario: R majority Yea, D majority Nay, result Failed
+     (GOP-backed House measure failed). D000032 (Donalds, R) defects Nay
+     → blocked="Republican". K000389 (Khanna, D) Nay also → blocked="Republican". -->
+<rollcall-vote>
+<vote-metadata>
+<majority>R</majority>
+<congress>119</congress>
+<session>1st</session>
+<chamber>U.S. House of Representatives</chamber>
+<rollcall-num>920</rollcall-num>
+<legis-num>H R 9020</legis-num>
+<vote-question>On Motion to Suspend the Rules and Pass</vote-question>
+<vote-type>2/3 YEA-AND-NAY</vote-type>
+<vote-result>Failed</vote-result>
+<action-date>22-May-2025</action-date>
+<action-time time-etz="14:00">2:00 PM</action-time>
+<vote-desc>GOP-backed House measure failed under 2/3 rule</vote-desc>
+<vote-totals>
+<totals-by-party>
+<party>Republican</party>
+<yea-total>200</yea-total>
+<nay-total>15</nay-total>
+<present-total>0</present-total>
+<not-voting-total>0</not-voting-total>
+</totals-by-party>
+<totals-by-party>
+<party>Democratic</party>
+<yea-total>10</yea-total>
+<nay-total>190</nay-total>
+<present-total>0</present-total>
+<not-voting-total>0</not-voting-total>
+</totals-by-party>
+<totals-by-party>
+<party>Independent</party>
+<yea-total>0</yea-total>
+<nay-total>0</nay-total>
+<present-total>0</present-total>
+<not-voting-total>0</not-voting-total>
+</totals-by-party>
+</vote-totals>
+</vote-metadata>
+<vote-data>
+<recorded-vote><legislator name-id="J000289" sort-field="Jordan" unaccented-name="Jordan" party="R" state="OH" role="legislator">Jordan</legislator><vote>Yea</vote></recorded-vote>
+<recorded-vote><legislator name-id="D000032" sort-field="Donalds" unaccented-name="Donalds" party="R" state="FL" role="legislator">Donalds</legislator><vote>Nay</vote></recorded-vote>
+<recorded-vote><legislator name-id="K000389" sort-field="Khanna" unaccented-name="Khanna" party="D" state="CA" role="legislator">Khanna</legislator><vote>Nay</vote></recorded-vote>
+</vote-data>
+</rollcall-vote>

+ 31 - 0
tests/fixtures/failed_blocking_rep_senate.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- derived from data/119/senate/cache/1_00001.xml; member votes/totals edited.
+     Scenario: R majority Yea, D majority Nay, result "Rejected" (GOP-backed
+     measure blocked). A Nay-voting Republican (defector) → blocked="Republican".
+     S366 (Warren, D) Nay → blocked="Republican" (helped block GOP-backed bill). -->
+<roll_call_vote>
+<congress>119</congress>
+<session>1</session>
+<congress_year>2025</congress_year>
+<vote_number>911</vote_number>
+<vote_date>May 21, 2025,  04:00 PM</vote_date>
+<vote_question_text>On the Motion to Proceed</vote_question_text>
+<vote_document_text>Test bill</vote_document_text>
+<vote_result_text>Motion Rejected (49-50)</vote_result_text>
+<question>On the Motion</question>
+<vote_title>S. 911 — GOP-backed measure rejected</vote_title>
+<vote_result>Motion Rejected</vote_result>
+<document>
+<document_congress>119</document_congress>
+<document_type>S.</document_type>
+<document_number>911</document_number>
+</document>
+<count><yeas>49</yeas><nays>50</nays><present/><absent>1</absent></count>
+<members>
+<member><member_full>Cruz (R-TX)</member_full><last_name>Cruz</last_name><first_name>Ted</first_name><party>R</party><state>TX</state><vote_cast>Yea</vote_cast><lis_member_id>S288</lis_member_id></member>
+<member><member_full>Graham (R-SC)</member_full><last_name>Graham</last_name><first_name>Lindsey</first_name><party>R</party><state>SC</state><vote_cast>Yea</vote_cast><lis_member_id>S272</lis_member_id></member>
+<member><member_full>Collins (R-ME)</member_full><last_name>Collins</last_name><first_name>Susan</first_name><party>R</party><state>ME</state><vote_cast>Nay</vote_cast><lis_member_id>S289</lis_member_id></member>
+<member><member_full>Schumer (D-NY)</member_full><last_name>Schumer</last_name><first_name>Chuck</first_name><party>D</party><state>NY</state><vote_cast>Nay</vote_cast><lis_member_id>S168</lis_member_id></member>
+<member><member_full>Warren (D-MA)</member_full><last_name>Warren</last_name><first_name>Elizabeth</first_name><party>D</party><state>MA</state><vote_cast>Nay</vote_cast><lis_member_id>S366</lis_member_id></member>
+</members>
+</roll_call_vote>

+ 32 - 0
tests/fixtures/failed_blocking_senate.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- derived from data/119/senate/cache/1_00001.xml; member votes/totals edited.
+     Scenario: D majority Yea, R majority Nay, result "Failed" (Dem-backed bill
+     blocked). A Nay-voting Democrat (e.g., Manchin-style) → blocked="Democrat".
+     S288 (Cruz, R) Nay → also blocked="Democrat" since he helped block the
+     Dem-backed bill. S168 (Schumer, D) Yea → no block. -->
+<roll_call_vote>
+<congress>119</congress>
+<session>1</session>
+<congress_year>2025</congress_year>
+<vote_number>910</vote_number>
+<vote_date>May 20, 2025,  03:00 PM</vote_date>
+<vote_question_text>On Cloture on the Motion to Proceed</vote_question_text>
+<vote_document_text>Test bill</vote_document_text>
+<vote_result_text>Cloture Not Invoked (49-50)</vote_result_text>
+<question>On Cloture</question>
+<vote_title>S. 910 — Dem-backed measure blocked</vote_title>
+<vote_result>Cloture Not Invoked</vote_result>
+<document>
+<document_congress>119</document_congress>
+<document_type>S.</document_type>
+<document_number>910</document_number>
+</document>
+<count><yeas>49</yeas><nays>50</nays><present/><absent>1</absent></count>
+<members>
+<member><member_full>Schumer (D-NY)</member_full><last_name>Schumer</last_name><first_name>Chuck</first_name><party>D</party><state>NY</state><vote_cast>Yea</vote_cast><lis_member_id>S168</lis_member_id></member>
+<member><member_full>Warren (D-MA)</member_full><last_name>Warren</last_name><first_name>Elizabeth</first_name><party>D</party><state>MA</state><vote_cast>Yea</vote_cast><lis_member_id>S366</lis_member_id></member>
+<member><member_full>Manchin (D-WV)</member_full><last_name>Manchin</last_name><first_name>Joe</first_name><party>D</party><state>WV</state><vote_cast>Nay</vote_cast><lis_member_id>S307</lis_member_id></member>
+<member><member_full>Cruz (R-TX)</member_full><last_name>Cruz</last_name><first_name>Ted</first_name><party>R</party><state>TX</state><vote_cast>Nay</vote_cast><lis_member_id>S288</lis_member_id></member>
+<member><member_full>Graham (R-SC)</member_full><last_name>Graham</last_name><first_name>Lindsey</first_name><party>R</party><state>SC</state><vote_cast>Nay</vote_cast><lis_member_id>S272</lis_member_id></member>
+</members>
+</roll_call_vote>

+ 54 - 0
tests/fixtures/partisan_house.xml

@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- derived from data/119/house/cache/2025_001.xml; member votes and totals edited
+     for test. Scenario: R majority Yea, D majority Nay, Passed.
+     Members: J000289 (Jordan, R-OH) Yea, K000389 (Khanna, D-CA) Nay,
+              D000032 (Donalds, R-FL) Nay (defector for lone-wolf test),
+              G000596 (Greene, R-GA) Yea, A000370 (Adams, D-NC) Nay,
+              A000371 (Aguilar, D-CA) Yea (defector). -->
+<rollcall-vote>
+<vote-metadata>
+<majority>R</majority>
+<congress>119</congress>
+<session>1st</session>
+<chamber>U.S. House of Representatives</chamber>
+<rollcall-num>900</rollcall-num>
+<legis-num>H R 9001</legis-num>
+<vote-question>On Passage</vote-question>
+<vote-type>RECORDED VOTE</vote-type>
+<vote-result>Passed</vote-result>
+<action-date>15-May-2025</action-date>
+<action-time time-etz="14:00">2:00 PM</action-time>
+<vote-desc>Test partisan passage</vote-desc>
+<vote-totals>
+<totals-by-party>
+<party>Republican</party>
+<yea-total>210</yea-total>
+<nay-total>4</nay-total>
+<present-total>0</present-total>
+<not-voting-total>0</not-voting-total>
+</totals-by-party>
+<totals-by-party>
+<party>Democratic</party>
+<yea-total>3</yea-total>
+<nay-total>200</nay-total>
+<present-total>0</present-total>
+<not-voting-total>0</not-voting-total>
+</totals-by-party>
+<totals-by-party>
+<party>Independent</party>
+<yea-total>0</yea-total>
+<nay-total>0</nay-total>
+<present-total>0</present-total>
+<not-voting-total>0</not-voting-total>
+</totals-by-party>
+</vote-totals>
+</vote-metadata>
+<vote-data>
+<recorded-vote><legislator name-id="J000289" sort-field="Jordan" unaccented-name="Jordan" party="R" state="OH" role="legislator">Jordan</legislator><vote>Yea</vote></recorded-vote>
+<recorded-vote><legislator name-id="G000596" sort-field="Greene (GA)" unaccented-name="Greene (GA)" party="R" state="GA" role="legislator">Greene (GA)</legislator><vote>Yea</vote></recorded-vote>
+<recorded-vote><legislator name-id="D000032" sort-field="Donalds" unaccented-name="Donalds" party="R" state="FL" role="legislator">Donalds</legislator><vote>Nay</vote></recorded-vote>
+<recorded-vote><legislator name-id="K000389" sort-field="Khanna" unaccented-name="Khanna" party="D" state="CA" role="legislator">Khanna</legislator><vote>Nay</vote></recorded-vote>
+<recorded-vote><legislator name-id="A000370" sort-field="Adams" unaccented-name="Adams" party="D" state="NC" role="legislator">Adams</legislator><vote>Nay</vote></recorded-vote>
+<recorded-vote><legislator name-id="A000371" sort-field="Aguilar" unaccented-name="Aguilar" party="D" state="CA" role="legislator">Aguilar</legislator><vote>Yea</vote></recorded-vote>
+</vote-data>
+</rollcall-vote>

+ 29 - 0
tests/fixtures/partisan_senate.xml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- derived from data/119/senate/cache/1_00001.xml; member votes/totals edited.
+     Scenario: R majority Yea, D majority Nay, Passed (partisan-line). -->
+<roll_call_vote>
+<congress>119</congress>
+<session>1</session>
+<congress_year>2025</congress_year>
+<vote_number>900</vote_number>
+<vote_date>May 15, 2025,  02:00 PM</vote_date>
+<vote_question_text>On Passage of S. 900</vote_question_text>
+<vote_document_text>Test bill</vote_document_text>
+<vote_result_text>Passed</vote_result_text>
+<question>On Passage</question>
+<vote_title>S. 900 — Test partisan passage</vote_title>
+<vote_result>Bill Passed</vote_result>
+<document>
+<document_congress>119</document_congress>
+<document_type>S.</document_type>
+<document_number>900</document_number>
+</document>
+<count><yeas>52</yeas><nays>47</nays><present/><absent>1</absent></count>
+<members>
+<member><member_full>Cruz (R-TX)</member_full><last_name>Cruz</last_name><first_name>Ted</first_name><party>R</party><state>TX</state><vote_cast>Yea</vote_cast><lis_member_id>S288</lis_member_id></member>
+<member><member_full>Graham (R-SC)</member_full><last_name>Graham</last_name><first_name>Lindsey</first_name><party>R</party><state>SC</state><vote_cast>Yea</vote_cast><lis_member_id>S272</lis_member_id></member>
+<member><member_full>Schumer (D-NY)</member_full><last_name>Schumer</last_name><first_name>Chuck</first_name><party>D</party><state>NY</state><vote_cast>Nay</vote_cast><lis_member_id>S168</lis_member_id></member>
+<member><member_full>Warren (D-MA)</member_full><last_name>Warren</last_name><first_name>Elizabeth</first_name><party>D</party><state>MA</state><vote_cast>Nay</vote_cast><lis_member_id>S366</lis_member_id></member>
+<member><member_full>Sanders (I-VT)</member_full><last_name>Sanders</last_name><first_name>Bernie</first_name><party>I</party><state>VT</state><vote_cast>Nay</vote_cast><lis_member_id>S313</lis_member_id></member>
+</members>
+</roll_call_vote>

+ 48 - 0
tests/fixtures/split_party_house.xml

@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- derived from data/119/house/cache/2025_001.xml; member votes/totals edited.
+     Scenario: D tied (yea==nay), R majority Yea, result Passed.
+     Used to verify _majority_position returns "Split" for D and that
+     classify_vote does not award a false "Helped Democrats" on a tied party. -->
+<rollcall-vote>
+<vote-metadata>
+<majority>R</majority>
+<congress>119</congress>
+<session>1st</session>
+<chamber>U.S. House of Representatives</chamber>
+<rollcall-num>930</rollcall-num>
+<legis-num>H R 9030</legis-num>
+<vote-question>On Passage</vote-question>
+<vote-type>RECORDED VOTE</vote-type>
+<vote-result>Passed</vote-result>
+<action-date>23-May-2025</action-date>
+<action-time time-etz="14:00">2:00 PM</action-time>
+<vote-desc>Split D party</vote-desc>
+<vote-totals>
+<totals-by-party>
+<party>Republican</party>
+<yea-total>200</yea-total>
+<nay-total>10</nay-total>
+<present-total>0</present-total>
+<not-voting-total>0</not-voting-total>
+</totals-by-party>
+<totals-by-party>
+<party>Democratic</party>
+<yea-total>100</yea-total>
+<nay-total>100</nay-total>
+<present-total>0</present-total>
+<not-voting-total>0</not-voting-total>
+</totals-by-party>
+<totals-by-party>
+<party>Independent</party>
+<yea-total>0</yea-total>
+<nay-total>0</nay-total>
+<present-total>0</present-total>
+<not-voting-total>0</not-voting-total>
+</totals-by-party>
+</vote-totals>
+</vote-metadata>
+<vote-data>
+<recorded-vote><legislator name-id="K000389" sort-field="Khanna" unaccented-name="Khanna" party="D" state="CA" role="legislator">Khanna</legislator><vote>Yea</vote></recorded-vote>
+<recorded-vote><legislator name-id="J000289" sort-field="Jordan" unaccented-name="Jordan" party="R" state="OH" role="legislator">Jordan</legislator><vote>Yea</vote></recorded-vote>
+</vote-data>
+</rollcall-vote>

+ 205 - 0
tests/parity_check.py

@@ -0,0 +1,205 @@
+#!/usr/bin/env python3
+"""Phase 3 KPI parity gate: compares legacy/*.html DATA blocks against
+data/119/members/<id>.json metrics blocks for 8 sample members."""
+import json
+import re
+import sys
+from pathlib import Path
+
+ROOT = Path("/home/user/polisci")
+LEGACY = ROOT / "legacy"
+MEMBERS = ROOT / "data" / "119" / "members"
+
+PAIRS = [
+    ("ThomasMassie119.html", "M001184", "Thomas Massie"),
+    ("RoKhanna119.html", "K000389", "Ro Khanna"),
+    ("AlexandriaOcasioCortez119.html", "O000172", "Alexandria Ocasio-Cortez"),
+    ("IlhanOmar119.html", "O000173", "Ilhan Omar"),
+    ("MarjorieTaylorGreene119.html", "G000596", "Marjorie Taylor Greene"),
+    ("JimJordan119.html", "J000289", "Jim Jordan"),
+    ("ByronDonalds119.html", "D000032", "Byron Donalds"),
+    ("LindseyGraham119.html", "S293", "Lindsey Graham"),
+]
+
+SCALAR_KPIS = [
+    "total", "voting", "yeas", "nays", "nv", "present",
+    "blocked_dem_count", "blocked_rep_count",
+    "voted_with_gop", "voted_against_gop",
+    "voted_with_dem", "voted_against_dem",
+]
+CANONICAL_ALIGN = ["Helped Republicans", "Helped Democrats", "Helped Both", "Helped Neither"]
+
+
+def extract_legacy(path):
+    text = path.read_text(encoding="utf-8")
+    m = re.search(r"const DATA\s*=\s*(\{.+?\});", text)
+    if not m:
+        raise RuntimeError(f"No DATA block in {path}")
+    return json.loads(m.group(1))
+
+
+def cmp_member(legacy_file, mid, display):
+    out_lines = []
+    fails = []
+
+    legacy_path = LEGACY / legacy_file
+    new_path = MEMBERS / f"{mid}.json"
+    legacy = extract_legacy(legacy_path)
+    new_full = json.loads(new_path.read_text(encoding="utf-8"))
+    new = new_full["metrics"]
+
+    # scalar KPIs
+    for k in SCALAR_KPIS:
+        lv = legacy.get(k)
+        nv = new.get(k)
+        if lv != nv:
+            fails.append(f"  {k}: legacy={lv} new={nv}  (delta {(nv if nv is not None else 0) - (lv if lv is not None else 0)})")
+
+    # lone_wolf (only if legacy has it)
+    if "lone_wolf" in legacy:
+        if legacy["lone_wolf"] != new.get("lone_wolf"):
+            fails.append(f"  lone_wolf: legacy={legacy['lone_wolf']} new={new.get('lone_wolf')}")
+
+    # alignment canonical 4
+    la = legacy.get("alignment", {})
+    na = new.get("alignment", {})
+    for k in CANONICAL_ALIGN:
+        lv, nv = la.get(k, 0), na.get(k, 0)
+        if lv != nv:
+            fails.append(f"  alignment[{k}]: legacy={lv} new={nv}  (delta {nv - lv})")
+    # N/A sums
+    la_na = sum(v for k, v in la.items() if k.startswith("N/A"))
+    na_na = sum(v for k, v in na.items() if k.startswith("N/A"))
+    if la_na != na_na:
+        fails.append(f"  alignment[N/A sum]: legacy={la_na} new={na_na}  (delta {na_na - la_na})")
+
+    # blocked
+    lb = legacy.get("blocked", {})
+    nb = new.get("blocked", {})
+    for k in ("Democrat", "Republican"):
+        if lb.get(k, 0) != nb.get(k, 0):
+            fails.append(f"  blocked[{k}]: legacy={lb.get(k,0)} new={nb.get(k,0)}")
+
+    # monthly: align by month string
+    l_months = legacy.get("months", [])
+    n_months = new.get("months", [])
+    l_monthly = legacy.get("monthly", {})
+    n_monthly = new.get("monthly", {})
+    all_months = sorted(set(l_months) | set(n_months))
+    for cat in CANONICAL_ALIGN:
+        l_arr = l_monthly.get(cat, [])
+        n_arr = n_monthly.get(cat, [])
+        l_map = dict(zip(l_months, l_arr))
+        n_map = dict(zip(n_months, n_arr))
+        for mo in all_months:
+            lv = l_map.get(mo, 0)
+            nv = n_map.get(mo, 0)
+            if lv != nv:
+                fails.append(f"  monthly[{cat}][{mo}]: legacy={lv} new={nv}  (delta {nv - lv})")
+
+    # rows length must equal total
+    l_rows = len(legacy.get("rows", []))
+    n_rows = len(new.get("rows", []))
+    if l_rows != legacy.get("total"):
+        fails.append(f"  legacy rows length {l_rows} != legacy total {legacy.get('total')}")
+    if n_rows != new.get("total"):
+        fails.append(f"  new rows length {n_rows} != new total {new.get('total')}")
+    if l_rows != n_rows:
+        fails.append(f"  rows length: legacy={l_rows} new={n_rows}  (delta {n_rows - l_rows})")
+
+    if not fails:
+        out_lines.append(f"{mid} {display} — PASS  (all {len(SCALAR_KPIS)}+1 scalar KPIs + alignment4 + blocked + monthly + rows)")
+        return True, out_lines
+    else:
+        out_lines.append(f"{mid} {display} — FAIL")
+        out_lines.extend(fails)
+        return False, out_lines
+
+
+def main():
+    report = []
+    report.append("# Phase 3 — KPI Parity Gate Report")
+    report.append("")
+    report.append("Compared legacy/*.html DATA blocks vs data/119/members/<id>.json metrics.")
+    report.append("")
+    report.append("## Per-member results")
+    report.append("")
+    passes = 0
+    for legacy_file, mid, display in PAIRS:
+        ok, lines = cmp_member(legacy_file, mid, display)
+        if ok:
+            passes += 1
+        report.extend(lines)
+        report.append("")
+    summary = f"Phase 3 gate: {passes}/{len(PAIRS)} members PASS"
+    report.append("## Summary")
+    report.append("")
+    report.append(summary)
+    report.append("")
+
+    # MTG banner check
+    mtg = json.loads((MEMBERS / "G000596.json").read_text(encoding="utf-8"))
+    report.append("## MTG (G000596) banner check")
+    report.append("")
+    report.append(f"- served_partial: {mtg.get('served_partial')}")
+    report.append(f"- metrics.total: {mtg['metrics'].get('total')}")
+    report.append(f"- served_from: {mtg.get('served_from')}, served_to: {mtg.get('served_to')}")
+    if mtg.get("served_partial") or mtg["metrics"].get("total") == 0:
+        report.append("- Banner SHOULD trigger.")
+    else:
+        report.append("- No banner needed (full term, has votes).")
+    report.append("")
+
+    # deep-link in template/app.js
+    appjs = (ROOT / "template" / "app.js").read_text(encoding="utf-8")
+    has_pushstate = "pushState" in appjs
+    has_popstate = "popstate" in appjs
+    report.append("## Deep-link URL behavior (template/app.js)")
+    report.append("")
+    report.append(f"- pushState present: {has_pushstate}")
+    report.append(f"- popstate present: {has_popstate}")
+    report.append("")
+
+    # CDN check
+    import subprocess
+    cdn = subprocess.run(
+        ["grep", "-RE", r"cdn\.|cdnjs|jsdelivr|unpkg", str(ROOT / "template")],
+        capture_output=True, text=True,
+    )
+    report.append("## CDN traffic check (template/)")
+    report.append("")
+    # Filter out comment lines inside vendored files (documentation references, not runtime URLs)
+    real_hits = []
+    for line in cdn.stdout.strip().splitlines():
+        if "/vendor/" in line and ("More information" in line or "Do NOT use SRI" in line):
+            continue  # vendored chart.js header comment
+        real_hits.append(line)
+    if real_hits:
+        report.append("- FAIL — CDN references found:")
+        for line in real_hits:
+            report.append(f"  {line}")
+    else:
+        report.append("- PASS — no runtime CDN references (vendored-file documentation comments ignored).")
+        if cdn.stdout.strip():
+            report.append("  Note: documentation/comment-only mentions inside template/vendor/ were ignored:")
+            for line in cdn.stdout.strip().splitlines():
+                report.append(f"    {line}")
+    report.append("")
+
+    # decision
+    report.append("## Decision")
+    report.append("")
+    if passes == len(PAIRS):
+        report.append("**GATE PASSED — safe to proceed to Phase 4.**")
+    else:
+        report.append("**GATE FAILED — investigate the delta(s) before proceeding.**")
+    report.append("")
+
+    text = "\n".join(report)
+    print(text)
+    (ROOT / "research" / "PHASE3_PARITY.md").write_text(text, encoding="utf-8")
+    return 0 if passes == len(PAIRS) else 1
+
+
+if __name__ == "__main__":
+    sys.exit(main())

+ 189 - 0
tests/test_analyze.py

@@ -0,0 +1,189 @@
+"""Frozen-fixture suite for analyze.py classifier and aggregator.
+
+Each test parses a real-ish Clerk/Senate XML fixture via parse.py, then asserts
+classify_vote / aggregate behavior. Fixtures live in tests/fixtures/ and are
+trimmed/edited copies of real cached XMLs (see header comment in each).
+"""
+import os
+import pytest
+
+import analyze
+import parse
+
+FIX = os.path.join(os.path.dirname(__file__), "fixtures")
+
+
+# ---------- Unit tests on pure helpers ----------
+
+@pytest.mark.parametrize("raw,expected", [
+    ("Yea", "Yea"), ("Aye", "Yea"),
+    ("Nay", "Nay"), ("No", "Nay"),
+    ("Present", "Present"),
+    ("Not Voting", "Not Voting"),
+    (None, None), ("", ""),
+])
+def test_norm_vote(raw, expected):
+    assert analyze._norm_vote(raw) == expected
+
+
+@pytest.mark.parametrize("totals,expected", [
+    ({"yea": 100, "nay": 50}, "Yea"),
+    ({"yea": 50, "nay": 100}, "Nay"),
+    ({"yea": 100, "nay": 100}, "Split"),
+    ({"yea": 0, "nay": 0}, "Split"),
+])
+def test_majority_position(totals, expected):
+    assert analyze._majority_position(totals) == expected
+
+
+# ---------- Fixture loaders ----------
+
+def _house(name, year=2025, roll=900):
+    rec, _ = parse.parse_house_vote(os.path.join(FIX, name), year, roll)
+    assert rec is not None, f"parse_house_vote returned None for {name}"
+    return rec
+
+
+def _senate(name, session=1, vnum=900):
+    rec, _ = parse.parse_senate_vote(os.path.join(FIX, name), session, vnum)
+    assert rec is not None, f"parse_senate_vote returned None for {name}"
+    return rec
+
+
+# ---------- Classification tests ----------
+
+def test_partisan_house_helps_republicans():
+    rec = _house("partisan_house.xml")
+    # Jordan (R) Yea on R-Yea/D-Nay partisan vote
+    cls = analyze.classify_vote(rec, "J000289")
+    assert cls["member_vote"] == "Yea"
+    assert cls["member_vote_norm"] == "Yea"
+    assert cls["r_pos"] == "Yea"
+    assert cls["d_pos"] == "Nay"
+    assert cls["alignment"] == "Helped Republicans"
+    assert cls["blocked"] is None
+
+
+def test_partisan_house_helps_democrats():
+    rec = _house("partisan_house.xml")
+    # Khanna (D) Nay on R-Yea/D-Nay partisan vote
+    cls = analyze.classify_vote(rec, "K000389")
+    assert cls["alignment"] == "Helped Democrats"
+
+
+def test_bipartisan_aye_no_normalization():
+    rec = _house("bipartisan_house.xml")
+    # Both parties Yea; Jordan voted "Aye" (procedural)
+    cls = analyze.classify_vote(rec, "J000289")
+    assert cls["member_vote"] == "Aye"          # raw preserved
+    assert cls["member_vote_norm"] == "Yea"     # normalized
+    assert cls["alignment"] == "Helped Both"
+    # Khanna also Aye → Helped Both
+    assert analyze.classify_vote(rec, "K000389")["alignment"] == "Helped Both"
+    # Donalds "No" → normalized Nay, helped neither majority
+    donalds = analyze.classify_vote(rec, "D000032")
+    assert donalds["member_vote"] == "No"
+    assert donalds["member_vote_norm"] == "Nay"
+    assert donalds["alignment"] == "Helped Neither"
+
+
+def test_member_absent():
+    rec = _house("partisan_house.xml")
+    cls = analyze.classify_vote(rec, "Z999999")  # not in record
+    assert cls["alignment"].startswith("N/A: absent")
+    assert cls["member_vote"] is None
+    assert cls["member_vote_norm"] is None
+    assert cls["blocked"] is None
+
+
+def test_split_party_no_false_helped():
+    rec = _house("split_party_house.xml")
+    # D is tied (100/100) → Split. Khanna (D) voted Yea; R majority is Yea.
+    # Should classify as Helped Republicans only — NOT Helped Democrats.
+    cls = analyze.classify_vote(rec, "K000389")
+    assert cls["d_pos"] == "Split"
+    assert cls["r_pos"] == "Yea"
+    assert cls["alignment"] == "Helped Republicans"
+    # And aggregate must not credit Khanna a "with-dem" or "against-dem"
+    # because d_pos is Split (no party position to compare against).
+    kpi = analyze.aggregate([rec], "K000389", "D", "house")
+    assert kpi["voted_with_dem"] == 0
+    assert kpi["voted_against_dem"] == 0
+    assert kpi["voted_with_gop"] == 1
+
+
+# ---------- Failed-blocking tests ----------
+
+def test_failed_blocking_democrat_senate():
+    rec = _senate("failed_blocking_senate.xml", session=1, vnum=910)
+    # D-Yea / R-Nay / Failed. Manchin (D) voted Nay → blocked="Democrat".
+    cls = analyze.classify_vote(rec, "S307")
+    assert cls["d_pos"] == "Yea"
+    assert cls["r_pos"] == "Nay"
+    assert cls["blocked"] == "Democrat"
+    # Cruz (R) Nay also gets blocked="Democrat" (he helped block Dem-backed bill)
+    assert analyze.classify_vote(rec, "S288")["blocked"] == "Democrat"
+    # Schumer (D) Yea → no block (he supported it)
+    assert analyze.classify_vote(rec, "S168")["blocked"] is None
+
+
+def test_failed_blocking_republican_senate():
+    rec = _senate("failed_blocking_rep_senate.xml", session=1, vnum=911)
+    # R-Yea / D-Nay / Rejected. Collins (R) Nay → blocked="Republican".
+    assert analyze.classify_vote(rec, "S289")["blocked"] == "Republican"
+    # Warren (D) Nay → blocked="Republican" (Dem helped block GOP-backed bill)
+    assert analyze.classify_vote(rec, "S366")["blocked"] == "Republican"
+    # Cruz (R) Yea → no block
+    assert analyze.classify_vote(rec, "S288")["blocked"] is None
+
+
+def test_failed_blocking_house_analog():
+    rec = _house("failed_blocking_house.xml", year=2025, roll=920)
+    # House R-Yea / D-Nay / Failed. Confirms classifier is chamber-agnostic.
+    assert analyze.classify_vote(rec, "D000032")["blocked"] == "Republican"
+    assert analyze.classify_vote(rec, "K000389")["blocked"] == "Republican"
+
+
+# ---------- Aggregate end-to-end ----------
+
+def test_aggregate_end_to_end_house():
+    recs = [
+        _house("partisan_house.xml", year=2025, roll=900),
+        _house("bipartisan_house.xml", year=2025, roll=901),
+        _house("failed_blocking_house.xml", year=2025, roll=920),
+        _house("split_party_house.xml", year=2025, roll=930),
+    ]
+    # Khanna (D) — votes: Nay (partisan), Aye(=Yea) (bipartisan),
+    # Nay (failed-blocking-house), Yea (split). All 4 are voting (no Present/NV).
+    kpi = analyze.aggregate(recs, "K000389", "D", "house")
+    assert kpi["chamber"] == "house"
+    assert kpi["total"] == 4
+    assert kpi["voting"] == 4
+    assert kpi["yeas"] == 2     # bipartisan Aye→Yea, split Yea
+    assert kpi["nays"] == 2     # partisan Nay, failed-blocking-house Nay
+    assert kpi["present"] == 0
+    assert kpi["nv"] == 0
+    # voted_with_gop: vote matched R majority position (excluding R-Split votes).
+    # partisan: R=Yea, Khanna=Nay → against. bipartisan: R=Yea, Khanna=Yea → with.
+    # failed-blocking-house: R=Yea, Khanna=Nay → against. split: R=Yea, Khanna=Yea → with.
+    assert kpi["voted_with_gop"] == 2
+    assert kpi["voted_against_gop"] == 2
+    # blocked counter: failed_blocking_house gives blocked="Republican" for Khanna
+    assert kpi["blocked_rep_count"] == 1
+    assert kpi["blocked_dem_count"] == 0
+    assert kpi["lone_wolf_threshold"] == 5
+    # alignment counts sum to total
+    assert sum(kpi["alignment"].values()) == 4
+
+
+def test_lone_wolf_threshold_chamber_dependent():
+    # partisan_house: R yea=210, R nay=4. Donalds (R) voted Nay → defector.
+    # defectors == 4. House threshold 5 → lone wolf. Senate threshold 3 → NOT.
+    rec = _house("partisan_house.xml")
+    house_kpi = analyze.aggregate([rec], "D000032", "R", "house")
+    assert house_kpi["lone_wolf"] == 1
+    assert house_kpi["lone_wolf_threshold"] == 5
+
+    senate_kpi = analyze.aggregate([rec], "D000032", "R", "senate")
+    assert senate_kpi["lone_wolf"] == 0
+    assert senate_kpi["lone_wolf_threshold"] == 3

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません