diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3541834..cf3a9ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: push: branches: [main] pull_request: + workflow_dispatch: permissions: contents: read @@ -45,5 +46,8 @@ jobs: npm run validate:house-map npm run validate:legislative-data + - name: Validate launch product contract + run: npm run validate:launch + - name: Build static production export run: npm run build diff --git a/README.md b/README.md index c87d76e..4040c3c 100644 --- a/README.md +++ b/README.md @@ -14,14 +14,15 @@ Election Scenario Playground turns national swing and local race assumptions int ## Highlights -- **Three connected workspaces:** Explore presidential, House, and Senate outcomes from one responsive interface. +- **Three connected workspaces:** Explore House and Senate midterm scenarios, with historical presidential replay available for context. - **Historical replay:** Use calculation-ready state presidential baselines for every election from 2000 through 2024. - **Interactive election maps:** Inspect all 538 electoral votes, 435 voting House districts, and 100 Senate seats with margin-based color intensity. - **Real-time scenario controls:** Adjust national swing and demographic-style assumptions and see the map, counters, summaries, and pressure points update immediately. - **One-click stress tests:** Try youth-turnout, suburban-shift, rural-surge, low-turnout, and popular-vote/Electoral College split scenarios. - **Local overrides:** Apply state, district, or Senate-race adjustments to explore outcomes that a national model cannot capture. - **Decision context:** Review flipped states, tipping-point rankings, paths to 270, largest assumption effects, and a deterministic Monte Carlo uncertainty stress test. -- **Save and share:** Copy a scenario URL, bookmark named scenarios locally, preview an embed card, or export the presidential map as a PNG. +- **Save and share:** Copy a scenario URL, bookmark named scenarios locally, export a scenario card as SVG or PNG, or export the presidential map as PNG. +- **Progressive disclosure:** The map and essential outcome lead; customization, sharing, comparison, methodology, and stress testing remain available on demand. - **Accessible controls:** Keyboard-friendly native inputs, visible focus states, descriptive labels, and light and dark themes. - **Transparent data notes:** Model explanations and dataset readiness labels distinguish sourced baselines from illustrative assumptions. @@ -50,6 +51,7 @@ Open [http://localhost:3000](http://localhost:3000). npm run typecheck npm run lint npm test +npm run validate:launch npm run build npm run start ``` @@ -58,13 +60,13 @@ The project uses Next.js static export. `npm run build` writes the deployable si ## User Guide -1. **Choose a chamber.** Use the President, House, and Senate tabs to switch workspaces. Each chamber has its own map, seat counter, controls, and detail panel. -2. **Pick a baseline.** In the presidential workspace, choose a historical election year from 2000–2024. The 2024 baseline is selected by default. +1. **Choose a chamber.** Use House, Senate, or History to switch workspaces. House is the launch default; History contains presidential replay rather than a current forecast. +2. **Pick a baseline.** House uses the 2024 result baseline, Senate uses the latest completed race baselines, and History supports presidential election years from 2000–2024. 3. **Set the broad environment.** Move the national swing control toward Democrats or Republicans. Results and margins update as you drag. 4. **Try a preset or tune the electorate.** Open the scenario presets for a quick stress test, or expand the demographic-style controls to adjust individual inputs. Vote-shift controls label their partisan direction; turnout controls show higher or lower participation and use the model's state-specific sensitivities. 5. **Inspect the map.** Select a state, House district, or Senate seat to see its baseline, simulated margin, major drivers, and available local overrides. Darker fills indicate larger modeled margins, not greater certainty. -6. **Read the analysis.** The counters show chamber control; summaries surface flips and seat changes; pressure points show close states, tipping points, and paths to 270. Monte Carlo output is an uncertainty exercise built around the current scenario—not a win probability forecast. -7. **Save or share.** Copy the current link to preserve scenario inputs, save named bookmarks in the current browser, or export the presidential map. Anyone opening a shared URL receives the encoded scenario state. +6. **Read the analysis when needed.** Open **Advanced analysis and methodology** for summaries, pressure points, sensitivity, and Monte Carlo stress testing. Monte Carlo output is an uncertainty exercise—not a win-probability forecast. +7. **Save or share.** Open **Share, save, reset, and compare chambers** to copy the current link, save a named browser-local scenario, or export an SVG/PNG card. Anyone opening a shared URL receives the encoded scenario state. 8. **Reset when needed.** Reset controls return the relevant assumptions and overrides to their neutral baseline. ## How the Model Works @@ -111,6 +113,7 @@ The runtime has no API server or database. Scenario links use URL parameters, wh | `npm run validate:state-map` | Validate state/DC map coverage and related records | | `npm run validate:house-map` | Validate House district geometry coverage | | `npm run validate:legislative-data` | Validate legislative records and source metadata | +| `npm run validate:launch` | Validate the pre-launch product contract and regression guardrails | ## Repository Guide @@ -144,4 +147,5 @@ npm run build npm run validate:state-map npm run validate:house-map npm run validate:legislative-data +npm run validate:launch ``` diff --git a/docs/legislative-data-foundation.md b/docs/legislative-data-foundation.md index 3d321fd..6fbc93a 100644 --- a/docs/legislative-data-foundation.md +++ b/docs/legislative-data-foundation.md @@ -2,7 +2,7 @@ Last updated: 2026-07-06 -This document defines the public data needed for the House and Senate portions of Election Forecast Playground. The goal is a calculation-ready legislative layer: complete current rosters, clean election returns, stable geographic IDs, explicit incumbency and open-seat flags, transparent third-party handling, and source metadata that can be audited later. +This document defines the public data needed for the House and Senate portions of Election Scenario Playground. The goal is a calculation-ready legislative layer: complete current rosters, clean election returns, stable geographic IDs, explicit incumbency and open-seat flags, transparent third-party handling, and source metadata that can be audited later. The current app already has a first calculation baseline from MIT Election Data and Science Lab congressional results plus `unitedstates/congress-legislators` current roster data. The remaining work is mostly hardening: authoritative boundary ingestion, explicit tenure and incumbency fields, open-seat detection, presidential partisan baselines by district/state, and deeper historical comparison rules. diff --git a/docs/prelaunch-product-requirements.md b/docs/prelaunch-product-requirements.md new file mode 100644 index 0000000..74e6346 --- /dev/null +++ b/docs/prelaunch-product-requirements.md @@ -0,0 +1,378 @@ +# Election Scenario Playground: Pre-Launch Product Requirements + +Status: Approved implementation contract +Owner: Product +Last updated: 2026-08-26 +Target readiness window: Early October 2026 +Branch: `docs/prelaunch-product-prd` + +## 1. Product Decision + +Election Scenario Playground will launch as a focused, educational tool for exploring how political assumptions could change control of the U.S. House and Senate. Historical presidential scenarios remain available as a supporting mode, not as the lead product. + +The launch experience must feel like one clear activity: + +> Choose a chamber, adjust a small number of assumptions, and see which contests and control outcomes change. + +The platform may retain sophisticated analysis, sharing, and local customization, but those capabilities must not compete with the primary simulation loop. Depth is revealed when a user asks for it. + +## 2. Problem + +The product has enough capability to be useful, but capability has previously been represented as interface volume. Multiple chamber summaries, model diagnostics, sharing tools, detailed metadata, local overrides, portraits, and display options appeared with similar visual importance. + +This created four risks: + +1. New users could not immediately identify what to do. +2. The map and election outcome—the product's strongest assets—were visually diluted. +3. Historical data and heuristic scenarios could be mistaken for a live forecast. +4. Mobile users had to pass secondary controls before reaching the core experience. + +Pre-launch work must reduce these risks without deleting the expert capabilities that make the simulator credible and reusable. + +## 3. Goals + +### Primary goals + +- Make the first useful interaction understandable without instructions. +- Make House and Senate midterm exploration the unmistakable product focus. +- Keep the map, active outcome, and primary assumptions above secondary analysis. +- Ensure users understand that outputs are scenarios rather than calibrated forecasts. +- Provide a complete, usable experience on desktop and mobile. +- Preserve advanced capabilities through progressive disclosure. +- Reach a stable release candidate early enough for testing before October 2026. + +### Success definition + +A first-time user should be able to land on the site, understand the selected chamber, change an assumption, see the map and chamber outcome respond, inspect a competitive contest, and reset or share the scenario without external guidance. + +## 4. Non-Goals for Initial Launch + +The initial launch will not attempt to become: + +- A polling average or real-time election news service +- A calibrated probability forecast +- A complete 2026 candidate database +- A campaign-management or voter-targeting system +- A social network or public scenario marketplace +- An account-based cloud workspace +- A mobile-native application +- A replacement for primary election-data sources + +Features that imply these promises must not be added before launch unless the product boundary is formally changed. + +## 5. Target Users + +### Primary: politically curious students and voters + +They want to understand which assumptions matter and which seats could determine control. They need plain language, immediate feedback, and no requirement to understand modeling terminology. + +### Secondary: educators, journalists, and election enthusiasts + +They want to demonstrate scenarios, inspect individual races, compare outcomes, and share a reproducible state. They benefit from methodology and advanced analysis but do not need it permanently visible. + +### Not targeted at launch: professional campaign analysts + +The product does not provide the polling, fundraising, turnout-file, or continuously updated candidate infrastructure expected by professional campaign operations. + +## 6. Core User Journey + +The default journey must follow this order: + +1. **Orient:** See the product name, scenario disclaimer, selected chamber, baseline, and current chamber balance. +2. **Explore:** View the map and identify competitive contests. +3. **Adjust:** Change national swing or a small set of clearly labeled assumptions. +4. **Understand:** See the chamber counter, map, and selected contest update together. +5. **Inspect:** Select a state, district, or Senate race for local context. +6. **Deepen if desired:** Open customization, comparison, methodology, or stress-testing disclosures. +7. **Keep or share:** Copy a validated URL, export a card, or save locally. + +No optional feature may interrupt steps 1–4. + +## 7. Information Architecture + +### Primary navigation + +The required order is: + +1. House — `435 seats` +2. Senate — `100 seats` +3. History — `President` + +“President” must not return as the default or first navigation item before launch. Historical presidential exploration must remain visibly distinct from current-cycle midterm scenarios. + +### Page hierarchy + +Each chamber workspace must use this hierarchy: + +1. Chamber navigation and one theme action +2. Active scenario identity and active assumption summary +3. Essential outcome counter +4. Interactive map +5. Selected-contest context +6. Primary assumptions +7. Optional customization and analysis +8. Supporting data and methodology + +On mobile, the map must appear before selected-contest detail and optional analysis. Secondary content must never create horizontal overflow. + +## 8. Required Default Experience + +### 8.1 Header and navigation + +- Use the name **Election Scenario Playground**. +- State that the experience models scenarios rather than predicting results. +- Default to the House workspace on a bare URL. +- Preserve explicit and legacy scenario URLs. +- Present one theme toggle; do not present separate Light, Dark, and Focus Map choices. +- Do not restore Focus Map. The default workspace itself must prioritize the map. + +### 8.2 Active scenario strip + +The always-visible summary must contain only: + +- Active chamber or historical mode +- Baseline identity +- Active preset, if applicable +- Material assumptions that differ from the baseline + +It must not permanently show all three chamber outcomes, save controls, export controls, or comparison tools. + +### 8.3 Outcome counter + +The default counter must show: + +- Democratic and Republican totals +- Control threshold +- Clear balance visualization +- Control or tie status in plain language + +Vacancies, cycle counts, special handling, and similar metadata belong under **More chamber details** or **More electoral vote details**. + +### 8.4 Map + +- The map is the primary visualization and must receive the largest useful workspace area. +- Hover, focus, and selection states must be distinguishable. +- Keyboard navigation must remain supported. +- Competitive and data-limited contests must not be represented with misleading precision. +- House coverage and data-quality records belong under **Map coverage and data flags**. + +### 8.5 Primary assumptions + +- Show only inputs that create an understandable, material change in the active scenario. +- Use plain-language labels and brief contextual help. +- Provide safe defaults and a visible reset path. +- Updating an input must produce timely feedback in the map and outcome counter. +- Avoid duplicating the same assumption in multiple permanent panels. + +### 8.6 Selected-contest detail + +The default selected-contest view must prioritize: + +- Contest identity +- Baseline result or margin +- Current scenario result or margin +- Competitiveness or control relevance +- Important data-quality caveats + +Local overrides belong under **Customize this district/race** or **Customize this state**. Portraits and biographical information belong under **Member profile and seat context**. + +## 9. Progressive Disclosure Requirements + +The following capabilities must remain available but closed by default: + +| Disclosure | Contents | +| --- | --- | +| Share, save, reset, and compare chambers | Copy URL, export card, reset all, cross-chamber comparison, named local scenarios | +| More chamber details | Vacancies, seats in cycle, flags, threshold detail, and supporting totals | +| More electoral vote details | Supporting electoral-vote totals and special handling | +| Map coverage and data flags | Coverage, source, delegate handling, and known limitations | +| Customize this district/race | Local assumptions and district or active-race overrides | +| Customize this state | Presidential state override controls | +| Member profile and seat context | Portrait and secondary member information | +| Advanced analysis and methodology | Comparison, Monte Carlo, sensitivity, share preview, and methodology | + +Rules for disclosures: + +- Labels must describe the result of opening them, not use generic “More” or “Advanced” alone. +- Closed disclosures must not reserve large blank areas. +- Core status and blocking warnings must never be hidden. +- Disclosure state does not need to persist between visits for launch. +- Opening advanced analysis must not move or resize the map unpredictably on desktop. + +## 10. Content and Trust Requirements + +- Use “scenario,” “baseline,” “estimate,” or “simulation” instead of “forecast” when no calibrated forecast exists. +- Clearly identify the source year and type of every baseline. +- Distinguish completed results from current-cycle information. +- Surface low-data and heuristic limitations near the affected contest. +- Keep methodology accessible from every chamber mode. +- Avoid probability language unless the displayed value is supported by a documented probabilistic model. +- Do not imply that a named candidate is current without a validated source and date. + +## 11. Functional Requirements + +### Required for launch + +- Working House, Senate, and historical presidential simulations +- National swing controls and curated presets +- State, district, and active Senate-race overrides +- Responsive chamber maps and counters +- Competitive-contest lists and paths to control +- Validated, shareable scenario URLs +- Local named scenarios +- Reset behavior at local and global levels +- SVG/PNG scenario-card export +- Light and dark themes +- Keyboard-operable maps and semantic controls +- Data-source, limitation, and methodology access +- Graceful handling of invalid or outdated URL parameters + +### Optional only if stable before release freeze + +- Additional curated presets +- Editorial examples or classroom prompts +- Minor export-card styling enhancements +- Additional non-blocking explanatory copy + +### Deferred until after launch + +- Accounts and cloud synchronization +- Public scenario galleries +- Comments, reactions, or other social features +- Live polling ingestion +- Automated news feeds +- Notifications +- Additional display modes +- More permanent dashboard panels +- New model controls without validated explanatory value + +## 12. Usability and Visual Requirements + +- One visually dominant object per viewport section. +- No more than one primary action per component group. +- Secondary actions must be visually quieter than simulation inputs. +- Color cannot be the sole indicator of party, control, selection, or warning state. +- Labels and values must remain legible at 200% browser zoom. +- Mobile layouts must work at 320 CSS pixels without horizontal page scrolling. +- Interactive targets should be at least 44 by 44 CSS pixels where practical. +- Motion must respect reduced-motion preferences. +- Empty, loading, and invalid-data states must explain what the user can do next. + +## 13. Performance and Reliability Requirements + +- Production dependency audit must report no known high- or critical-severity vulnerabilities. +- Type checking, linting, automated tests, and all data validators must pass. +- Static production build and configured base-path build must pass. +- No uncaught browser-console errors in the primary House, Senate, and History journeys. +- No horizontal overflow on supported mobile widths. +- Input-to-visible-update latency should feel immediate for standard controls; target under 100 ms on a typical laptop after initial load. +- Maps must remain usable during rapid slider interaction. +- Shared URLs must restore the represented scenario or fall back safely with a clear baseline. +- Deployment must use supported GitHub Pages actions and complete from a clean checkout. + +## 14. Accessibility Requirements + +- All functionality must be reachable by keyboard. +- Focus order must follow the visual and task hierarchy. +- Visible focus indicators are required. +- Tabs, disclosures, sliders, buttons, and maps must expose meaningful accessible names and state. +- Results that change after input must be understandable without relying only on color. +- Heading structure and landmark regions must remain logical. +- Light and dark themes must maintain readable contrast. + +## 15. Analytics and Pre-Launch Evaluation + +If privacy-respecting product analytics are added, measure only what informs simplification: + +- Chamber selected +- First assumption changed +- Contest selected +- Disclosure opened by type +- Scenario shared or exported +- Reset used +- Client error category + +Do not block launch on analytics. Do not collect political preferences, entered labels, or personally identifying scenario content without a separate privacy review. + +Recommended moderated test tasks: + +1. “Make a scenario where control of the House changes.” +2. “Find a district that becomes competitive and explain why.” +3. “Switch to the Senate and change one race.” +4. “Send someone a link to your scenario.” +5. “Find out what data the simulator is based on.” + +Target: at least 4 of 5 first-time participants complete tasks 1–4 without intervention, and all participants can correctly explain that the result is a scenario rather than a forecast. + +## 16. Launch Acceptance Criteria + +The release candidate is approved only when all of the following are true: + +- [x] Bare URL opens House and presents a clear first action. +- [x] House and Senate are visually primary; History is visibly secondary. +- [x] Map and essential outcome appear before optional analysis on desktop and mobile. +- [x] No cross-chamber cockpit or expanded expert analysis appears by default. +- [x] Every advanced capability listed in Section 9 remains discoverable and functional. +- [x] Scenario language and baseline dates are accurate throughout the interface and exports. +- [x] Primary journeys pass keyboard, mobile, light-theme, and dark-theme review in the available Chromium environment. +- [x] Type checking, linting, tests, data validation, build, and dependency audit pass. +- [ ] Production deployment succeeds and public smoke testing passes. +- [x] Known limitations are documented and none contradict the product's public claims. +- [x] No unresolved severity-one or severity-two branch issue remains. + +## 17. Release Guardrails + +Before launch, reject or defer a proposed addition when any of these are true: + +- It introduces a new permanent panel for a secondary task. +- It duplicates information already visible elsewhere. +- It makes President appear like the current-cycle primary mode. +- It adds model precision that the underlying data cannot support. +- It pushes the map or core assumptions farther down the mobile experience. +- It requires an account for a journey that currently works locally. +- It increases operational dependence without a documented owner and fallback. +- It cannot be explained in one sentence to the primary user. + +When a useful feature fails only the hierarchy test, place it behind an existing disclosure before creating a new navigation destination. + +## 18. Proposed Timeline + +### By September 4: requirements freeze + +- Approve this PRD and product boundary. +- Convert acceptance criteria into an issue backlog. +- Identify data updates that are truly required for launch. + +### September 5–18: release-candidate implementation + +- Complete remaining functional and content issues. +- Avoid adding new product surface area. +- Keep all application work on feature branches. + +### September 19–25: structured validation + +- Run moderated usability tasks. +- Complete accessibility, mobile, performance, and data review. +- Fix only launch blockers and high-value clarity problems. + +### September 26–October 2: release freeze and launch candidate + +- Complete regression testing and production rehearsal. +- Finalize source dates, limitation language, and operational checklist. +- Tag the approved release candidate. + +### Early October: controlled launch + +- Deploy the approved release. +- Perform production smoke tests. +- Monitor errors and collect structured feedback before midterm usage increases. + +## 19. Resolved Product Decisions + +1. The simplified House-first build is the release-candidate baseline. +2. Launch does not require an unverified 2026 candidate refresh; labeled completed-result baselines remain the product boundary. +3. Initial launch omits analytics and uses structured evaluation plus repository reports. +4. The supported browser and device matrix is defined in `docs/release-operations.md`. +5. The repository maintainer owns data review, release approval, launch monitoring, and rollback. + +These decisions keep the pre-launch goal bounded and prevent the product surface from reopening during release stabilization. diff --git a/docs/prelaunch-release-evidence.md b/docs/prelaunch-release-evidence.md new file mode 100644 index 0000000..8f5d192 --- /dev/null +++ b/docs/prelaunch-release-evidence.md @@ -0,0 +1,83 @@ +# Pre-Launch Release Evidence + +Status: Release-candidate evidence +Branch: `docs/prelaunch-product-prd` +Last updated: 2026-08-26 + +Verified commit: `b314e39` + +Clean-checkout quality gate: [GitHub Actions run 32997013552](https://github.com/damonko1/demoPrediction/actions/runs/32997013552) — passed + +This matrix maps the pre-launch PRD to authoritative source, automated, build, and runtime evidence. Production deployment remains intentionally gated on review and merge into `main`. + +## Acceptance Matrix + +| PRD acceptance requirement | Status | Evidence | +| --- | --- | --- | +| Bare URL opens House with a clear first action | Pass | Browser opened `/` into the House tab; active strip identified the 2024 result baseline and the House swing control was available. URL parser tests cover the bare landing. | +| House and Senate primary; History secondary | Pass | Navigation and cross-chamber export order are House, Senate, History. `npm run validate:launch` enforces the order. | +| Map and essential outcome before optional analysis | Pass | At 320px, House map top was 749px, selected detail 2122px, controls 2601px, and optional analysis 3029px. No disclosure was open. | +| No permanent cross-chamber cockpit or expert analysis | Pass | Cross-chamber tools and analysis are closed native disclosures. Runtime audit found zero open disclosures on House, Senate, and History defaults. | +| Advanced capabilities discoverable and functional | Pass | Runtime accessibility snapshots exposed every named disclosure. SVG and PNG card downloads both completed successfully. Existing controls expose local overrides, sources, saved scenarios, comparison, sensitivity, Monte Carlo, and methodology. | +| Scenario language and source dates accurate | Pass | Header explicitly says “Scenario simulator — not a forecast.” House identifies the 2024 result baseline, Senate identifies latest-completed race baselines, and History identifies the selected result year. Data validators pass. | +| Keyboard, mobile, light, and dark review | Pass in Chromium | Tabs support arrow-key selection and focus. House-map ArrowRight moved focus and selection from PA-7 to PA-8. Light/dark toggle updated its accessible label. At 320px all non-SVG interactive targets reached 44px in both dimensions and the page had zero horizontal overflow. | +| Quality, data, build, and security gates | Pass | ESLint, TypeScript, 37 tests, state/Senate map validation, House map validation, legislative data validation, launch-contract validation, local static build, base-path build, and production dependency audit all passed. Audit reported zero vulnerabilities. | +| Production deployment and public smoke test | Pending merge | The branch is intentionally isolated and has not replaced the public `main` deployment. The Pages workflow uses supported actions and the base-path artifact builds successfully. | +| Known limitations consistent with public claims | Pass | `docs/data-accuracy.md`, `docs/launch-readiness-audit.md`, the PRD, and release runbook define completed-result baselines, heuristic limits, and non-goals. | +| No unresolved severity-one or severity-two issue | Pass for audited branch | No blocking issue remains from source, automated, data, build, security, or Chromium runtime review. Cross-browser production checks remain a release-procedure gate. | + +## Functional Capability Matrix + +| Required capability | Evidence | +| --- | --- | +| House simulation | House loaded by default; swing update changed active settings and chamber totals. | +| Senate simulation | Senate tab loaded with its 100-seat counter, state map, assumptions, selected race, and advanced disclosure. | +| Historical presidential simulation | History loaded with electoral counter, map, state selection, historical baseline controls, and advanced disclosure. | +| National swing and presets | Native sliders and closed preset disclosures are present in every relevant workspace. | +| State, district, and Senate-race overrides | URL round-trip tests and local-override tests pass; customization disclosures remain available. | +| Responsive maps and counters | 320px House, Senate, and History audits produced zero horizontal overflow. | +| Competitive contests and paths to control | Legislative and presidential summary components remain under advanced analysis. | +| Validated share URLs | URL tests cover round trips, malformed values, bounded values, legacy links, and malformed-tab House fallback. | +| Local named scenarios | Bookmark persistence tests pass; browser UI exposes save, load, and delete states. | +| Local and global reset | Chamber reset controls and consolidated Reset all remain available; default state correctly disables reset. | +| SVG/PNG card export | Both files downloaded through the browser from the consolidated tools disclosure. | +| Light and dark themes | Runtime toggle moved from `light` to `dark` and changed its label to “Use light mode.” | +| Keyboard maps and semantic controls | Tab semantics, named controls, visible focus CSS, and map arrow navigation verified. | +| Sources, limitations, and methodology | Data flags and advanced methodology disclosures are present in every workspace. | +| Invalid and outdated URL handling | New malformed-link tests pass; a malformed runtime URL returned to House, neutral assumptions, PA-7, and the labeled baseline without errors. | + +## Runtime Measurements + +| Measurement | Result | +| --- | --- | +| Chromium viewport | 320 × 800 CSS pixels | +| Horizontal overflow | 0px in House, Senate, and History | +| Closed disclosures on default load | 0 open | +| House swing visible-update latency | 30.6ms over two animation frames | +| Browser console | 0 errors, 0 warnings in audited journeys | +| Non-SVG mobile target minimum | 44 × 44 CSS pixels after remediation | +| Export output | 1200 × 630 SVG and PNG downloads | + +## Automated Gate + +The CI quality gate runs: + +1. dependency installation from the lockfile; +2. lint; +3. TypeScript; +4. all Vitest tests; +5. all map and legislative-data validators; +6. the pre-launch product-contract validator; and +7. the production static build. + +The quality workflow also supports manual dispatch so the feature branch can be tested from a clean GitHub checkout before a pull request or merge. Run `32997013552` passed every job for the verified implementation commit. + +## Remaining Release Actions + +These are deployment controls, not missing branch implementation: + +1. Review and merge the release-candidate branch. +2. Complete native Safari/WebKit confirmation on a supported environment; Playwright WebKit and Firefox are unavailable on the current macOS 12 ARM runtime. +3. Watch the clean-checkout Quality Gate and GitHub Pages deployment. +4. Smoke-test the deployed commit at the public URL and record its workflow links here. +5. Run the recommended moderated tasks with first-time users before the early-October launch window. diff --git a/docs/release-operations.md b/docs/release-operations.md new file mode 100644 index 0000000..d6a47d3 --- /dev/null +++ b/docs/release-operations.md @@ -0,0 +1,114 @@ +# Pre-Launch Release Operations + +Last updated: 2026-08-26 + +This runbook owns the operational decisions required by the pre-launch PRD. It applies to the release-candidate branch and to the eventual merge into `main`. + +## Release Baseline + +The simplified House-first interface is the release-candidate baseline. Pre-launch changes may fix correctness, accessibility, performance, content, or reliability issues. New permanent panels, navigation destinations, live-data dependencies, accounts, and additional display modes remain deferred. + +## Data Freeze Decision + +Launch does not require an unverified 2026 candidate dataset. House scenarios use the labeled 2024 result baseline; Senate scenarios use latest-completed race baselines; presidential results are historical replay. Generated data must pass all three repository validators. + +A source refresh becomes release-blocking only if: + +- an existing displayed source date or contest identity is false; +- a baseline record fails validation; +- a named candidate is described as current without a validated source and date; or +- the public product claim changes to include current polling or comprehensive 2026 candidates. + +## Analytics Decision + +Analytics are omitted from the initial launch. The static application collects no political preferences, scenario labels, or personally identifying scenario content. Early evaluation uses structured testing and repository issue reports. Analytics require a separate privacy review before introduction. + +## Supported Browsers + +The supported release matrix is: + +| Platform | Support target | +| --- | --- | +| Chrome and Edge desktop | Current and previous stable major | +| Firefox desktop | Current and previous stable major | +| Safari on macOS | Current and previous major macOS release | +| Safari on iPhone/iPad | Current and previous major iOS/iPadOS release | +| Chrome on Android | Current stable major | + +The minimum supported viewport is 320 CSS pixels. JavaScript, SVG, Canvas, `URLSearchParams`, and browser-local storage must be available. If clipboard access is denied, copy functions must use their provided fallback or report failure clearly. + +Before merging a release candidate, run the primary House, Senate, and History journeys in Chromium plus at least one WebKit/Safari environment. Firefox-specific failures are release blockers when they affect the primary simulation loop. + +## Ownership + +- **Release decision:** repository maintainer +- **Data validation:** repository maintainer running the committed validators +- **Deployment:** GitHub Actions using the Pages environment +- **Production monitoring:** repository maintainer for the launch window +- **Rollback decision:** repository maintainer + +No runtime API, database, scheduled ingestion job, or third-party analytics system requires a separate operator. + +## Severity Policy + +### Severity 1 — launch blocking + +- Site unavailable or blank +- House, Senate, or History cannot load +- Scenario calculations fail or corrupt shared state +- Material baseline or result misrepresentation +- Keyboard or mobile users cannot complete the primary simulation loop +- High- or critical-severity production vulnerability + +### Severity 2 — launch blocking unless explicitly accepted + +- Sharing, reset, local overrides, or export fails in supported browsers +- Persistent horizontal overflow at a supported width +- Advanced analysis or methodology is inaccessible +- Serious contrast, focus, labeling, or navigation defect +- Repeated uncaught console error in a primary journey + +### Severity 3 — may ship with documentation + +- Cosmetic inconsistency +- Non-blocking secondary-copy issue +- Minor layout defect outside the primary journey +- Enhancement request or deferred analytical capability + +No unresolved severity-one or severity-two issue may ship. + +## Release Procedure + +1. Confirm the release commit is on a feature branch and the worktree is clean. +2. Run `npm ci` from a clean checkout. +3. Run lint, type checking, all tests, all data validators, the launch validator, dependency audit, and both static builds. +4. Complete the browser acceptance matrix in `docs/prelaunch-release-evidence.md`. +5. Review public language, baseline dates, data flags, and export output. +6. Merge only the reviewed release candidate into `main`. +7. Watch both the Quality Gate and Deploy GitHub Pages workflows through completion. +8. Smoke-test the public URL without query parameters and with a known shared scenario URL. +9. Record the deployed commit and workflow links in the release evidence. + +## Launch Monitoring + +For the first hour after deployment: + +- verify the public URL after GitHub Pages cache turnover; +- test House, Senate, and History once from production; +- inspect GitHub Actions and Pages deployment status; +- test one shared URL and both scenario-card formats; and +- review new repository reports for severity-one or severity-two failures. + +Repeat the production smoke test after 24 hours. Because the application is static and has no telemetry or server, monitoring is availability- and report-based. + +## Rollback + +If a severity-one issue appears after merge: + +1. Identify the last verified production commit from the release evidence. +2. Create a normal revert commit for the faulty release; do not rewrite `main` history. +3. Push the revert to `main` and watch both workflows. +4. Verify the public URL and known scenario link after deployment. +5. Document the issue before attempting a replacement release. + +GitHub Pages retains workflow and deployment history, but the repository revert is the authoritative, reproducible rollback path. diff --git a/package-lock.json b/package-lock.json index 571492c..c3be3d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "election-forecast-playground", + "name": "election-scenario-playground", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "election-forecast-playground", + "name": "election-scenario-playground", "version": "0.1.0", "dependencies": { "lucide-react": "1.22.0", @@ -1940,16 +1940,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { @@ -2806,9 +2806,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -4919,9 +4919,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index 424f5c4..937f81a 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "election-forecast-playground", + "name": "election-scenario-playground", "version": "0.1.0", "private": true, "engines": { @@ -17,7 +17,8 @@ "build:legislative-data": "node scripts/build-legislative-data.mjs", "validate:house-map": "node scripts/validate-house-district-map.mjs", "validate:state-map": "node scripts/validate-state-map.mjs", - "validate:legislative-data": "node scripts/validate-legislative-data.mjs" + "validate:legislative-data": "node scripts/validate-legislative-data.mjs", + "validate:launch": "node scripts/validate-launch-readiness.mjs" }, "dependencies": { "lucide-react": "1.22.0", diff --git a/scripts/validate-launch-readiness.mjs b/scripts/validate-launch-readiness.mjs new file mode 100644 index 0000000..d23e633 --- /dev/null +++ b/scripts/validate-launch-readiness.mjs @@ -0,0 +1,128 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +const root = process.cwd(); + +function read(relativePath) { + return readFileSync(join(root, relativePath), "utf8"); +} + +function requireText(source, text, label) { + if (!source.includes(text)) { + throw new Error(`${label}: missing ${JSON.stringify(text)}`); + } +} + +function rejectText(source, text, label) { + if (source.includes(text)) { + throw new Error(`${label}: found forbidden ${JSON.stringify(text)}`); + } +} + +function requireOrder(source, values, label) { + let previousIndex = -1; + + for (const value of values) { + const index = source.indexOf(value, previousIndex + 1); + if (index === -1 || index < previousIndex) { + throw new Error(`${label}: expected ${values.map(JSON.stringify).join(" before ")}`); + } + previousIndex = index; + } +} + +const playground = read("src/components/Playground.tsx"); +const unifiedSummary = read("src/components/UnifiedScenarioSummary.tsx"); +const chamberCounter = read("src/components/ChamberCounter.tsx"); +const electoralCounter = read("src/components/ElectoralCounter.tsx"); +const houseMap = read("src/components/HouseDistrictMap.tsx"); +const legislativeDetail = read("src/components/LegislativeDetailPanel.tsx"); +const stateDetail = read("src/components/StateDetailPanel.tsx"); +const legislativeWorkspace = read("src/components/LegislativeWorkspace.tsx"); +const globalStyles = read("src/app/globals.css"); +const playgroundStyles = read("src/components/Playground.module.css"); +const metadata = read("src/app/layout.tsx"); +const packageJson = JSON.parse(read("package.json")); +const ci = read(".github/workflows/ci.yml"); +const deploy = read(".github/workflows/deploy-pages.yml"); + +requireText(playground, 'useState("house")', "House-first landing"); +requireOrder( + playground, + [ + '{ id: "house", label: "House", detail: "435 seats"', + '{ id: "senate", label: "Senate", detail: "100 seats"', + '{ id: "president", label: "History", detail: "President"', + ], + "Primary navigation order", +); +requireText(playground, "Election Scenario Playground", "Product identity"); +requireText(playground, "Scenario simulator — not a forecast", "Trust statement"); +rejectText(playground, "isFocusMode", "Map-first layout"); +rejectText(playground, "Focus map", "Map-first layout"); + +const disclosures = [ + [unifiedSummary, "Share, save, reset, and compare chambers"], + [chamberCounter, "More chamber details"], + [electoralCounter, "More electoral vote details"], + [houseMap, "Map coverage and data flags"], + [legislativeDetail, "Customize this"], + [stateDetail, "Customize this state"], + [legislativeDetail, "Member profile and seat context"], + [legislativeWorkspace, "Advanced analysis and methodology"], + [playground, "Advanced analysis and methodology"], +]; + +for (const [source, label] of disclosures) { + requireText(source, label, "Progressive disclosure"); +} + +for (const source of [ + unifiedSummary, + chamberCounter, + electoralCounter, + houseMap, + legislativeDetail, + stateDetail, + legislativeWorkspace, + playground, +]) { + rejectText(source, "
.simulationDockActions { + grid-area: tools-actions; +} + +.scenarioToolsBody > .unifiedSummaryGrid { + grid-area: tools-results; + min-width: 0; +} + +.scenarioToolsBody > .scenarioBookmarkDisclosure { + grid-area: tools-saved; + min-width: 0; +} + .scenarioBookmarkDisclosure { border: 0; background: transparent; @@ -6090,6 +6110,23 @@ } @media (max-width: 760px) { + .shell button, + .shell input:not([type="range"]):not([type="radio"]):not([type="checkbox"]), + .shell select, + .shell summary { + min-height: 44px; + } + + .shell button, + .shell select { + min-width: 44px; + } + + .shell summary { + display: flex; + align-items: center; + } + .unifiedSummary { grid-template-areas: "simulation-header" diff --git a/src/components/Playground.tsx b/src/components/Playground.tsx index e16dbd3..630f7b1 100644 --- a/src/components/Playground.tsx +++ b/src/components/Playground.tsx @@ -674,8 +674,8 @@ export function Playground() {
- State, seat, and chamber results update live - Simulation only + Adjust assumptions; results update live + Scenario simulator — not a forecast
diff --git a/src/components/ScenarioBookmarks.module.css b/src/components/ScenarioBookmarks.module.css index 717cf08..c4b0e1c 100644 --- a/src/components/ScenarioBookmarks.module.css +++ b/src/components/ScenarioBookmarks.module.css @@ -206,4 +206,8 @@ justify-content: flex-start; text-align: left; } + + .selectField select { + min-width: 44px; + } } diff --git a/src/components/UnifiedScenarioSummary.tsx b/src/components/UnifiedScenarioSummary.tsx index 83be078..0fa1609 100644 --- a/src/components/UnifiedScenarioSummary.tsx +++ b/src/components/UnifiedScenarioSummary.tsx @@ -42,7 +42,7 @@ function getActiveLabel(activeTab: SimulationTab) { return "Senate active"; } - return "President active"; + return "History active"; } function formatCountDelta(value: number, label: string) { @@ -175,31 +175,80 @@ export function UnifiedScenarioSummary({ queueStatusReset(); } - function exportSnapshotCard() { - const svg = ` + function createSnapshotCardSvg() { + return ` Election Scenario Playground - Full national scenario · Simulation only - President - D ${presidentialScenario.totals.democratic} - R ${presidentialScenario.totals.republican} - House - D ${houseScenario.controlTotals.democratic} - R ${houseScenario.controlTotals.republican} - Senate - D ${senateScenario.controlTotals.democratic} - R ${senateScenario.controlTotals.republican} + National scenario · Not a forecast + House + D ${houseScenario.controlTotals.democratic} + R ${houseScenario.controlTotals.republican} + Senate + D ${senateScenario.controlTotals.democratic} + R ${senateScenario.controlTotals.republican} + History · President + D ${presidentialScenario.totals.democratic} + R ${presidentialScenario.totals.republican} Custom assumptions ${stateOverrideCount} state · ${districtOverrideCount} district · ${raceOverrideCount} Senate race overrides Shared URL restores every national and local assumption `; - const blobUrl = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml" })); + } + + function downloadBlob(blob: Blob, extension: "svg" | "png") { + const blobUrl = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = blobUrl; - link.download = `national-election-scenario-${new Date().toISOString().slice(0, 10)}.svg`; + link.download = `national-election-scenario-${new Date().toISOString().slice(0, 10)}.${extension}`; link.click(); URL.revokeObjectURL(blobUrl); + } + + async function exportSnapshotCard(format: "svg" | "png") { + const svg = createSnapshotCardSvg(); + + if (format === "svg") { + downloadBlob(new Blob([svg], { type: "image/svg+xml" }), "svg"); + } else { + const svgUrl = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml" })); + const image = new Image(); + + try { + await new Promise((resolve, reject) => { + image.onload = () => resolve(); + image.onerror = () => reject(new Error("Scenario card image failed to load")); + image.src = svgUrl; + }); + const canvas = document.createElement("canvas"); + canvas.width = 1200; + canvas.height = 630; + const context = canvas.getContext("2d"); + + if (!context) { + throw new Error("Canvas is unavailable"); + } + + context.drawImage(image, 0, 0); + const pngBlob = await new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (blob) { + resolve(blob); + } else { + reject(new Error("PNG export failed")); + } + }, "image/png"); + }); + downloadBlob(pngBlob, "png"); + } catch { + setShareStatus("failed"); + queueStatusReset(); + return; + } finally { + URL.revokeObjectURL(svgUrl); + } + } + setShareStatus("saved"); queueStatusReset(); } @@ -243,9 +292,13 @@ export function UnifiedScenarioSummary({ {shareStatus === "copied" ? : } {shareStatus === "copied" ? "Copied" : "Copy summary"} -
-
- President - D {presidentialScenario.totals.democratic} / R {presidentialScenario.totals.republican} - {formatCountDelta(presidentialEvDelta, "D EV")} -
House D {houseScenario.controlTotals.democratic} / R {houseScenario.controlTotals.republican} @@ -274,6 +322,11 @@ export function UnifiedScenarioSummary({ D {senateScenario.controlTotals.democratic} / R {senateScenario.controlTotals.republican} {formatCountDelta(senateSeatDelta, "D seats")}
+
+ History · President + D {presidentialScenario.totals.democratic} / R {presidentialScenario.totals.republican} + {formatCountDelta(presidentialEvDelta, "D EV")} +
diff --git a/src/lib/scenarioUrl.test.ts b/src/lib/scenarioUrl.test.ts index 96fa792..66a814b 100644 --- a/src/lib/scenarioUrl.test.ts +++ b/src/lib/scenarioUrl.test.ts @@ -29,6 +29,22 @@ describe("landing workspace URL state", () => { "senate", ); }); + + it("returns malformed explicit tabs to the House launch default", () => { + expect( + simulationTabFromSearchParams( + new URLSearchParams("tab=unknown&state=PA&swing=2"), + ), + ).toBe("house"); + }); + + it("does not mistake malformed legacy fields for a presidential scenario", () => { + expect( + simulationTabFromSearchParams( + new URLSearchParams("state=ZZ&swing=broken&year=1900"), + ), + ).toBe("house"); + }); }); describe("local override URL state", () => { diff --git a/src/lib/scenarioUrl.ts b/src/lib/scenarioUrl.ts index 1e2207b..4413617 100644 --- a/src/lib/scenarioUrl.ts +++ b/src/lib/scenarioUrl.ts @@ -334,16 +334,28 @@ export function simulationTabFromSearchParams( return tab; } + // A present but unknown tab is not a legacy presidential URL. Treat it as + // an invalid modern link and return to the launch-default House workspace. + if (tab !== null) { + return "house"; + } + // Lead new and campaign-tagged landing URLs with the current midterm cycle. // Older presidential links did not include an explicit tab, so retain that // behavior only when an actual presidential scenario field is present. - const hasLegacyPresidentialState = [ - swingParam, - baselineYearParam, - selectedStateParam, - stateOverridesParam, - ...Object.values(demographicParams), - ].some((paramName) => params.has(paramName)); + const hasFiniteValue = (paramName: string) => { + const value = params.get(paramName); + return value !== null && value.trim() !== "" && Number.isFinite(Number(value)); + }; + const legacyYear = Number(params.get(baselineYearParam)); + const legacyState = params.get(selectedStateParam); + const hasLegacyPresidentialState = + hasFiniteValue(swingParam) || + (params.has(baselineYearParam) && + historicalElectionYears.includes(legacyYear as HistoricalElectionYear)) || + (legacyState !== null && validStateCodes.has(legacyState)) || + Object.keys(stateOverridesFromSearchParams(params)).length > 0 || + Object.values(demographicParams).some(hasFiniteValue); return hasLegacyPresidentialState ? "president" : "house"; }