|
@@ -1,7 +1,7 @@
|
|
|
# Project: 119th Congress Voting Dashboard
|
|
# 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`.
|
|
|
|
|
|
|
+_Written post-hoc by the PM agent (2026-05-24) to codify shipped reality through Phase 7 + Rounds A–D.
|
|
|
|
|
+The implementation plan that drove the initial build lives at `research/PLAN.md`.
|
|
|
Only the PM agent may edit this file._
|
|
Only the PM agent may edit this file._
|
|
|
|
|
|
|
|
---
|
|
---
|
|
@@ -41,12 +41,13 @@ load.
|
|
|
|
|
|
|
|
## 4. Scope of Work
|
|
## 4. Scope of Work
|
|
|
|
|
|
|
|
-### In Scope (MVP — as built through Phase 6)
|
|
|
|
|
|
|
+### In Scope (as built through Phase 7 + Rounds A–D)
|
|
|
|
|
|
|
|
| Feature | Description | Priority |
|
|
| 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 |
|
|
| 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 |
|
|
|
|
|
|
|
+| 5 core charts | Vote distribution, alignment doughnut, blocking bars, alignment-over-time line, with/against stacked bar — created once at page init; fixed-height frames (340px desktop / 280px mobile) with `.chart-canvas-wrap` to prevent Chart.js infinite-growth bug | Must Have |
|
|
|
|
|
+| 8 KPI tiles | Vote counts, alignment rates, plus "Voted With GOP" and "Voted With Dem" tiles (up from 6 at Phase 6) | Must Have |
|
|
|
| Sortable/filterable vote table | Per-vote rows; all upstream strings rendered via `textContent` (no `innerHTML`) | 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 |
|
|
| 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) |
|
|
| localStorage persistence | Last-selected member persisted as `polisci:v119:lastMember`; validated against manifest on read | Could Have (shipped) |
|
|
@@ -57,6 +58,10 @@ load.
|
|
|
| Parameterized pipeline | All build scripts accept `--congress N`; generalizes to future Congresses without code changes | 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 |
|
|
| 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 |
|
|
| Test suite | `pytest tests/test_analyze.py` with frozen XML fixtures covering partisan, bipartisan, absent-member, and failed-blocking cases | Must Have |
|
|
|
|
|
+| Rankings page | `ranking.html` + `ranking.js` — 14 sortable metrics across House/Senate; party + chamber filters; row-click opens member dashboard in new tab; shareable URL state; driven by per-member KPI dict (`k` field) in the manifest (one fetch only) | Must Have |
|
|
|
|
|
+| Structural member banners | `app.js` `renderNote()` detects and renders contextual banners by priority: delegate > unseated (member-elect) > died > replaced_by > replaces > served_partial > none; predecessor/successor names rendered as in-app links via `state.membersById` | Must Have |
|
|
|
|
|
+| Delegate detection | 6 territorial House delegates (AS/DC/GU/MP/PR/VI) detected by USPS code; `parse.py` corrects "XX" state in vote XMLs to real territory code; `is_delegate` flag in per-member JSON + `dl: true` in manifest; yellow explanatory banner in `app.js` | Must Have |
|
|
|
|
|
+| Replacement linking | 8 House replacement pairs auto-linked in the 119th (e.g. Gaetz→Patronis, Waltz→Fine, Grijalva R.→Grijalva A.); `replaces` / `replaced_by` bioguide refs in per-member JSON | Must Have |
|
|
|
|
|
|
|
|
### Out of Scope (Future / Deferred — see `NOTES.md` for full rationale)
|
|
### Out of Scope (Future / Deferred — see `NOTES.md` for full rationale)
|
|
|
|
|
|
|
@@ -74,20 +79,21 @@ load.
|
|
|
**Single-member view (`app.html`):**
|
|
**Single-member view (`app.html`):**
|
|
|
|
|
|
|
|
```
|
|
```
|
|
|
-1. Page loads → fetches manifest.json (552 members, version-stamped)
|
|
|
|
|
|
|
+1. Page loads → manifest inlined in <script type="application/json" id="polisci-manifest"> (no fetch needed under file://)
|
|
|
2. Sidebar populated (chamber, party, state checkboxes)
|
|
2. Sidebar populated (chamber, party, state checkboxes)
|
|
|
3. User types in typeahead or applies sidebar filters → list narrows live
|
|
3. User types in typeahead or applies sidebar filters → list narrows live
|
|
|
4. User selects a member → pushState updates URL to ?id=<bioguide>
|
|
4. User selects a member → pushState updates URL to ?id=<bioguide>
|
|
|
5. App fetches data/members/<id>.json (cache-busted by manifest version)
|
|
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
|
|
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
|
|
|
|
|
|
|
+7. KPI cards (8 tiles), sortable/filterable vote table, and member-note banner update
|
|
|
|
|
+8. Structural banner rendered if member is a delegate, member-elect, deceased, or part of a replacement chain
|
|
|
|
|
+9. Reloading the URL restores the same member
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
**Comparison view (`compare.html`):**
|
|
**Comparison view (`compare.html`):**
|
|
|
|
|
|
|
|
```
|
|
```
|
|
|
-1. Page loads → same manifest fetch and sidebar setup
|
|
|
|
|
|
|
+1. Page loads → same inlined manifest; sidebar setup
|
|
|
2. User selects members via typeahead → color-coded pills appear
|
|
2. User selects members via typeahead → color-coded pills appear
|
|
|
3. App fetches each selected member's JSON; 5 overlay charts update
|
|
3. App fetches each selected member's JSON; 5 overlay charts update
|
|
|
4. URL updates to ?ids=<id1>,<id2>,... (shareable, capped at 6)
|
|
4. URL updates to ?ids=<id1>,<id2>,... (shareable, capped at 6)
|
|
@@ -95,6 +101,17 @@ load.
|
|
|
6. Reloading restores all selected members from URL
|
|
6. Reloading restores all selected members from URL
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
|
|
+**Rankings view (`ranking.html`):**
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+1. Page loads → manifest KPI dict (k field) used directly — no per-member fetches
|
|
|
|
|
+2. Table rendered with all members; 14 metrics available as sortable columns
|
|
|
|
|
+3. Party + chamber filters narrow the table live
|
|
|
|
|
+4. Column header click sorts ascending/descending
|
|
|
|
|
+5. Row click opens member's app.html?id=<id> in a new tab
|
|
|
|
|
+6. Filter + sort state encoded in URL (shareable)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
## 6. Usability Concerns
|
|
## 6. Usability Concerns
|
|
@@ -102,7 +119,7 @@ load.
|
|
|
- **Mobile**: sidebar collapses on small viewports; responsive `@media (max-width: 768px)` block in `app.css`
|
|
- **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
|
|
- **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
|
|
- **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
|
|
|
|
|
|
|
+- **Performance**: 552-member manifest loads once (inlined into HTML by `build_app.py` for file:// compatibility); 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
|
|
- **No external requests after page load**: verified by Phase 6 grep audit and HTTP smoke test
|
|
|
|
|
|
|
|
---
|
|
---
|
|
@@ -114,20 +131,35 @@ load.
|
|
|
- Vanilla JS (ES2020, no framework, no transpiler)
|
|
- Vanilla JS (ES2020, no framework, no transpiler)
|
|
|
- HTML/CSS (no preprocessor)
|
|
- HTML/CSS (no preprocessor)
|
|
|
- Chart.js 4.4.0 + SortableJS 1.15.2, vendored locally
|
|
- Chart.js 4.4.0 + SortableJS 1.15.2, vendored locally
|
|
|
|
|
+- Manifest inlined by `build_app.py` into `<script type="application/json" id="polisci-manifest">` so the picker works under `file://` without a local HTTP server; per-member JSON still requires HTTP
|
|
|
|
|
|
|
|
**Data pipeline:**
|
|
**Data pipeline:**
|
|
|
```
|
|
```
|
|
|
clerk.house.gov XML ──┐
|
|
clerk.house.gov XML ──┐
|
|
|
- ├─→ fetch.py → parse.py → enrich_roster.py
|
|
|
|
|
-senate.gov XML ────┘ │
|
|
|
|
|
-Congress.gov API ─────────────────────────────────────┘
|
|
|
|
|
|
|
+ ├─→ fetch.py → parse.py → enrich_roster.py ─────────────┐
|
|
|
|
|
+senate.gov XML ────┘ │
|
|
|
|
|
+Congress.gov API ─────────────────────────────────────────────────────────────→┘
|
|
|
|
|
+ enrich_roster.py runs three passes:
|
|
|
|
|
+ 1. Rescue pass — individual /v3/member/{bg} fetch for
|
|
|
|
|
+ vote-derived bioguides missing from the bulk listing
|
|
|
|
|
+ (e.g. Matt Gaetz, member-elect who declined the seat)
|
|
|
|
|
+ 2. Replacement-linking pass — pairs predecessor↔successor
|
|
|
|
|
+ by (state, district) within the Congress window;
|
|
|
|
|
+ emits replaces / replaced_by bioguide refs
|
|
|
|
|
+ 3. Detail-enrichment pass — individual lookups for every
|
|
|
|
|
+ member on a replacement chain to get congress_term
|
|
|
|
|
+ (startYear/endYear/district), death_year, current_member
|
|
|
↓
|
|
↓
|
|
|
build_members.py (parallel pool)
|
|
build_members.py (parallel pool)
|
|
|
→ data/119/members/<id>.json × 552
|
|
→ data/119/members/<id>.json × 552
|
|
|
|
|
+ (carries: congress_term, death_year, current_member,
|
|
|
|
|
+ replaces, replaced_by, is_delegate)
|
|
|
→ data/119/manifest.json
|
|
→ data/119/manifest.json
|
|
|
|
|
+ (carries: per-member KPI dict k, dl flag for delegates)
|
|
|
↓
|
|
↓
|
|
|
build_app.py
|
|
build_app.py
|
|
|
→ results/119/ (embeddable artifact)
|
|
→ results/119/ (embeddable artifact)
|
|
|
|
|
+ (manifest inlined into app.html / ranking.html)
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
**Data sources:**
|
|
**Data sources:**
|
|
@@ -135,6 +167,10 @@ Congress.gov API ─────────────────────
|
|
|
- `senate.gov` — Senate roll-call XML (789 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)
|
|
- `congress.gov/v3` API — complete 119th roster + Senate LIS-to-bioguide crosswalk; API key in `.env` (gitignored)
|
|
|
|
|
|
|
|
|
|
+**Documentation:**
|
|
|
|
|
+- `Methodology.md` (repo root + copied into `results/<C>/`) — end-user methodology doc; linked from page footers alongside House Clerk + Senate XML indexes
|
|
|
|
|
+- `CLAUDE.md` (repo root) — Claude Code project context
|
|
|
|
|
+
|
|
|
**Security posture:**
|
|
**Security posture:**
|
|
|
- All upstream strings rendered via `textContent`, never `innerHTML`
|
|
- All upstream strings rendered via `textContent`, never `innerHTML`
|
|
|
- `parse.py` rejects strings containing `<`, `>`, or control characters
|
|
- `parse.py` rejects strings containing `<`, `>`, or control characters
|
|
@@ -195,11 +231,22 @@ Congress.gov API ─────────────────────
|
|
|
- [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.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)
|
|
- [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)
|
|
|
|
|
|
|
+### Phase 7 — Documentation and close-out
|
|
|
- [ ] Task 7.1 — Update `DOCUMENTATION.md` §8 (new file layout), §9 (new regeneration commands), §11 (change-log entries)
|
|
- [ ] 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)
|
|
- [x] Task 7.2 — Write `PROJECT_SCOPE.md` reflecting shipped reality (this file)
|
|
|
- [ ] Task 7.3 — Delete `legacy/` after user confirmation
|
|
- [ ] Task 7.3 — Delete `legacy/` after user confirmation
|
|
|
|
|
|
|
|
|
|
+### Phase 8 — Post-launch enhancements (Rounds A–D)
|
|
|
|
|
+- [x] Task 8.1 — Rankings page (`ranking.html` + `ranking.js`): 14 sortable metrics, party + chamber filters, row-click to member dashboard, shareable URL state
|
|
|
|
|
+- [x] Task 8.2 — Manifest KPI dict (`k` field per member) so Rankings page needs only one fetch; `build_app.py` inlines full manifest into `<script type="application/json" id="polisci-manifest">` for file:// support
|
|
|
|
|
+- [x] Task 8.3 — Fix Chart.js infinite-growth bug: fixed-height chart frames (340px desktop / 280px mobile); canvases wrapped in `.chart-canvas-wrap`
|
|
|
|
|
+- [x] Task 8.4 — Add "Voted With GOP" / "Voted With Dem" KPI tiles (total count: 6 → 8)
|
|
|
|
|
+- [x] Task 8.5 — Page footers redesigned: links to House Clerk + Senate XML indexes and to `Methodology.md`; write `Methodology.md` (repo root + copied into `results/<C>/`); write `CLAUDE.md` (project context, 104 lines)
|
|
|
|
|
+- [x] Task 8.6 — Delegate banner: detect 6 territorial delegates by USPS code; `parse.py` corrects "XX" state in vote XMLs; `is_delegate` in per-member JSON + `dl: true` in manifest; yellow explanatory banner in `app.js`
|
|
|
|
|
+- [x] Task 8.7 — Structural banners: `enrich_roster.py` rescue pass (individual lookups for vote-derived bioguides missing from bulk listing); replacement-linking pass (pairs predecessor↔successor by state+district, emits `replaces`/`replaced_by`); detail-enrichment pass (individual lookups for replacement-chain members to get `congress_term`, `death_year`, `current_member`)
|
|
|
|
|
+- [x] Task 8.8 — `app.js` `renderNote()` priority chain: delegate > unseated > died > replaced_by > replaces > served_partial > none; predecessor/successor rendered as in-app links via `state.membersById`; 8 House replacement pairs auto-linked
|
|
|
|
|
+- [x] Task 8.9 — `.claude/skills/bulk-update/SKILL.md`: skill doc orchestrating the multi-round scope-update workflow
|
|
|
|
|
+
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
## 9. Success Criteria
|
|
## 9. Success Criteria
|
|
@@ -212,6 +259,8 @@ Congress.gov API ─────────────────────
|
|
|
- [x] Switching members in `app.html` re-renders without a page load
|
|
- [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] Comparison view accepts up to 6 members and renders all 5 overlay charts
|
|
|
- [x] Iframe and inline-div embedding verified
|
|
- [x] Iframe and inline-div embedding verified
|
|
|
|
|
+- [x] Rankings page renders all members with 14 sortable metrics and shareable URL state
|
|
|
|
|
+- [x] Delegate and structural banners render correctly for all 8 replacement pairs and 6 territorial delegates
|
|
|
- [ ] Manual cross-browser smoke (Chrome, Firefox, Safari) — deferred to user (no headless browser available on build host)
|
|
- [ ] Manual cross-browser smoke (Chrome, Firefox, Safari) — deferred to user (no headless browser available on build host)
|
|
|
|
|
|
|
|
---
|
|
---
|
|
@@ -226,6 +275,7 @@ See `NOTES.md` for the full 6-item list with rationale. Summary:
|
|
|
4. **localStorage scope** (Low) — only `lastMember` persisted; full filter persistence not implemented.
|
|
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.
|
|
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.
|
|
6. **Own-party-defection proxy** (Low) — `Helped Neither` used as proxy in compare chart; a true series requires a future `analyze.py` change.
|
|
|
|
|
+7. **Replacement-detection heuristic** (Low) — predecessor↔successor pairing is keyed on (state, district) overlap within the Congress window; a seat with rapid turnover (3+ holders) could produce a mis-link. Validate manually if more than two holders of the same seat appear in a future Congress.
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|