Browse Source

docs: sync scope/CLAUDE/DOCUMENTATION + add bulk-update skill

PROJECT_SCOPE.md (PM): §4 In-Scope adds Rankings page, structural banners,
delegate detection, replacement linking, 8 KPI tiles, chart-growth fix.
§5 Feature Flow adds the Rankings flow + banners step. §7 Tech adds the
three-pass enrich_roster, new per-member/manifest fields, Methodology.md
+ CLAUDE.md. §8 adds Phase 8 (tasks 8.1–8.9, all checked) for Rounds A–D.
§9 adds 2 success criteria. §10 adds risk 7 (replacement-detection
heuristic mis-link risk under rapid seat turnover).

CLAUDE.md (104→108 lines): Architecture Decisions gains inline-manifest
injection, chart-canvas-wrap pattern, per-Congress term data source,
"XX" state override, expanded roster-merge field list. Pipeline lines for
parse.py / enrich_roster.py updated to describe directory propagation and
three-pass structure. Data Layout lists new manifest fields (k/dl/un/rs/rb/
sy/ey/dy). Frontend Pages updates app.html (8 tiles) and adds banner-
priority bullet. Active Tasks now points at PM's open 7.1 + 7.3.

DOCUMENTATION.md (657→711): §8 file layout includes CLAUDE.md,
Methodology.md, .claude/skills/bulk-update/SKILL.md, ranking.html,
ranking.js, and the copied Methodology.md in results/<C>/. §9 clarifies
that the second parse.py run merges the now-populated members_directory.
§11 v1.1.0 entry covers Rounds A (Rankings + UX), B (delegate banner),
C (member-elect/replaced/died banners), D (bulk-update skill).

New project-level skill: .claude/skills/bulk-update/SKILL.md orchestrates
this very cycle — PM scope update → parallel CLAUDE/DOC sync → git
add/commit/push. Designed for end-of-round runs, not per-edit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Max 1 tháng trước cách đây
mục cha
commit
44e3ba0150
4 tập tin đã thay đổi với 232 bổ sung23 xóa
  1. 101 0
      .claude/skills/bulk-update/SKILL.md
  2. 12 8
      CLAUDE.md
  3. 56 2
      DOCUMENTATION.md
  4. 63 13
      PROJECT_SCOPE.md

+ 101 - 0
.claude/skills/bulk-update/SKILL.md

