`.
---
## 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 `
` 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 `
` | 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=
5. App fetches data/members/.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=,,... (shareable, capped at 6)
5. Pill click opens member's app.html?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/.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) | | | |