# 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= ``` Endpoint: `https://api.congress.gov/v3/...?api_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 `
` 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: ``` R or D e.g. 47 e.g. "H R 1234" e.g. "On Passage" e.g. "Passed" / "Failed" / "Agreed to" / "Rejected" e.g. "3-Jan-2025" short bill title { party, yea-total, nay-total, present-total, not-voting-total } × R/D/I Massie Yea | Nay | Aye | No | Present | Not Voting ... (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: ` | "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/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 `` 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//` 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// 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/.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/.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// ``` ### 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//{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//members/.json`, `manifest.json`, and `build_report.json`. - **`build_app.py`** — copies `template/` + vendored libraries + the built data into `results//` — 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 | `Yea` | `Yea` | | Member party | attribute on `` | child `` element | | Date format | `3-Jan-2025` | `January 9, 2025, 02:54 PM` | | Vote question | `` + `` | `` + `` + `` | | Result | `` (e.g. "Failed") | `` (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/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 `/` and increment `{R,D,I} × {yea,nay,present,not_voting}` based on `` and ``. 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 `/` + `` (e.g. "S. 5"). For nominations, fall back to `` (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.