OllinDocs
Architecture

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 to serving/…/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-up boundaries bridge with coverage marks, plus catalog.json. Published under serving/…/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.

On this page