Setup
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
# Snapshot and API contract, version 1
|
||||
|
||||
The backend is C++23 using Ligo HTTP and Noto/SQLite. `fpol_admin` and HTTP routes call the same domain functions. SQLite migration `001_snapshots` stores immutable JSON documents with typed metadata, catalog foreign keys and an import-time validator. This deliberately small persistence model preserves source fields and history; normalized league/draft/trade tables come with multiplayer.
|
||||
|
||||
## Import format
|
||||
|
||||
See the runnable documents in [data/demo](../data/demo). All documents require:
|
||||
|
||||
- `schema_version: 1`, a unique `id`, `kind` (`catalog`, `ratings`, `results`), `mode` (`demo`, `real`), `label`, and `as_of`.
|
||||
- `source: {name, url, retrieved_at}`. Real sources require an HTTP(S) URL. Only demo sources may use `fixture:` URLs.
|
||||
- UTC timestamps in `YYYY-MM-DDTHH:MM:SSZ` form. An adapter must normalize provider offsets, never invent observation times. `as_of` is the source observation time, `retrieved_at` the collection time. Older snapshots imported later keep their earlier position.
|
||||
|
||||
A catalog has a `complete` boolean, `races` and `candidates`. Each race needs `id`, `name`, `office` (`house`, `senate`, `governor`), two-letter `state`, and `round`. Each candidate needs `id`, `race_id`, `name`, `affiliation` (`democrat`, `republican`, `other`), `qualified: true`, `ballot_lines`, `provider_ids`, and `source`. IDs describe candidacies, not a person's name. Provider mappings are separate from those IDs. A provider ID may identify only one candidate. Qualification and fusion reconciliation are curator responsibilities; the validator checks structure, identities and explicit qualification, not external truth.
|
||||
|
||||
Ratings have `catalog_id` and `entries: [{race_id, rating}]`, where `rating` is `safe`, `likely`, `lean`, or `toss_up`. Missing entries stay unknown. New rating editions receive new IDs; a preview references exactly one edition. No draft exists yet to enforce a league's pre-draft snapshot selection.
|
||||
|
||||
Results have `catalog_id` and `entries`, each containing:
|
||||
|
||||
```json
|
||||
{
|
||||
"race_id": "demo-house-01",
|
||||
"round": "general",
|
||||
"status": "reporting",
|
||||
"total_votes": 1000,
|
||||
"winner_id": null,
|
||||
"votes": [
|
||||
{"candidate_id": "demo-house-01-1", "votes": 510},
|
||||
{"candidate_id": "demo-house-01-2", "votes": 490},
|
||||
{"candidate_id": "demo-house-01-3", "votes": 0},
|
||||
{"candidate_id": "demo-house-01-4", "votes": 0}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Statuses: `reporting`, `called`, `certified`, `pending_round`. Only called/certified rows have `winner_id`. `total_votes` may be null; candidate counts are nonnegative integers bounded at one billion, their sum cannot exceed the total. Totals may include nondraftable write-ins; those still belong in the vote-share denominator. An absent candidate row is unknown, not zero. Each document is a complete snapshot of its included race data, not a delta: omitted races do not inherit previous votes. Preserve per-race source timestamps as additional fields when a future adapter combines sources; the current fixtures use one observation time per document.
|
||||
|
||||
Duplicate races, duplicate candidate counts, foreign-race candidates, conflicting modes, unknown references and mismatched rounds are rejected. Combine fusion ballot counts explicitly upstream, retaining original evidence. Missing or pending-round scores are returned as null. Runoff/RCV progression is a launch blocker, not inferred from reporting status.
|
||||
|
||||
An identical repeat import is a no-op; conflicting content under the same ID fails. Corrections get a new ID and observation time, including call retractions. A single document insertion is atomic. Multi-file CLI imports commit each file independently in argument order, so import catalogs before dependent snapshots. Persisted history is never updated by the importer; replay requires no rollback writes. Administrators should run one importer at a time; simultaneous identical imports can produce a uniqueness error that is safe to retry.
|
||||
|
||||
## Scoring and roster semantics
|
||||
|
||||
`(100 × candidate_votes / total_votes + awarded_win_bonus) × race_multiplier`.
|
||||
|
||||
The awarded bonus is 50 only for the explicit called/certified winner; other candidates receive zero. Multipliers are Safe 1.00, Likely 1.10, Lean 1.25, Toss-up 1.50. Integer arithmetic rounds each candidate half up to two decimals before summing. More votes for a candidate with opponents' counts fixed cannot reduce their score. Shares can still fall as opponents receive votes; corrections can lower totals or retract bonuses.
|
||||
|
||||
Every office has one D, R, Other and Flex slot. Flex accepts any affiliation within that office. A candidacy may occupy only one roster slot; opponents may both be rostered. Partial rosters are valid for exploration but have no complete total. Cross-team ownership, draft locks and trade enforcement belong to the multiplayer milestone.
|
||||
|
||||
`leader_preview_points` is hypothetical: only an untied leader with all catalog candidate rows present and all votes accounted for receives a hypothetical bonus, and an explicit call always takes precedence. It is separate from awarded points. A tied or incompletely reported lead has a null preview; `unavailable_leader_previews` counts gaps, and the preview subtotal only includes available previews. Missing scores do not become zero; `known_subtotal` sums only available scores and `missing_scores` reports gaps. `total` exists only when all twelve slots have valid, available scores. A complete total can still be provisional during reporting.
|
||||
|
||||
## HTTP
|
||||
|
||||
All responses use Ligo's `{ok: true, payload: ...}` or `{ok: false, code, message}` envelope. Invalid input is HTTP 400; roster eligibility errors are a successful validation response with `valid: false`. Unknown routes are 404. No import/write/admin route is exposed.
|
||||
|
||||
| Method / path | Request | Payload |
|
||||
| --- | --- | --- |
|
||||
| `GET /api/health` | None | `{status: "ok"}` process liveness |
|
||||
| `GET /api/rules` | None | Version, slots, multipliers, formula and rounding |
|
||||
| `GET /api/catalog` | None | `{documents: [...]}`, ordered by `as_of`, then ID |
|
||||
| `POST /api/capacity` | `{catalog_id, players}` | Per-office party and total shortages |
|
||||
| `POST /api/score-preview` | `{catalog_id, ratings_id, results_id, roster: [{slot, candidate_id}]}` | Validation, breakdown, baseline and leader preview |
|
||||
|
||||
The complete document response is intentionally suitable for this small prototype; pagination/indexed views will be needed as real replay history grows. Exact routes have no query-string normalization; the browser filters locally. Imported data becomes visible on the next request without restarting. The browser loads its catalog snapshot on page load; refresh after imports. No authentication is implemented yet; use local development until the multiplayer access controls exist.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Data feasibility — checked 2026-10-04
|
||||
|
||||
**Real-league launch is blocked on national candidate coverage, frozen ratings, and verified automatic results/calls.** The prototype is usable with synthetic data. No live provider adapter or polling service is enabled, and no historical scoring backtest is claimed.
|
||||
|
||||
## Scope and evidence
|
||||
|
||||
The [USPS election calendar](https://about.usps.com/postal-bulletin/2026/pb22694/html/cover_004.htm) lists November 3, 2026, with 435 House seats, 35 Senate seats and 36 state governorships. This milestone assumes the 50 states, including safe and uncontested races. Territories, nonvoting delegates and separate special House contests need an explicit scope decision before a national catalog is frozen. The [150-row matrix](../data/coverage_2026.csv) distinguishes offices without a scheduled election from missing or unverified data. It is an office/state checklist, not a verified seat-by-seat national manifest.
|
||||
|
||||
The [Michigan official general-election list](https://mi-boe.entellitrak.com/etk-mi-boe-prod/page.request.do?electionType=GEN&electionYear=2026&page=page.miboePublicReport) was readable. Its 13 House races, Senate race and governor race were manually transcribed into [a 73-candidate snapshot](../data/real/mi-2026-10-04.json). Entries marked DISQ were excluded. Governor tickets use the gubernatorial candidate. Provenance dates have day precision; midnight UTC is a storage convention. The source can change: independently reconcile the transcription, ballot qualification and replacements before a draft. Empty provider mappings mean they have not been matched to any results feed. This catalog is deliberately `complete: false`.
|
||||
|
||||
[FEC information](https://www.fec.gov/introduction-campaign-finance/election-results-and-voting-information/) is useful for retrospective federal results; campaign filings are not proof of general-election ballot qualification. State election offices remain the catalog authority.
|
||||
|
||||
## Provider findings
|
||||
|
||||
| Source | Verified evidence | Still unverified / missing |
|
||||
| --- | --- | --- |
|
||||
| [civicAPI documentation](https://civicapi.org/api-documentation) | Advertises free, unauthenticated real-time results and calls; documents a v2 API base | Concrete race endpoint, response schema, 2026 office/state coverage, stable candidate IDs, call provenance, timestamp and correction behavior |
|
||||
| [Open America API contract](https://openamerica.io/elections/api/) | Documents a free results endpoint, 2024 replay and 22-state coverage | Live payload retrieval, individual office/seat coverage and identity reconciliation; no calls or pre-election candidate catalog |
|
||||
| State election offices | Michigan catalog retrieved | National catalog and automatic feed coverage |
|
||||
| Commissioner ratings import | Versioned, sourced snapshots supported | No real pre-draft ratings imported; no dated historical rating dataset verified |
|
||||
|
||||
Open America documents `https://openamerica.io/elections/results.json?year=2026&office=House`; substitute Senate/Governor or year 2024. Its `final` flag concerns precinct reporting, not certification. Candidate names and parties require explicit internal-ID matching. Follow its 60-second polling guidance, conditional requests, gzip, attribution and backoff. State inclusion does not guarantee every office or uncontested race. Outside the documented 22 states, coverage is missing. Calls are missing everywhere.
|
||||
|
||||
Attempts to open provider JSON endpoints through the available research tools returned no usable payload. civicAPI `/api/v2/status`, `/api/v2/races`, and the API root were exploratory guesses, **not verified endpoint contracts**. This is not evidence of a provider outage. Shell networking is restricted here. No provider's transport, election-night reliability or correction behavior has been validated end to end.
|
||||
|
||||
## Capacity findings
|
||||
|
||||
The C++ capacity endpoint checks 2–12 players. For **each office**, twelve teams need at least 12 Democrats, 12 Republicans, 12 Others and 48 distinct candidates overall. The total requirement reserves enough candidates for Flex after the affiliation slots are filled. These conditions are sufficient for these disjoint office/affiliation constraints; they say nothing about data coverage.
|
||||
|
||||
| Imported catalog | House D / R / Other / total | Senate | Governor |
|
||||
| --- | --- | --- | --- |
|
||||
| Synthetic | 12 / 12 / 24 / 48 | 12 / 12 / 24 / 48 | 12 / 12 / 24 / 48 |
|
||||
| Michigan partial | 13 / 13 / 36 / 62 | 1 / 1 / 4 / 6 | 1 / 1 / 3 / 5 |
|
||||
|
||||
The fictional catalog supports every allowed league size. Michigan alone cannot fill two complete rosters because Senate and Governor each have only one D and one R. **Nationwide feasibility is unknown**, not disproven. Obtain the remaining qualified candidates before deciding whether the Other slots work for twelve players. Do not silently relax the roster rules.
|
||||
|
||||
## Gates before multiplayer launch
|
||||
|
||||
1. Reconcile every in-scope seat and qualified candidate, including fusion lines, independents, uncontested seats and replacements. Use one candidacy ID per candidate/race; preserve the original ballot labels and commissioner-assigned roster affiliation. Never draft aggregate write-in buckets.
|
||||
2. Capture actual provider responses and retain their URL, retrieval time and payload. Confirm candidate-level IDs or reviewed mappings, vote denominators, timestamps, rounds, calls/retractions and corrections. A party leader or 100% precincts is never a call.
|
||||
3. Obtain sourced, dated ratings for every race; freeze the selected immutable snapshot before drafting. Unrated races block that gate; they do not default to Safe.
|
||||
4. Verify real capacity for the selected player count, then rehearse automatic polling and replay with outage/staleness handling. Free automatic coverage is a launch requirement. Resolve unsupported states and calls before promising a complete election-night experience.
|
||||
5. Settle runoff and ranked-choice scoring policy. The current importer rejects a different round for the same catalog race and `pending_round` remains unscored. A later round must not be added to an earlier count. Do not launch affected races until provider semantics and final-round handling are specified and tested.
|
||||
|
||||
Commissioner JSON imports make the foundation reviewable; they are not the planned election-night automation. If free sources cannot meet these gates, bring that concrete limitation back for a product decision rather than introducing a paid dependency or silently narrowing the game.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Local verification — 2026-10-04
|
||||
|
||||
## Passed
|
||||
|
||||
- Both `fpol_server` and `fpol_admin` compile in Debug and Release using the available **GCC 16.2.1** host compiler and C++23.
|
||||
- 25 core tests pass against the production admin executable in both configurations. These cover Noto migrations, reopening SQLite, import idempotency, immutable IDs, corrections, chronological replay, mode isolation, roster eligibility, rounding, capacity, missing data, ambiguous leader previews and round mismatches.
|
||||
- The real Michigan snapshot imports successfully; candidate counts match the coverage report. Demo and real data are seeded in the ignored local `var/fpol.sqlite`.
|
||||
- Strict TypeScript checking and the Vite production build pass.
|
||||
- Shared Modulus formatting and `git diff --check` pass. The bootstrap is unchanged from the canonical shared file.
|
||||
- Server help and invalid-startup tests pass without sockets.
|
||||
|
||||
## Environment limits
|
||||
|
||||
The full Debug CTest run executes all 25 core tests successfully, then runs eight server tests. Two server tests pass; six cannot execute their socket-dependent paths because the sandbox rejects socket creation with `Operation not permitted`. The suite is deliberately still failing here, not skipped or reported green. HTTP success/error envelopes, real request transport, static delivery and shutdown need the normal development/CI environment. A browser interaction/visual check also remains outstanding.
|
||||
|
||||
Docker's socket is inaccessible in this environment, and GCC 12 is not installed on the host. Consequently, the configured Modulus GCC 12/container workflow could not be verified here. The fallback builds local Modulus/Noto sources into ignored directories under this repository and uses already-cached Boost/OpenSSL/SQLite plus the existing local Ligo build. Release application compilation maps cached dependency configurations to Debug; this is an optimized application check, not a fully optimized dependency build or performance measurement. No shared library sources or dependency caches were modified.
|
||||
|
||||
The committed Gitea workflow still runs the intended GCC 12 debug/release builds and the full HTTP suite. Run it, or the README's normal Modulus checks, before merging.
|
||||
|
||||
## Reproduce these local fallback checks
|
||||
|
||||
These paths are local artifacts, not portable prerequisites:
|
||||
|
||||
```sh
|
||||
cmake --build build/kickoff_debug
|
||||
cmake --build build/kickoff_release
|
||||
python3 tests/test_core.py build/kickoff_debug/fpol_admin
|
||||
python3 tests/test_core.py build/kickoff_release/fpol_admin
|
||||
ctest --test-dir build/kickoff_debug --output-on-failure
|
||||
npm --prefix frontend run build
|
||||
```
|
||||
|
||||
The local database and frontend assets are already prepared. Outside the socket-restricted sandbox, the locally built prototype can be started from this repo root:
|
||||
|
||||
```sh
|
||||
build/kickoff_debug/fpol_server --web-root frontend/dist
|
||||
```
|
||||
|
||||
Open `http://127.0.0.1:8080`. Provider JSON transport and automatic election-night coverage are separate outstanding checks listed in [the data report](data-coverage.md).
|
||||
Reference in New Issue
Block a user