System overview
Teacher solver → sealed backbone → extraction ETLs → serving packs → stateless API → this app.
The system is two planes with a hard wall between them. The data plane runs solvers offline and writes plain artifacts to cloud storage. The serving plane reads those artifacts and never computes strategy — and never links solver code.
The stages
1. Teacher solver (offline)
The postflop teacher is a fork of b-inary/postflop-solver (Rust,
AGPL-3.0-or-later). It runs only in the offline data plane — separate repos,
separate processes — and its sole output is data files.
It never runs in the serving path; the API repo carries a CI deny-gate
that fails on any AGPL dependency, denylisted symbol, or solver package name.
Details: the AGPL boundary.
Preflop is separately clean: the trees are the CEO's own HRC solve output — a proprietary tool, company-owned results — exported to JSON. No solver code is involved in producing or serving them.
2. Sealed backbone
The berserk_v1 campaign solved 242 heads-up arriving spots × 184 canonical
flops = 44,528 solves (accuracy ≤ 0.3% pot each), producing 133,584 result
artifacts (~468 GB): per-solve root output, compressed turn/river boundary
records (129.6M of them), and logs. The campaign was sealed as an
immutable snapshot — copy verified object-for-object, live decks never
mutated — under the dataset stamp 20260721T131354Z, with a read copy in
gs://ollin-solver-data/backbone/berserk_v1/20260721T131354Z/.
The 184-board set is the strategically distinct subset chosen from the 1,755 suit-isomorphic flop classes (out of 22,100 raw flops); every spot solves all 184.
3. Extraction ETLs
Two extractors in solver-lab/data_plane turn backbone artifacts into
serving packs:
extract_roots_v0.py— reads each family's 184 root solves plus one ranged line of each boundary file (never the multi-GB payloads), and emits one JSON pack per family (1.6–6.5 MB) with per-combo root strategies, EVs, and both arriving ranges, plus a manifest with sha256 content addresses. Uploads continuously toserving/…/v0-roots/.extract_preflop_v0.py— walks the HRC node trees with the validated converter (99/99 vs reference at 30bb) and emits one pack per depth (packs/100bb.json, …) carrying every decision node, the arriving ranges, and the heads-upboundariesbridge with coverage marks, pluscatalog.json. Published underserving/…/preflop-v0/.
4. Serving packs in GCS
Everything under a (campaign, stamp) prefix is immutable — a new
extraction gets a new stamp. That single property powers the whole caching
model: packs are verified against their sha256 on read and cached in-process;
responses carry strong content-derived ETags with year-long CDN lifetimes.
5. ollin-study-api
A stateless, read-only Fastify service. It adapts producer packs to the
frozen wire contracts (NodeDocument v1, Preflop v1), aggregates the 13×13
grid server-side so every client sees one derivation, and serves typed errors
for everything it cannot serve — including the deliberate
501 TIER_NOT_AVAILABLE on the future tree endpoint. Full details:
API reference.
6. This app
Next.js App Router. Server components fetch from the API (or fall back to the deterministic mock dataset), view-models derive every display value from the wire documents, and components never touch wire shapes. Full pipeline: Data flow.
One contract, growing coverage
The serving tiers — root (now), tree (extraction campaign), live
(real-time) — all speak the same NodeDocument v1. Coverage grows; the
contract, the endpoints, and the UI reading them do not change. That is why
unbuilt features (tree navigation, the Breakdown tab) appear in the UI as
locked slots rather than being absent.