diff --git a/README.md b/README.md
index 5e2c847..0ab22b0 100644
--- a/README.md
+++ b/README.md
@@ -6,19 +6,9 @@ Deep per-player statistics, head-to-head comparison, and first-principles match
simulation for **both the ATP and WTA tours** — built from raw match data, with the
working shown.
-> **Status: live, and Phases 1 to 5 are done.** Ingestion, the schema, the API, the Elo
-> engine, player pages, head-to-head, rankings, search, both simulators, the methodology
-> page, the Match Charting Project's per-set sheets, tournaments and seasons as pages, and
-> the serve and return leaderboards are all deployed at [deucepoint.net](https://deucepoint.net),
-> which has been up since 14 September 2026. Since then the home page shows this week's
-> events as they are played, refreshed hourly, and the head-to-head draws both players'
-> last year on tour as a percentile radar; see [Roadmap](#roadmap).
-

-**Live URL:** [deucepoint.net](https://deucepoint.net) — API at [api.deucepoint.net](https://api.deucepoint.net/api/v1/health)
-
----
+**Live:** [deucepoint.net](https://deucepoint.net) — API at [api.deucepoint.net](https://api.deucepoint.net/api/v1/health)
## Why this exists
@@ -43,12 +33,12 @@ interface that is designed rather than assembled.
```mermaid
flowchart LR
subgraph sources["Data sources (CC BY-NC-SA 4.0)"]
- A["Sackmann-lineage mirrors
ATP + WTA, all tiers
tour / Challenger / Futures / ITF"]
- B["Tennismylife
ATP 2025-26, WTA 2022-26"]
- C["Match Charting Project
shot-by-shot, to May 2026"]
+ A["Sackmann-lineage mirrors
ATP + WTA, all tiers"]
+ B["Tennismylife
current seasons + this week"]
+ C["Match Charting Project
shot-by-shot"]
end
- A --> I["cmd/ingest
streaming, idempotent"]
+ A --> I["cmd/ingest"]
B --> I
C --> I
@@ -57,493 +47,44 @@ flowchart LR
R --> PG
PG --> API["cmd/api
Go + chi"]
RD[("Redis 7")] <--> API
- API --> SIM["internal/simulate
closed form + Monte Carlo"]
+ API --> SIM["internal/simulate"]
SIM --> API
API --> WEB["web/
React + TS + Vite"]
-
- V["cmd/validate"] -.reads.-> PG
- DQ["cmd/dataqual"] -.reads.-> PG
-```
-
-Go handles ingestion, rating, simulation, and the API. PostgreSQL does the statistical
-heavy lifting through window functions. The frontend is a static SPA served from a CDN —
-deliberately **not** in the Kubernetes cluster.
-
-## Coverage
-
-| Tier | Matches (approx.) | Serve statistics |
-|---|---|---|
-| ATP tour | 195,000 | 89% (2005) → 99% (2022) |
-| ATP qualifying + Challenger | 223,000 | 0% (2005) → 99.7% (2022) |
-| ATP Futures | 447,000 | none, in any year |
-| WTA tour | 250,000 | 19% (2005) → 89% (2022) |
-| WTA qualifying + ITF | 488,000 | effectively none |
-| **Total** | **~1.63 million** | |
-
-Over **115,000 players** across both tours. WTA records reach back to 1923.
-
-Depth is uneven and the site is explicit about it rather than hiding it: Futures and ITF
-matches support win/loss, head-to-head, and Elo but carry no point-level data, so they
-cannot feed the simulator. Where a statistic does not exist, the site explains why instead
-of showing a zero.
-
-**Coverage is disclosed rather than papered over.** Full-schema data runs to within a few
-weeks of the present for the ATP tour, Challenger and qualifying and for the WTA tour, and
-stops in 2021 for ATP Futures and WTA ITF. The upstream repositories were withdrawn
-mid-project; the lower tiers stop where the last mirror does. Filling that with
-results-only data would put two data regimes in one database and produce
-plausible-looking wrong numbers, so
-[ADR-0006](docs/decisions/0006-accept-and-disclose-the-coverage-gap.md) accepts the gap
-instead. `GET /api/v1/coverage` reports the real dates, queried from the database, so the
-claim cannot drift from the data.
-
-Full detail in [`docs/methodology.md`](docs/methodology.md) and
-[`DATA_LICENSE.md`](DATA_LICENSE.md).
-
-## Run it locally
-
-Needs Docker and Go 1.23+.
-
-```bash
-make seed # Postgres + Redis, schema, ~4,100 real matches, and their ratings
-make api # http://localhost:8080
-```
-
-`make seed` takes about ten seconds. The fixture is small but deliberately covers every
-data regime the site has to handle — full serve statistics, partial, never recorded at
-this tier, and never recorded in this era — so the interesting behaviour is visible
-immediately rather than after a full ingest. See [`testdata/README.md`](testdata/README.md).
-
-Or step by step:
-
-```bash
-docker compose up -d # Postgres 16 + Redis 7
-make migrate-up # apply the schema
-make ingest # load the seed fixture
-make rate # compute the Elo ratings from those matches
-```
-
-Do not skip `make rate`. Ingest writes matches but no ratings, and the Elo leaderboard,
-the surface strips, the rating charts and both simulators all read the ratings table — so
-without it they come up empty, which reads as a broken page rather than a stage nobody
-ran.
-
-Postgres is published on **5433** and Redis on **6380**, not the defaults, because a local
-install very often already holds 5432 and 6379.
-
-Useful targets — `make help` lists them all:
-
-| | |
-|---|---|
-| `make up` | start the stack and wait until healthy |
-| `make down` | stop it, keeping data |
-| `make reset` | stop it and delete all data |
-| `make psql` | open a shell on the database |
-| `make seed` | stack, schema, seed fixture and ratings in one |
-| `make rate` | recompute the Elo ratings from every match |
-| `make api` | run the HTTP API on port 8080 |
-| `make test` | run the test suite (starts its own Postgres) |
-| `make lint` | run golangci-lint |
-
-### The API
-
-`make ingest` then `make api`, and the read-only API is up:
-
-```
-GET /api/v1/health readiness, including a database round trip
-GET /api/v1/coverage what is actually in the database, and through when
-GET /api/v1/players?q=&tour=&limit= fuzzy search, diacritic-insensitive
-GET /api/v1/players/:slug profile and career summary
-GET /api/v1/players/:slug/matches match history, filterable and cursor-paged
-GET /api/v1/players/:slug/ratings Elo trajectory, per surface
-GET /api/v1/players/:slug/rankings published ATP/WTA ranking over time
-GET /api/v1/players/:slug/clutch break points, tiebreaks, deciding sets
-GET /api/v1/players/:slug/percentiles last tour-level year, each axis a percentile of the tour
-GET /api/v1/players/:slug/seasons a career a year at a time, every rate with its own count
-GET /api/v1/players/:slug/highlights runs, biggest wins, titles by level, record by round, rivals
-GET /api/v1/h2h/:slug/:opponent head-to-head, either way round
-GET /api/v1/h2h/:slug/:opponent/common every opponent both have faced, with each side's record
-GET /api/v1/rankings?type=elo|official leaderboards, as of the last week that exists
-GET /api/v1/rankings/trajectory the leaders' rating lines, for a chart
-GET /api/v1/simulate/match?a=&b= point to match, every rung of the chain
-GET /api/v1/simulate/draw?event=&season= a played draw, replayed ten thousand times
-GET /api/v1/simulate/draws?tour=&season= every draw complete enough to replay
-GET /api/v1/tournaments?tour=&level=&q= the index, grouped by level, searchable
-GET /api/v1/tournaments/:slug an event across seasons, with how each got there
-GET /api/v1/tournaments/:slug/:season one edition as a draw sheet
-GET /api/v1/recent?tier=&through= the finals of the last complete week, both tours
-GET /api/v1/this-week events in progress, as results so far (hourly, provisional)
-GET /api/v1/seasons a row per year, both tours, with the Slam finals
-GET /api/v1/seasons/:year?tour=&tier= every event of a year at one tier, with its final
-GET /api/v1/leaders/:stat?tour=&tier=&surface=&season=&min_matches=
- a leaderboard, with its population and every row's denominator
-```
-
-```bash
-curl 'localhost:8080/api/v1/players?q=Djokovi%C4%87'
-curl localhost:8080/api/v1/players/novak-djokovic
-curl 'localhost:8080/api/v1/players/novak-djokovic/matches?surface=clay&season=2016'
-```
-
-The history takes `surface`, `tier`, `season` and `opponent`, and they compose. Each row
-carries that player's serve line for the match, or the reason there is none.
-
-A ranking is always **as of the last week that exists**, never today, and the response says
-which week it used. Asking for a date outside coverage returns an empty page with that date
-stated rather than a 404. An Elo leaderboard also drops players whose last rating is over a
-year old — without that it is a list of the retired.
-
-**The simulators show their working.** `/simulate/match` returns every rung between a point
-and a match, because the amplification is the claim: a two-point edge on serve becomes a
-twelve-point edge on the match. Point probabilities are derived from the ratings rather than
-from per-player serve statistics — which reach 3% of the players here — and anchored on the
-measured tour-and-surface average, so the response names its own inputs
-([ADR-0007](docs/decisions/0007-elo-derived-point-probability.md)). A pair the model cannot
-serve is an answer with a reason, not a coin flip.
-
-`/simulate/draw` replays a draw that was actually played, with the ratings as of the week it
-began, ten thousand times, and reports a confidence interval on every figure. There is no
-upcoming draw to simulate and there will not be one, so the simulator does the thing the data
-supports and can be scored against: Wimbledon 2019 gives Djokovic 39.9% ±1.0, and he won it.
-
-`/simulate/draws` is the list of every draw that can be replayed, which is narrower than the
-list of draws that exist: a round robin has no bracket to rebuild, and an edition the file
-recorded only the final of has nothing to play through. The endpoint applies that cut in SQL
-rather than letting the page offer an address the simulator would decline, so every row it
-returns is an `?event=&season=` the simulator will accept. The simulator opens on the top of
-that list -- newest season, biggest event -- rather than a draw hardcoded in the frontend, so
-which draw it starts on follows the data.
-
-**Under pressure is measured against a stated population.** `/clutch` reports break points
-saved, tiebreaks won and deciding sets won, each against what the tour did at the same
-levels in the same decades, weighted by where that player's own opportunities actually
-fell -- so a Futures career is not measured against a tour average, and a career spanning
-1995 and 2025 is not measured against either one alone. The population is in the response,
-because a "+4" against an unnamed average is a number pretending to be a fact. Tiebreaks
-and deciding sets are derived from the parsed score and the baseline is a table the ingest
-rebuilds, since neither belongs in a request. Two of the three averages come out at exactly
-50%, and [`docs/performance.md`](docs/performance.md) explains why that is arithmetic
-rather than a finding.
-
-The profile carries current and peak Elo for every series the player has one in — a surface
-they never played is absent rather than sitting at the base rating, and a player nothing
-rated has a null block rather than five 1500s.
-
-**Redis caches the read path, and an ingest clears it.** Nothing expires on a timer: the
-data only moves when an ingest runs, so that is when the cache is cleared, and the 24-hour
-TTL on a key is a backstop rather than a freshness policy. A cached response lands in about
-3ms whatever it cost to produce — the Elo leaderboard goes from 77ms to 3ms against a warm
-Postgres, and from 842ms against a cold one. Redis being down costs time and nothing else:
-every failure is a miss, the handler does the work anyway, and readiness still reports
-healthy. `X-Cache` says `hit` or `miss` on every response and `/health` carries the running
-hit rate, so the value is measured rather than assumed. Leave `REDIS_URL` unset and the API
-runs with no cache at all, which is what every test does.
-
-Statistics that were never recorded are reported as absent with a reason, never as zero —
-see [Coverage](#coverage). Errors are RFC 7807 `problem+json`, and list responses are
-cursor-paginated.
-
-`make ingest-full` performs the complete multi-decade ingest from the configured sources.
-
-Tests need Docker but no setup: the integration suites start a throwaway Postgres per
-package, apply the migrations, and stop it afterwards. Without Docker they skip with an
-explanation rather than failing. Set `TEST_DATABASE_URL` to use a server you already have;
-it must be named `*test*`, because some of those tests truncate tables.
-### The frontend
-
-```bash
-make web # Vite dev server on :5173, proxying /api to the local API
-make site # the whole stack in Docker: site on :5174, API on :8080
-make web-build # tsc, eslint, vitest and a production build
```
-The site is **Deucepoint**; the repository, the Go module and the compose project keep the
-name `tennis-wiki`, which is an identifier rather than a brand and is not worth the churn of
-renaming.
-
-React, TypeScript and Vite under `web/`, styled with CSS Modules and custom properties.
-No Tailwind, no component library, no charting library, no icon set. Every page is set as a
-sheet from the tournament office: one monospaced face, ruled lines, seeds in brackets, scores
-typed as the source writes them, and colour only where a value is surface-scoped. The design
-record is [`DESIGN.md`](DESIGN.md), the reasoning is
-[ADR-0010](docs/decisions/0010-the-draw-sheet.md), and `/_components` renders every
-component in every state, which is the fastest way to check the system against a page.
-
-`/simulator` is the third: two pickers, the chain from a service point up to the match, the
-chance of each set score, one match played out game by game from the chain's serve figures
-when asked (a sample of the model, with a board, a line of commentary and a tally counted
-from the points it played), and a draw played ten thousand times with an interval on every
-figure. The caption carries the matchup's own numbers rather than the design's, and says
-where the inputs came from. The head-to-head page's "Simulate this matchup" button leads
-here with both players. The page's layout and beats follow
-[`docs/design/prototypes/simulator.html`](docs/design/prototypes/simulator.html).
-
-`/tournaments/:slug/:season` is the page the design was built for: the edition as its draw
-sheet. From 880px the bracket is drawn and paged the way the printed sheet is -- a 128 draw as
-four quarters and then the last eight, a 56 draw as two halves and the last four, 32 and under
-on one sheet -- with a name written above its rule, the pair joined at the right, the winner's
-rule stepping in from the midpoint and the score under the name in the column it earned.
-Below 880px it is one round at a time behind a sticky stepper. The bracket is grown back from
-the results, since the files carry who beat whom and not which line anyone was on, so a seed
-with no first-round match sits above a typed `bye` and a row the file lacks reads `n/r`.
-`/tournaments/:slug` is every edition of an event as a row, printing how each one got there
-(ADR-0012), and `/tournaments` the index grouped by level with a search. Every tournament
-name on a match row links to its sheet, and the sheet's "Replay this draw" opens the
-simulator on the same edition.
-
-The home page carries last week's finals: the week that ended Sunday, as the finals of the
-tour-level events that began it, both tours, each a link to the sheet and to both players,
-with the date each tour's data is current to written on it. The site does not do live, and
-this is the fastest way to show a visitor it is current without saying the word. An
-off-season week says which week it is showing instead rather than vanishing, and a tour with
-no final that week says so.
-
-Above it, **This week** shows the tour-level events being played now as their results so
-far: how far each draw has got and its latest matches, fetched hourly from the source's
-ongoing files and labelled as provisional, with how long ago they last changed. Still not
-live scores: a result appears once the source records the finished match. These rows are
-kept apart from the match table and feed no rating until the weekly load brings the finished
-event in ([ADR-0016](docs/decisions/0016-this-week.md)).
-
-`/seasons` is the calendar a year at a time, both tours on every row since a season is the
-one place the two share one: the events on each surface as the site's squares with their
-counts, then the Slam champions in calendar order, and the women's rows reaching back to 1923
-rather than starting where the men's files do. A year in progress says the date each tour
-is complete to. `/seasons/:year` is every event of the year grouped by level, with a tier
-switch so a year at Challenger level is reachable rather than silently cut.
-
-`/leaders` is the leaderboards: one figure at a time -- eight serve figures, five return
-figures, total points and the dominance ratio, and five read from the score -- filtered by
-tour, tier, surface and season, every filter in the URL. The population is written above the
-table (how many matches met the filter, how many had statistics, how many players clear the
-floor) and every row carries the matches it stands on and the counts behind its rate, because
-a first-serve rate over two matches and one over two hundred are different claims. A name
-that is not in the table can be looked up, and the page says which absence it is: below the
-floor, with the figure over the matches they do have, or without the figure at all. It sits
-beside `/rankings` rather than in the nav, because a leaderboard is a ranking.
-**The player page is a dozen cards deep, so it carries its own table of contents**: a
-sticky rail under the header that tracks the section in view, every entry a real anchor so a
-section is a link somebody can send. Under it the career is read four ways the rest of the
-site cannot read it.
-
-*Serve and return are two cards, not one.* A return figure is made of the opponent's serve
-line, so its denominators are theirs — break points converted is the break points *they*
-faced, return games won is *their* service games — and the two are counted over different
-sets of matches, because a row can carry one side's line and not the other's. Below them sit
-the two figures that need both at once, over the matches that carried both: total points won,
-and the dominance ratio (return points won over serve points lost; 1.00 is a player who
-returns exactly as well as they are returned against).
-
-*Runs and the schedule* is the career as what happened: the longest winning run and the one
-in progress, found with a gaps-and-islands window over the match list, with retirements and
-walkovers left out of the sequence entirely — a run of wins broken by an opponent who never
-came out has not been broken by a defeat. Beside them, the average and the highest Elo the
-schedule actually faced, and the record above 2000. Every opponent carries the rating they
-held the week of the match, not the one they ended their career on, so a win over a future
-champion is credited with what it was worth at the time.
-
-*Biggest wins* ranks those same wins by that rating, which is the list every career average
-is an average of. *How far, and what was won* draws the record round by round as the funnel
-a career actually is, scaled against the busiest round, with titles over finals reached at
-each kind of event beneath it. *Most-played opponents* is who kept turning up, each row a
-link to the rivalry page.
-
-The player page cuts a career two more ways. By the opponent's ranking on the day -- the
-record vs No. 1, the top 5, 10, 20, 50 and 100, outside the top 100 and unranked, the bands
-nesting -- and by closeness, with the matches a final-set tiebreak decided; each is captioned
-with the matches it is a record over, since a ranking is known for some matches and not
-others. And year by year, one row per season with the record, titles, sets, games and
-tiebreaks won, and where serve lines exist the hold, break, ace and double-fault rates and the
-dominance ratio, every rate over its own count of matches: a season with four recorded matches
-and sixty played is the common case in the 1990s and reads as sixty played, four recorded.
-Both come from `player_totals`, rebuilt by the ingest, and were verified against a count
-straight from the match rows for a long ATP career and a short WTA one.
-
-`/h2h/:a/:b` is the comparison and `/h2h` the picker, which is the same page: the URL is the
-state, so a comparison is a link somebody can send, and asking the other way round is the same
-rivalry read from the other end. A rivalry argument is usually about a narrower thing, so the
-cut is in the URL too: level, round, best of, surface, the meetings that went the distance,
-the ones with a tiebreak, and a season range. The endpoint recomputes the record, the strip,
-the serve figures and the meeting list under the cut, and the score line writes the whole
-sentence -- "3-6 in finals, at Slams, of 41 meetings" -- because 3-6 is a different claim.
-The deciding sets and tiebreaks between them are the rivalry's summary and are never cut. A
-cut that leaves nothing is an answer naming the filter; two players who never met is a full
-page, not an error.
-
-**Through a common opponent** is the comparison a three-match rivalry cannot make. Two
-players who have met three times have usually played the same few dozen people, and how each
-did against that shared field says more about the matchup than three results do. Its own
-request rather than a block on the comparison: it is two whole careers grouped and joined,
-and the score at the top should not wait for it.
-
-**Charts read as well as they draw.** A career line whose only legible values are its two
-ends is a picture of a career rather than a record of one, so pointing at the rating history
-puts a crosshair on the nearest week and writes that week's date and Elo over the line. The
-same readout answers to the arrow keys and is announced politely, because "what was he rated
-in 2016" should not need a mouse.
-
-**Search is a command palette**, on ⌘K or Ctrl-K anywhere and on `/` when nothing else has
-focus. A field in the header could only ever open a player page, and the best pages on this
-site are comparisons; this reaches every player, every route, and both comparisons. Type a
-name, press Tab, and the palette becomes "head to head against…" or "simulate a match
-against…" — then search the other half and land on `/h2h/a/b`, which is otherwise two
-searches on two pages. With nothing typed it offers the players this browser has actually
-read, kept in local storage and nowhere else. It debounces and cancels superseded requests,
-and every row carries tour, country, career match count and best tier, because at 115,000
-players a name is not an identifier. `/players?q=` is the same search as a full list, and the
-query and tour filter live in the URL so a search can be sent to somebody.
-
-**A dark theme, on a three-state toggle**: follow the system, light, or dark. Three states
-rather than two, because a reader who has chosen nothing is not the same as one who has
-chosen the theme their machine happens to be on, and their site should keep following them
-at dusk. The choice is one attribute on `` and a block of custom properties; every
-colour that carries meaning — the two sides of a comparison, the four surfaces, win and loss
-— is lifted until it clears 4.5:1 on the panel it sits on. An inline script in `index.html`
-applies a stored choice before the first paint, so nothing flashes cream on the way to dark.
-
-**Every page carries structured data** for the crawlers that execute scripts: `WebSite`
-with the player search as its search action on the root, `Person` on a player, `SportsEvent`
-on an edition with the finalists as competitors, and `BreadcrumbList` on every route below
-the root. Every value is copied from an API response and nothing is invented to fill a slot
-the schema offers -- `SportsEvent` has a location and the files carry none, so it stays
-absent -- and a test holds the `Person` to carrying no field the profile did not.
-
-**The TypeScript types are generated from the Go response structs**, not written by hand.
-`make web-types` runs [tygo](https://github.com/gzuidhof/tygo) over `internal/httpapi` into
-`web/src/api/types.gen.ts`, and CI regenerates and diffs it on every change, so altering a
-handler's shape without updating the client fails the build rather than the browser. Same
-discipline as sqlc, one layer up.
-
-**The absence system is three components, not one.** `AbsentCell` is a missing value in a
-populated table, `PartialAggregate` is a summary over a gappy column that has to declare
-its denominator, and `EmptyState` is a whole section that explains itself and points
-somewhere with data. 83% of matches carry no serve statistics, so these render more often
-than the numbers do; collapsing them into a falsy check would throw away the distinction
-every layer below works to preserve.
-
-`docker compose up` builds and runs the whole stack, with the API same-origin behind nginx
-so the built site and the dev server behave identically. `make ingest-full` loads the
-complete dataset.
+Go handles ingestion, rating, simulation and the read-only API; PostgreSQL does the
+statistical work. The API, Postgres and Redis run on a single k3s node, with a weekly
+CronJob catching the data up and an hourly one for this week's results. The frontend is a
+static SPA on Cloudflare Pages. Design decisions are recorded in [`docs/decisions/`](docs/decisions/),
+and the system and runbook in [`docs/architecture.md`](docs/architecture.md) and
+[`docs/deployment.md`](docs/deployment.md).
## Rating methodology
-Ratings are computed from scratch over every match in chronological order — no ratings
-are imported from any API. Standard Elo, with a decaying K-factor so that a player's early
-matches move their rating far more than their five-hundredth:
+Ratings are computed from scratch over every match, replayed in draw order, with no
+ratings imported from anywhere. It is standard Elo with a K-factor that decays with
+experience, scaled by how much a match matters (a Grand Slam final at 1.20, Futures at
+0.60):
$$K(n) = \frac{250}{(n + 5)^{0.4}}$$
-where `n` is the number of matches that player has completed **before** the current one, in
-that series. This gives K = 131.33 for a debutant and K = 20.73 at 500 matches — the spec's
-gloss of "about 25" is wrong, and K reaches 25 at roughly 311 matches
-([ADR-0004](docs/decisions/0004-tier-taxonomy-and-elo-pool.md)). K then scales by a
-match-importance weight, from a Grand Slam final at 1.20 down to Futures at 0.60.
-
-Matches are replayed in draw order, not date order: nearly every tournament in the source
-carries a single date for all of its matches, so ordering by date alone would rate a final
-before the semi-final that produced its finalist. Walkovers are not rated — nobody played
-them — and team events are excluded by default.
-
-Five independent rating series are kept per player — overall, hard, clay, grass, carpet.
-A series is snapshotted only in the weeks it moved, which is what keeps the table at three
-million rows instead of two billion. For display and simulation they are blended:
+Each player has five series: overall, hard, clay, grass and carpet. For display and
+simulation, a surface is blended with overall by how much of it the player has played:
-$$\text{blended} = w \cdot \text{surface elo} + (1 - w) \cdot \text{overall elo}, \qquad w = \min\left(0.75, \frac{\text{surface matches}}{40}\right)$$
+$$\text{blended} = w \cdot \text{surface} + (1 - w) \cdot \text{overall}, \qquad w = \min\left(0.75, \frac{\text{surface matches}}{40}\right)$$
-so a player with five clay matches leans on their overall rating while a clay specialist
-with a hundred leans on their clay rating. `rating.Blend` implements it and reports the
-weight it used alongside the figure, because 1900 from a hundred clay matches and 1900 from
-three are the same number and different claims. A surface never played is absent rather
-than blended at 1500. Ratings are recomputed from scratch on every
-full ingest and never incrementally patched, so a bug fix is always one rerun away from
-correct.
-
-`make validate` replays the whole history and reports predictive accuracy and calibration
-per tier, mean reversion across the pool, promotion continuity, and what the simulation
-chain adds on top of the ratings. Tour-level accuracy is
-**69.7%**, inside the 68-72% the spec asks for. The tier weights are configuration rather
-than constants, so `validate --weights` tries alternatives without a rebuild; what the
-evidence says about them is in [the methodology](docs/methodology.md).
-
-The simulator is checked for what it can be checked for. Its match-level answer is the
-rating's by construction, so the interesting question is what the chain claims underneath:
-it expects 44.3% of matches to reach a deciding set and 34.8% do, which is the independent-
-sets assumption showing through, and it is written up rather than quietly corrected. Draw
-simulations of 298 real events score 0.833 on Brier against 0.969 for knowing only the field
-size. Both are in [the methodology](docs/methodology.md).
-
-The full derivation, including the simulation chain from point to match, is at
+At tour level the ratings pick the winner **69.7%** of the time, two points better than
+the official ATP and WTA rankings on the same matches. The full derivation, validation and
+simulation chain are in [`docs/methodology.md`](docs/methodology.md), rendered at
[deucepoint.net/methodology](https://deucepoint.net/methodology).
-## Data attribution and license
-
-This project is built on data originating from the work of **Jeff Sackmann /
-[Tennis Abstract](http://www.tennisabstract.com/)**, licensed
-**[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)**.
-
-> **Note (September 2026):** the canonical `JeffSackmann/tennis_atp` and
-> `JeffSackmann/tennis_wta` repositories are no longer public. Only
-> [`tennis_MatchChartingProject`](https://github.com/JeffSackmann/tennis_MatchChartingProject)
-> remains. This project therefore ingests from license-compliant redistributions of that
-> data. See [`DATA_LICENSE.md`](DATA_LICENSE.md) for the full provenance chain and the
-> exact sources used.
-
-The license is taken seriously here, because its author takes it seriously:
-
-- **Attribution** appears in the site footer on every page and in `DATA_LICENSE.md`.
-- **NonCommercial** — there are no ads, no payment flows, and no paid tier. Ever.
-- **ShareAlike** — all ingested and derived data, including the seed fixtures in this
- repo, is redistributed under CC BY-NC-SA 4.0.
-
-**Code** in this repository is licensed under the [MIT License](LICENSE). **Data**, and
-any dataset derived from it, is licensed under
-[CC BY-NC-SA 4.0](DATA_LICENSE.md). See
-[ADR-0001](docs/decisions/0001-dual-license-code-and-data.md) for the reasoning behind
-that split.
-
-## Deliberately not built
-
-- **Live in-play scores.** Requires a paid feed ($40/month at the low end, enterprise
- quote at the high end). The architecture leaves a websocket seam for it, but shipping it
- would mean either paying indefinitely or scraping — neither is defensible for a public
- non-commercial site. What the home page does show is this week's finished matches,
- hourly, from the free source the rest of the data comes from ([ADR-0016](docs/decisions/0016-this-week.md)).
-- **Betting odds, tipping, or predictions framed as picks.** The simulator reports
- probabilities and shows its working. It is not a gambling product and will not be
- shaped into one.
-- **User accounts, comments, social features.** They add moderation burden and privacy
- obligations without making the statistics better.
-- **Mobile apps.** The site is responsive to 360px; that is the right amount of mobile
- investment for this product.
-- **Doubles** _(v1)_. ~26,000 doubles matches exist and are not discarded, but they need a
- four-player match model and a separate rating treatment. The schema does not preclude
- them; the work is deferred.
-
-## Roadmap
-
-| Phase | Scope | Status |
-|---|---|---|
-| 1 | Ingestion, schema, read-only API | Done |
-| 2 | Elo engine, player pages, H2H, rankings, search, clutch, caching | Done |
-| 3 | Match simulator (closed form), draw simulator (Monte Carlo) | Done |
-| 4 | Methodology page, image builds, k3s, deployment, the Match Charting Project | Done |
-| 5 | Tournaments and seasons as pages, the draw sheet rendered, serve and return leaderboards, player and head-to-head splits | Done ([#131](https://github.com/sami0076/tennis-wiki/issues/131)) |
-
-Since Phase 5: the redesign in cream, ink and white ([ADR-0014](docs/decisions/0014-court-colour.md)),
-the head-to-head radar of tour percentiles ([ADR-0015](docs/decisions/0015-player-percentiles.md)),
-and this week's results on the home page ([ADR-0016](docs/decisions/0016-this-week.md)).
-
-Clutch metrics were pulled forward into Phase 2 and shipped there. Phase 3's shape was
-checked against the data before it was planned, and two things moved: the point-win
-probability cannot come from serve statistics alone — only 3,362 of 125,719 players have
-ten or more matches carrying them, against 75,021 who are rated — and there is no upcoming
-draw to simulate, so the draw simulator replays draws that were already played and scores
-itself against what actually happened. Both are written up in the tracking issue.
+## Data and license
-## Documentation
+The match data comes from the work of **Jeff Sackmann / [Tennis Abstract](http://www.tennisabstract.com/)**,
+licensed [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/), through
+license-compliant redistributions, since the original repositories are no longer public.
+The site is non-commercial, attributes the source on every page, and redistributes derived
+data under the same license. Provenance is in [`DATA_LICENSE.md`](DATA_LICENSE.md).
-- [`docs/architecture.md`](docs/architecture.md) — system design and the deployment topology
-- [`docs/methodology.md`](docs/methodology.md) — coverage, the rating engine, the simulation chain and what the validation says about them; rendered at [deucepoint.net/methodology](https://deucepoint.net/methodology)
-- [`docs/deployment.md`](docs/deployment.md) — how it is deployed, the runbook, and what it costs
-- [`docs/performance.md`](docs/performance.md) — measured ingest, size and query cost at scale
-- [`docs/decisions/`](docs/decisions/) — architecture decision records
+**Code** is [MIT](LICENSE); **data** is [CC BY-NC-SA 4.0](DATA_LICENSE.md)
+([ADR-0001](docs/decisions/0001-dual-license-code-and-data.md)).