@@ -0,0 +1,101 @@
+---
+name: bulk-update
+description: After a non-trivial round of changes, run the full documentation+commit cycle in one shot — PM updates PROJECT_SCOPE.md, programmer agents sync CLAUDE.md and project docs (DOCUMENTATION.md, Methodology.md, plus any new top-level *.md files), then git add/commit/push if a remote is configured. Use when the user says "bulk update", "sync docs", "finalize this round", or similar after a meaningful set of edits.
+---
+
+# bulk-update
+
+A one-shot orchestration to keep the project's documentation, scope record, and version control in sync after a round of code changes. Designed to be run at the END of a meaningful unit of work (a feature, a bugfix series, a refactor), NOT after every tiny edit.
+
+## When to use
+
+- User says "bulk update", "sync everything", "wrap this up", "finalize the round", or similar.
+- A meaningful set of changes has landed (new feature, bug fix, schema change, new file added) and the docs / scope / repo are now stale.
+- Skip for trivial single-line edits where the cycle would create more churn than signal.
+
+## Inputs the orchestrator gathers BEFORE spawning agents
+
+Before launching any sub-agents, the main thread MUST collect:
+
+1. **Recent change summary** — review the conversation transcript and `git status` / `git diff --stat` for the changes since the last commit. Write a 5-10 line plain-English summary of WHAT changed and WHY. This summary is passed to every sub-agent so they don't duplicate the investigation.
+
+2. **New files inventory** — `git status --porcelain | awk '/^\?\?/ {print $2}'` plus any modified top-level files. Identify any new top-level documentation files (Methodology.md was an example) that need to be referenced from CLAUDE.md and the project README.
+
+3. **Remote configured?** — `git remote -v`. If `origin` is set, the push step runs; otherwise it's skipped and reported.
+
+4. **PROJECT_SCOPE.md exists?** — if not, this skill is the wrong tool; tell the user to run the PM agent in design mode first.
+
+## Orchestration plan
+
+Run as **three sequential phases**. Within each phase, parallel sub-agents fan out where independent.
+
+### Phase 1 — PM updates PROJECT_SCOPE.md (sequential, 1 agent)
+
+Spawn the PM agent with the change summary and instructions to:
+- Read current `PROJECT_SCOPE.md`.
+- Update Implementation Status, In-Scope, Out-of-Scope, Known Risks, and Future-Considerations sections to reflect the new reality.
+- Add a dated entry to any change-history section (if present).
+- Keep edits surgical — don't rewrite sections that haven't changed.
+- Report back what was changed (1-3 sentence summary).
+
+PM is the ONLY agent allowed to edit `PROJECT_SCOPE.md` (per global CLAUDE.md rule). Do NOT spawn a programmer for this file.
+
+### Phase 2 — Doc + CLAUDE.md sync (parallel, N agents)
+
+Spawn programmer agents in parallel — one per doc target. Always include:
+
+- **CLAUDE.md** — keep under its declared line cap (check the file's "Maintenance Rules" section); update Stack / Architecture Decisions / Pipeline / Data Layout / Frontend Pages / Conventions / Known Issues sections as relevant. Prune outdated lines; one-line-per-decision style.
+
+- **DOCUMENTATION.md** (if present) — update affected sections only. Common targets: §8 file layout (new scripts/files), §9 regeneration commands (new build steps), §11 change log (always append).
+
+- **Methodology.md** (if present) — update only if the methodology actually changed (new metric, classifier change, new data source). Pure UI changes or refactors do NOT touch Methodology.md.
+
+- **README.md inside the shipping artifact** (if `results/<N>/README.md` exists and `build_app.py` regenerates it) — usually skipped because build_app.py overwrites on every build. Only edit if the change affects the embedding contract.
+
+- **Any other top-level .md file** discovered in the new-files inventory — needs a routing decision: is it a one-off note (leave it), a permanent reference (link from CLAUDE.md + DOCUMENTATION.md §8), or a deferred-concerns log (link from CLAUDE.md "Known Issues")?
+
+Each programmer agent gets:
+- The change summary from Phase 0.
+- The PM's scope summary from Phase 1.
+- The specific file(s) it owns and the sections to consider.
+- Explicit instruction: DO NOT touch any file outside its assigned set.
+
+### Phase 3 — Git commit + push (sequential, 1 agent or inline)
+
+If `origin` is configured, run inline (no sub-agent — git operations are quick and the orchestrator has the context):
+
+1. `git status --short` to confirm only expected files changed.
+2. `git diff --stat` for the commit-message subject material.
+3. Stage with `git add -A` (the .gitignore is trusted to exclude secrets and build outputs).
+4. Commit with a HEREDOC message:
+   - First line: imperative subject (≤72 chars) summarizing the change.
+   - Body: 3-8 lines covering what changed and why, mirroring the Phase 0 summary.
+   - Trailing: `Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>`
+   - Use env-var author (`GIT_AUTHOR_NAME` / `GIT_AUTHOR_EMAIL` / `GIT_COMMITTER_*`) if local `git config user.email` is unset — DO NOT modify git config.
+5. Push: `git push origin <branch>` (current branch, usually `master` or `main`).
+6. If the push is blocked by the harness's auto-classifier (external remote = potential data exfiltration), STOP and surface the block to the user with the explicit message they need to type to retry (`! git push origin master`). Do NOT try to bypass.
+
+If `origin` is NOT configured, skip Phase 3 entirely and tell the user that the commit was made locally only.
+
+## What to report at the end
+
+A compact 4-6 line summary covering:
+- PROJECT_SCOPE.md sections that PM updated (or "no changes needed").
+- Files synced in Phase 2 (CLAUDE.md, DOCUMENTATION.md, Methodology.md, etc.) with line-count deltas.
+- Commit SHA + push outcome.
+- Any blockers or follow-ups (e.g., harness blocked the push, new file needs a manual routing decision).
+
+## Anti-patterns
+
+- DO NOT run this skill after every conversation turn. It's a wrap-up tool.
+- DO NOT spawn a programmer to edit PROJECT_SCOPE.md — global rule says PM only.
+- DO NOT bypass the gitignore. Trust it; if something sensitive leaks, fix the gitignore in a follow-up commit rather than untracking files mid-push.
+- DO NOT re-derive the change summary inside each sub-agent's investigation — pre-compute it once and pass it down. Repeated git-log inside sub-agents wastes context and produces inconsistent framings.
+- DO NOT skip the report. The user needs to know what changed AND what didn't.
+
+## Failure modes and recovery
+
+- **PM says scope is unchanged**: Phase 2 still runs (docs may still need updates), Phase 3 still runs if there are uncommitted code changes.
+- **No code changes, just doc updates pending**: skip Phase 1 (nothing for PM to update), run Phase 2 + 3.
+- **Tests are red**: do NOT proceed. Tell the user to fix the tests first. This skill assumes the working tree is in a shippable state.
+- **Push blocked**: commit is preserved; user can retry with `! git push`.

+ 12 - 8
CLAUDE.md

@@ -27,11 +27,13 @@ This is the persistent context file for Claude Code. Keep it concise and useful.
 - `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
+- Chart canvases wrapped in `.chart-canvas-wrap` (position:relative, flex:1 1 auto, min-height:0) inside fixed-height `.chart-frame` (340px / 280px mobile) — fixes 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`
+- Per-Congress term data (startYear/endYear/district, death_year, current_member) only on individual `/v3/member/{bg}` endpoint — bulk listing is too sparse, so enrich_roster runs detail lookups for every replacement-chain member
+- Territorial House delegates report state="XX" in vote XMLs — parse.py overrides with the directory's real USPS code (AS/DC/GU/MP/PR/VI)
+- Roster merge: vote-derived roster gets enriched with `full_name`, `district`, `served_from/to`, `photo_url`, `served_partial`, `is_delegate`, `congress_term`, `death_year`, `current_member`, `replaces`, `replaced_by` 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
@@ -41,8 +43,8 @@ This is the persistent context file for Claude Code. Keep it concise and useful.
 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)
