Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions .github/workflows/basic.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ jobs:
toolchain: ${{matrix.rust}}
components: rust-src
- uses: Swatinem/rust-cache@v2
- run: cargo test --all
# `full` is the 0.14.0 default set; the lean builds are covered by
# the no-default-features job and `make check-purity`.
- run: cargo test --all --features full

clippy:
name: Clippy
Expand All @@ -52,7 +54,7 @@ jobs:
with:
components: clippy, rust-src
- uses: Swatinem/rust-cache@v2
- run: cargo clippy -- -Dclippy::all -Dclippy::pedantic
- run: cargo clippy --features full -- -Dclippy::all -Dclippy::pedantic

# Semver posture (AUDIT_Fable_5.md P6): compares the crate's public API against
# the last version published to crates.io and fails if the change is breaking
Expand Down Expand Up @@ -91,7 +93,7 @@ jobs:
toolchain: stable
components: rust-src
- uses: Swatinem/rust-cache@v2
- run: cargo doc --no-deps
- run: cargo doc --no-deps --features full
env:
RUSTDOCFLAGS: -Dwarnings

Expand All @@ -109,16 +111,14 @@ jobs:
# examples (which `--lib --tests` skips) and runs the non-gated suite.
# Building examples is what surfaces one that uses a feature-gated API
# without a `required-features` entry (audit II.11 / P7) — the reason a
# plain `cargo check --lib --tests` never caught it. ~9,600 tests run here.
- run: cargo test --no-default-features
- run: cargo check --no-default-features --features hand-histories --lib --tests
- run: cargo check --no-default-features --features bot-profiles --lib --tests
- run: cargo check --no-default-features --features player-stats --lib --tests
- run: cargo check --no-default-features --features player-stats-persistence --lib --tests
# Equity in isolation: the `pkodds` service builds pkcore with just this
# feature. Guards against the equity engine bit-rotting or coupling to the
# default stack (it silently broke once because nothing on CI built it).
- run: cargo check --no-default-features --features equity --lib --tests
# plain `cargo check --lib --tests` never caught it. The suite needs
# `entropy` (0.15.0); `kernel_determinism` runs with nothing at all.
# Single source for both, and for the per-feature checks: the Makefile.
- run: make test-kernel
# Every feature alone — lib without `entropy`, lib and tests with it.
# Equity in isolation matters in particular: the `pkodds` service builds
# pkcore with just that feature.
- run: make check-features
# Kernel purity gate (AUDIT_Fable_5.md III.1 / III.6.1): with default
# features off, the storage (`store`) and terminal (`terminal`) layers must
# drop out entirely — no rusqlite/zstd/termion/dotenvy in the dependency
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ jobs:
# The pokerbench module is gated off by default, so the plain `test` job
# never compiles it. Exercise it explicitly here (tests, doc tests, lints).
# clippy comes from rust-toolchain.toml's components on the pinned toolchain.
- run: cargo test --features pokerbench
- run: cargo clippy --features pokerbench -- -D warnings
- run: cargo test --features full,pokerbench
- run: cargo clippy --features full,pokerbench -- -D warnings

optional-features:
name: Optional features (bot-training, debug-json)
Expand All @@ -40,8 +40,8 @@ jobs:
# so they could silently rot (audit VI.4 / P7). Exercise both explicitly.
# The heavy `training_integration` run stays #[ignore]d (release-only), so
# this only compiles the feature + runs the fast unit/doc tests.
- run: cargo test --features bot-training
- run: cargo test --features debug-json
- run: cargo test --features full,bot-training
- run: cargo test --features full,debug-json

marathon:
name: Bot Marathon (1 000 hands)
Expand All @@ -53,7 +53,7 @@ jobs:
- name: Run bot marathon
env:
MARATHON_DUMP_PATH: marathon_failure.yaml
run: cargo test --test bot_marathon -- --include-ignored --nocapture
run: cargo test --features full --test bot_marathon -- --include-ignored --nocapture
- name: Upload failure YAML
if: failure()
uses: actions/upload-artifact@v7
Expand All @@ -76,7 +76,7 @@ jobs:
# dead money) end to end. They are `#[ignore]`d for local speed, so run
# them explicitly here.
- name: Run variant replay round-trips
run: cargo test --test replay_consistency -- --include-ignored --nocapture
run: cargo test --features full --test replay_consistency -- --include-ignored --nocapture

okf:
name: OKF bundle conformance
Expand Down
23 changes: 16 additions & 7 deletions .okf/crate.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,27 @@ features.

