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.
|
||||
Reference in New Issue
Block a user