+- `parse.py` — XML → `votes.jsonl` + `roster.json`; rejects upstream strings with `<`, `>`, control chars; merges directory fields (delegate flag, congress_term, death_year, current_member, replaces/replaced_by) and overrides "XX" state for delegates
+- `enrich_roster.py` — three passes: (1) bulk Congress.gov → `members_directory.json` + LIS crosswalk; (2) rescue individual `/v3/member/{bg}` for vote-derived House bioguides missing from bulk (e.g. Gaetz); (3) replacement-linking by (state, district) within Congress window → `replaces`/`replaced_by`, then detail-enrich every chain member for term/death/current fields. 350ms throttle; `User-Agent: polisci-pipeline/1.0`
 - `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
@@ -50,7 +52,7 @@ fetch.py → parse.py → enrich_roster.py → parse.py → pytest → build_mem
 ## 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>/manifest.json` — picker index; per-member carries `k` (KPI dict for Rankings), `dl` (delegate), `un` (unseated), `rs`/`rb` (replacement refs), `sy`/`ey` (term years), `dy` (death year)
 - `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
@@ -58,9 +60,10 @@ fetch.py → parse.py → enrich_roster.py → parse.py → pytest → build_mem
 - `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
+- `app.html` — single-member dashboard: sidebar filters (chamber/party/state), typeahead, 8 KPI tiles (incl. Voted With GOP / Voted With Dem), 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
+- `ranking.html` — rank House or Senate members by any of 14 metrics; House/Senate radio + party checkboxes + metric dropdown + order radio; URL state; row click opens member dashboard
+- Structural banners on app.html via `renderNote()` priority: delegate > unseated > died > replaced_by > replaces > served_partial; successor/predecessor rendered as in-app links via `state.membersById`
 - Nav between pages: `Member · Compare · Rankings` in `<header>`
 - Member IDs in URL only (`?id=`, `?ids=`, `?c=&m=&o=&p=`) — deep-linkable, validated against manifest
 