# Feature flags

Default features enable the full player-stats stack plus bot profiles
and hand histories so examples run with a plain `cargo run --example`.
Downstream consumers can opt out with `default-features = false`.
Since 0.15.0 the crate is pure by default: the default set is `equity`,
`player-stats` and `hup-charts`, with no format crate, file helper or
thread pool. `full` turns on the 0.14.0 default set — use it for
`cargo test`, the examples, or to keep the old behaviour downstream.
`make check-purity` asserts the default tree stays pure.

| Feature | Purpose |
|---|---|
| `store` | On-disk storage layer (`rusqlite`, `zstd`). |
| `terminal` | Interactive terminal layer (`termion`). |
| `equity` | Pure-compute multi-way equity engine in `analysis::equity` (exact enumeration + seeded Monte Carlo, parallelized with rayon). |
| `full` | Umbrella: every feature below except `store`, `pokerbench`, `bot-training`, `debug-json`, `generators`. |
| `store` | On-disk storage layer (`rusqlite`, `zstd`, `csv`). |
| `terminal` | Console helpers for the REPL examples: `Terminal::receive_*` (stdin/stdout) and `Terminal::pause` (`termion`). |
| `entropy` | OS randomness and the wall clock, for the conveniences that pick an id or seed for you (`Player::new`, `*_from_seats`, `start_hand`, `decide`, `casino::dealer`). Default. Each has a seeded or id-taking twin; the `--no-default-features` build has no OS entropy. |
| `persistence` | Filesystem wrappers over the pure serializers (`SolverResult::save`/`load`, `BotProfile::to_file`/`from_file`, `HandCollection::save`, `Pluribus::read_in_log`). |
| `equity` | Pure-compute multi-way equity engine in `analysis::equity` (exact enumeration + seeded Monte Carlo). Default. |
| `parallel` | rayon-backed parallelism for the equity engine and `par_*` methods. |
| `hup-charts` | Embedded heads-up preflop chart and every function that reads it (`HUPResult::lookup`, `Versus::hups_at_deal`). Default. |
| `bot-profiles` | YAML serialization for `BotProfile` (`serde_yaml_bw`). |
| `hand-histories` | YAML serialization for `HandHistory`. |
| `player-stats` / `player-stats-persistence` | Per-player aggregator and its optional persistence. |
| `player-stats` / `player-stats-persistence` | Per-player aggregator (default) and its optional YAML persistence. |
| `json` | `SolverResult` JSON helpers (`serde_json`). |
| `csv` | CSV file helpers (`SortedHeadsUp`, `IndexCardMap`, `util::csv`). |
| `debug-json` | Human-readable JSON for `SolverResult::save`/`load`. |

# Lint posture
Expand Down
157 changes: 157 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,163 @@ All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.15.0] - 2026-09-15

Fixes 1, 1a–1c, 2 and 8 of
[docs/KERNEL_PURITY_AUDIT.md](docs/KERNEL_PURITY_AUDIT.md). pkcore is pure by
default: a plain `cargo add pkcore` compiles no format crate, file helper or
thread pool. The filesystem, console and embedded-chart code that was compiled
into every build is behind features, each with a pure twin. The kernel build
(`--no-default-features`) reads no OS randomness: everything that picked a
random id, shuffle or seed for you has a twin that takes it, core code uses only
the twins, and the random versions sit behind a new `entropy` feature that is
on by default.

### Added