@@ -101,4 +104,5 @@ fetch.py → parse.py → enrich_roster.py → parse.py → pytest → build_mem
 - 120th-Congress dry run impossible until 120th data exists
 
 ## Active Tasks
-- (none — Phase 7 closed)
+- Task 7.1 — Update `DOCUMENTATION.md` §8/§9/§11 for new file layout + regeneration commands + change-log
+- Task 7.3 — Delete `legacy/` after user confirmation

+ 56 - 2
DOCUMENTATION.md

@@ -384,8 +384,11 @@ polisci/
 ├── DOCUMENTATION.md            # this file
 ├── NOTES.md                    # deferred concerns; see file
 ├── PROJECT_SCOPE.md            # PM-owned scope record (created/updated by PM agent only)
+├── CLAUDE.md                   # Claude Code project context
+├── Methodology.md              # end-user methodology doc (copied into results/<C>/)
 ├── .env                        # gitignored — CONGRESS_GOV_API_KEY=...
 ├── .gitignore
+├── .claude/skills/bulk-update/SKILL.md  # project skill: post-change PM+docs+commit orchestration
 ├── fetch.py                    # idempotent network fetch
 ├── parse.py                    # XML → votes.jsonl + roster.json (+ merge w/ Congress.gov directory)
 ├── analyze.py                  # pure analytics; classify_vote + aggregate
@@ -400,6 +403,7 @@ polisci/
 ├── template/
 │   ├── app.html, app.css, app.js
 │   ├── compare.html, compare.js
+│   ├── ranking.html, ranking.js
 │   └── vendor/
 │       ├── chart.umd.min.js    # Chart.js 4.4.0
 │       └── sortable.min.js     # SortableJS 1.15.2
@@ -413,9 +417,10 @@ polisci/
 │   ├── 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
+│   ├── app.html, compare.html, ranking.html, app.js, compare.js, ranking.js, app.css
 │   ├── vendor/{chart…, sortable…}
 │   ├── data/{manifest.json, members/<id>.json}