- **`full` feature**, the umbrella: everything that was on by default in 0.14.0
(`bot-profiles`, `hand-histories`, `player-stats`,
`player-stats-persistence`, `equity`, `parallel`, `hup-charts`) plus `json`,
`csv`, `persistence`, `terminal` and `entropy`. **Add
`features = ["full"]` to keep the old behaviour.**
- **`entropy` feature**, in `default` and `full`. It turns on `rand`'s OS RNG,
`uuid`'s `v4`, and on wasm `getrandom`'s JS backend. Without it the crate
has no path to `getrandom` at all.
- **`persistence` feature** for the filesystem wrappers:
`SolverResult::save`/`load`/`save_binary`/`load_binary`/`save_json`/`load_json`,
`BotProfile::to_file`/`from_file`, `HandCollection::save`,
`Pluribus::read_in_log` and `Nubificus::get_log_files`. Their pure twins stay
always-on: `to_binary_bytes`/`from_binary_bytes`,
`to_json_string`/`from_json_str`, `to_yaml_string`/`from_yaml_str`,
`HandCollection::to_yaml`, and the new `Pluribus::parse_log`.
- **`json` feature** (`dep:serde_json`) for `SolverResult::to_json_string`,
`from_json_str`, `save_json` and `load_json`. `debug-json` and `pokerbench`
imply it. `serde_json` is no longer a required dependency.
- **`csv` feature** (`dep:csv`) for `SortedHeadsUp::generate_csv`/`read_csv`,
`IndexCardMap::generate_csv` and the `util::csv` module. `store` and
`pokerbench` imply it. `csv` is no longer a required dependency.
- **`Pluribus::parse_log(text)`**, the reading twin of `Pluribus::write_log`.
`read_in_log` now reads the file and calls it.
- **Id-taking twins for every table constructor:** `Table::from_seats_with_id`,
`nlh_from_seats_with_id`, `nlh_primed_with_id`,
`limit_holdem_from_seats_with_id`, `plo_from_seats_with_id`,
`stud_hi_from_seats_with_id`, `razz_from_seats_with_id`, and
`Player::with_id`. Every `Table` and `Player` used to get a random v4 id from
OS entropy, and the table wrote its id into the event log as `TableOpen`, so
the same seats, seed and actions gave a different `Table` and event log each
run. With fixed ids the same inputs now give an equal `Table`. The random-id
constructors call these with `Uuid::new_v4()`.
- **RNG-taking twins for the shuffling drivers:** `Table::act_shuffle_deck_with`,
`PokerSession::start_hand_with` and `PokerSession::run_hand_with`.
- `SimTable::DEFAULT_SEED` and `EquityOptions::DEFAULT_SEED` — what an unseeded
sim or equity request uses without `entropy`.
- `tests/kernel_determinism.rs`, the one test target that builds without
`entropy`, so the entropy-free kernel is exercised, not just compiled.

### Changed

- **Breaking: the default feature set is now `equity`, `player-stats`,
`hup-charts` and `entropy`.** A plain `cargo add pkcore` gets the table
engine, evaluation, equity, the stats aggregator and the heads-up chart, and
no longer compiles a YAML parser (`serde_yaml_bw`), a thread pool (`rayon`),
`serde_json` or `csv`. `bot-profiles`, `hand-histories`,
`player-stats-persistence` and `parallel` left the default set.
- **Breaking: `BotDecider::decide_seeded` is now the required method.**
`decide` and `on_new_hand` became provided methods (behind `entropy`) that
call `decide_seeded` / `on_new_hand_with_rng` with the thread-local RNG.
Implementors rename `decide` to `decide_seeded` and draw from the `rng`
argument; an `on_new_hand` override moves to `on_new_hand_with_rng`. No
sibling repo implements the trait.
- **Breaking: the console helpers need the `terminal` feature.**
`Terminal::receive_cards`, `receive_cards_in_twos`, `receive_range`,
`receive_usize`, `receive_usize_from` and `receive_x_cards` read stdin and
print to stdout, and were compiled into every build. `Terminal::index_cleaner`
(pure, used by the card parsers) and `random_happy`/`random_sad` stay
available. The audit proposed gating the whole `util::terminal` module; that
would have broken `Cards::from_str`, which uses `index_cleaner`.
- **Breaking: the embedded heads-up chart and everything that reads it need
`hup-charts`.** `analysis::store::embedded::hup_cache`, `HUPResult::lookup`,
`SortedHeadsUp::hup_result`, `Versus::hups_at_deal` and
`bot::hand_order::derive_hand_ordering` used to compile without the feature
and pulled the 15.8 MB chart into any binary that called them. `hup-charts`
is on by default, so default users see no change.
- **Breaking, for users of the `persistence` wrappers:** add
`features = ["persistence"]` (or `"full"`). Known sibling callers: `cardroom`
(`BotProfile::from_file`) and `pkcore.py` (`Pluribus::read_in_log`).
- **Breaking, for `default-features = false` users only:** these need the
`entropy` feature — `Player::new`, `Player::new_with_chips`,
`Table::from_seats` and the six `*_from_seats` / `nlh_primed` constructors,
`Cards::shuffle`/`shuffle_in_place`, `CardsCell::shuffle`/`shuffle_in_place`,
`Deck::poker_cards_shuffled`, `Table::act_shuffle_deck`,
`PokerSession::start_hand`/`run_hand`, `BotDecider::decide`/`on_new_hand`,
`JokerDecider::new` and its `Default`, `From<String> for Seat`,
`From<Vec<String>> for Seats`, and the whole `casino::dealer` module
(`PokerSession` is the canonical driver). Known lean callers that will need
`features = ["entropy"]` on upgrade: pkwasm, pkarena0-web, cardroom.
- **`DealEval::new` with two hands and no `hup-charts` uses the equity engine**
(seeded Monte Carlo) instead of the chart. It used to read the chart whatever
the features said.
- **`SimTable` always holds an RNG.** Unseeded, it is seeded once from the OS
(with `entropy`) or from `DEFAULT_SEED` (without). It used to fall back to
the thread-local RNG on every draw; results are equally random, and the seeded
path is unchanged.
- **Replays name their players by the record, not at random.**
`HandHistory::replay` gives each player their recorded `player_id`, or a UUID
v5 of the name for a legacy record, and names the table by a UUID v5 of the
hand id. `TryFrom<&Pluribus> for Table` does the same from the player names and
the log line, so one player keeps one id across a whole Pluribus log. The
`TestData` fixtures and the training evaluator use fixed ids. So the same
record always replays to an equal `Table`.
- Without `entropy`: a seedless `EquityRequest` uses `DEFAULT_SEED`, the
stats timestamp `SimTable` records is 0, and `Terminal::random_happy` /
`random_sad` return a fixed face (as on wasm).
- `Table::default()` names its table with the nil id in the event log too; it
used to log a random id and then set the field to nil.
- **`make check-purity` checks the default build as well as
`--no-default-features`.** Both fail on `clap`, `structopt`, `csv` and
`serde_json` (added to the hard list), and the `--no-default-features` build
also fails on `getrandom`; the default build only warns on it, since
`entropy` is on by default. "Pure by default" is now a gate, not a comment.
The gate no longer watches `rand` or `postcard`: a seeded PRNG is pure, and
`cardpack` needs `rand` non-optionally, so a ratchet on it could never shut.
- `make`, `make test`, `make clippy`, the docs build and the CI test, clippy,
doc, pokerbench, optional-feature, marathon and variant-replay jobs now pass
`--features full`, so they test what they tested before.
- `make test-kernel` runs the suite with `--features entropy` (it builds its
tables with the random-id conveniences) and then `kernel_determinism` with no
features. `make test-serial` adds `entropy`. `make check-features` checks
`json`, `csv`, `persistence`, `hup-charts` and `terminal` on their own, each
feature's lib without `entropy`, and its lib and tests with it. The CI
no-default-features job calls `make test-kernel` and `make check-features`
instead of inlining them.
- The examples and tests that call gated items now declare them in
`required-features`, including `bot_selfplay`, `interactive_play*`,
`bot_capability_bench`, `gto_solver`, `bcrepl`, `unum`, the `pluribus*`
examples, `replay_consistency` and `heavy_tests`. Three `BotProfile` tests
that check the `data/bots` YAML files now read the file themselves, so they
still run without `persistence`.
- The standalone `perf/` crate builds its self-play table with fixed ids.

### Fixed

- `make generate-hups-bin` passes `--features store`. It had been broken since
0.11.0 took `store` out of the defaults.

### Removed

- **`util::name::Name` and `NAMER`, and the `random_name_generator`
dependency.** Nothing in pkcore or its sibling repos called them.
`Name::generate_with` also put `rnglib::RNG` in a public signature. The crate
pulled a CLI parser (`clap`), a second `rand` (0.8) and `getrandom` 0.2 into
every build, including `--no-default-features`. The wasm-only `getrandom` 0.2
pin, which existed to serve that `rand` 0.8, is gone too.
- **`Util::read_lines` and `Util::commentary_action_to`.** The first put a path
in a public signature and was used once, by `read_in_log`; the second
`println!`ed and had no callers.

`rand` stays a required dependency on purpose: `cardpack` needs it anyway, and
a seeded PRNG is pure. Only the OS entropy behind it moved behind `entropy`.

## [0.14.0] - 2026-09-13

### Changed
Expand Down
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ still gets a changelog line if it changes what a user is told.

## Commands you would not guess

`cargo test` and `cargo build` work as normal. These do not:
Default features are the lean, pure kernel (0.15.0). Plain `cargo test` skips
the YAML, bot, rayon, JSON and CSV code; use `cargo test --features full` (what
`make test` runs) for the whole suite, and `--features full` on
`cargo run --example`. These do not work as you would guess either:

```bash
make ayce # the full local gate: fmt, clippy, test, docs, plus the
Expand Down
Loading
Loading