+│   ├── Methodology.md          # copied at build time by build_app.py
 │   └── README.md
 └── legacy/                     # archived pre-pivot single-member dashboards
 ```
@@ -430,7 +435,7 @@ polisci/
 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
+python3 parse.py          --congress 119      # re-run: essential — re-does directory-merge now that members_directory.json + lis_to_bioguide.json are populated
 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>/
@@ -546,6 +551,55 @@ Generalizes to future Congresses via `--congress N`.
 
 Known limitations carried forward to v1.1: see `NOTES.md`.
 
+## 2026-05-24 — v1.1.0  Rankings page, structural banners, file:// support
+
+Three rounds of post-launch enhancements:
+
+**Rankings & UX (Round A)** — New `ranking.html` / `ranking.js` page sorts
+House or Senate members by any of 14 metrics with chamber/party filters and
+shareable URL state. Manifest now carries per-member KPI dict (`k`) so the
+page needs a single fetch. `build_app.py` inlines the full manifest into
+both HTML heads, restoring file:// support for the picker (per-member JSON
+still requires HTTP). Fixed a Chart.js infinite-growth feedback loop by
+giving `.chart-frame` a fixed height and wrapping each canvas in a
+`.chart-canvas-wrap` with `position: relative` + `flex: 1 1 auto`. Two new
+KPI tiles ("Voted With GOP" / "Voted With Dem", now 8 total). Page footers
+now link to the underlying clerk.house.gov / senate.gov XML and to a new
+`Methodology.md` (end-user methodology doc, also copied into the shipping
+artifact). New `CLAUDE.md` (Claude Code project context).
+
+**Delegate banner (Round B)** — Detected the 6 territorial House delegates
+(AS, DC, GU, MP, PR, VI) by USPS code; `parse.py` overrides the vote XMLs'
+"XX" state code with the directory's real code. Per-member JSON gets an
+`is_delegate` flag; `app.js` renders a yellow banner explaining that
+delegates may vote in committee and on Committee-of-the-Whole amendments
+but not on House final passage — their low participation rate is
+structural, not absenteeism.
+
+**Structural banners for member-elect / replaced / died (Round C)** —
+`enrich_roster.py` now does three extra passes beyond the original bulk
+fetch: (1) **Rescue** — individual `/v3/member/{bg}` lookups for any
+vote-derived House bioguide missing from the per-Congress bulk listing
+(recovers Matt Gaetz, FL-1, never-seated member-elect, plus his full name
+and term history); (2) **Replacement-linking** — pairs predecessor↔successor
+by (state, district) within the Congress window, emitting `replaces` /
+`replaced_by` bioguide refs; (3) **Detail-enrichment** — individual lookups
+for every member on a replacement chain to get accurate per-Congress
+`congress_term` (startYear/endYear/district) and `death_year` (the bulk
+listing only carries chamber + startYear). `parse.py` and `build_members.py`
+propagate these into the per-member JSON; `app.js` `renderNote()` branches
+by status with priority delegate > unseated > died > replaced_by > replaces
+> served_partial. Predecessor and successor names render as in-app links
+via `manifestById` lookup. 8 House replacement pairs auto-linked in the
+119th (Gaetz→Patronis FL-1, Waltz→Fine FL-6, Grijalva R.→Grijalva A. AZ-7,
+Turner→Menefee TX-18, Connolly→Walkinshaw VA, Greene→Fuller GA,
+Green→Van Epps TN, Sherrill→Mejia NJ).
+
+**Developer tooling (Round D)** — `.claude/skills/bulk-update/SKILL.md`
+defines a project-level Claude Code skill that orchestrates the full
+post-change cycle: PM updates PROJECT_SCOPE.md, parallel programmer agents
+sync CLAUDE.md and DOCUMENTATION.md, then git add/commit/push.
+
 ---
 
 ## 12. Senate Dashboard Plan (in progress)

+ 63 - 13
PROJECT_SCOPE.md

@@ -1,7 +1,7 @@
 # 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._
 
 ---
@@ -41,12 +41,13 @@ load.
 
 ## 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 |
 |---------|-------------|----------|
 | 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 |
 | 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) |
@@ -57,6 +58,10 @@ load.
 | 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 |
+| 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)
 
@@ -74,20 +79,21 @@ load.
 **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)
 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
+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`):**
 
 ```
-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
 3. App fetches each selected member's JSON; 5 overlay charts update
 4. URL updates to ?ids=<id1>,<id2>,... (shareable, capped at 6)
@@ -95,6 +101,17 @@ load.
 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
@@ -102,7 +119,7 @@ load.
 - **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
+- **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
 
 ---
@@ -114,20 +131,35 @@ load.
 - Vanilla JS (ES2020, no framework, no transpiler)
 - HTML/CSS (no preprocessor)
 - 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:**
 ```
 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)
                              → data/119/members/<id>.json × 552
+                               (carries: congress_term, death_year, current_member,
+                                replaces, replaced_by, is_delegate)
                              → data/119/manifest.json
+                               (carries: per-member KPI dict k, dl flag for delegates)
                                build_app.py
                              → results/119/  (embeddable artifact)
+                               (manifest inlined into app.html / ranking.html)
 ```
 
 **Data sources:**
@@ -135,6 +167,10 @@ Congress.gov API ─────────────────────
 - `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)
 
+**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:**
 - All upstream strings rendered via `textContent`, never `innerHTML`
 - `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.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)
 - [x] Task 7.2 — Write `PROJECT_SCOPE.md` reflecting shipped reality (this file)
 - [ ] 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
@@ -212,6 +259,8 @@ Congress.gov API ─────────────────────
 - [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
+- [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)
 
 ---
@@ -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.
 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.
+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.
 
 ---