|
| 1 | +# Context |
| 2 | + |
| 3 | +Orientation for anyone (human or agent) landing in the `binary-ensemble` workspace. It explains what |
| 4 | +the project is, how the code is shaped, and the invariants that aren't obvious from any single file. |
| 5 | + |
| 6 | +## What this project is |
| 7 | + |
| 8 | +`binary-ensemble` compresses **ensembles of districting plans**. A redistricting sampler (MCMC |
| 9 | +ReCom, SMC, etc.) emits thousands to millions of plans as canonicalized JSONL: one |
| 10 | +`{"assignment": [...], "sample": n}` line per draw. Those files are enormous and highly redundant. |
| 11 | +This workspace turns them into compact binary formats and provides the tooling to encode, decode, |
| 12 | +inspect, relabel, and bundle them. |
| 13 | + |
| 14 | +It is the spiritual successor to [PCompress](https://github.com/mggg/pcompress) and interoperates |
| 15 | +with it. |
| 16 | + |
| 17 | +The formats, in increasing capability: |
| 18 | + |
| 19 | +- **`.ben`**: a banner plus bit-packed, run-length-encoded frames. One frame per sample. |
| 20 | +- **`.xben`**: a BEN stream's payload wrapped in LZMA2 for maximum size reduction. |
| 21 | +- **`.bendl`**: a self-describing _bundle_ holding a header, optional assets (dual graph, metadata, |
| 22 | + node-permutation map), an embedded BEN/XBEN assignment stream, and a trailing directory. Feels |
| 23 | + like one file; supports interrupted writes and post-finalize appends. |
| 24 | + |
| 25 | +## Domain model (in brief) |
| 26 | + |
| 27 | +`docs/glossary.md` is the **source of truth** for terminology and is worth reading before making |
| 28 | +changes. The essentials: |
| 29 | + |
| 30 | +- **Plan**: a partition of dual-graph nodes into districts (the mathematical object). |
| 31 | + **Assignment**: its vector encoding, `Vec<u16>` where index _i_ is the district id of node _i_. |
| 32 | + One plan has many assignments. |
| 33 | +- **Sample**: `(sample_number, assignment)`. **Ensemble**: an ordered stream of samples from one |
| 34 | + sampler run; the thing every format wraps. |
| 35 | +- **Variant**: `Standard` | `MkvChain` | `TwoDelta`. Fixed per stream by its banner. `Standard` |
| 36 | + stores each sample independently; `MkvChain` collapses repeated consecutive samples with a count; |
| 37 | + `TwoDelta` delta-encodes single-ReCom-step transitions. Variant fitness depends on the sampler; |
| 38 | + see the glossary. |
| 39 | +- **Dual graph**: the geographic adjacency graph that gives a node ordering meaning. |
| 40 | + Relabeling/reordering operations are defined against it. |
| 41 | + |
| 42 | +The glossary also nails down deliberately-disambiguated words ("header", "extract", "payload", |
| 43 | +"canonical\*") and the relabeling taxonomy. Honor those distinctions in code and prose. |
| 44 | + |
| 45 | +## Architecture |
| 46 | + |
| 47 | +A Cargo workspace with two members: |
| 48 | + |
| 49 | +- **`ben/`**: package `binary-ensemble`, library `binary_ensemble`, plus two thin CLI binaries. |
| 50 | +- **`ben-py/`**: PyO3 bindings (cdylib) published as the `binary_ensemble` Python package. Depends |
| 51 | + on `ben/` by path; the core library has no Python dependency. |
| 52 | + |
| 53 | +### CLI binaries (`ben/src/bin/*.rs`) |
| 54 | + |
| 55 | +Each is a one-line wrapper over `cli::<tool>::run()`. `ben` is a subcommand tree; `bendl` owns the |
| 56 | +bundle container role: |
| 57 | + |
| 58 | +| Binary | Role | Does | | ------- | -------- | |
| 59 | +------------------------------------------------------------------------- | | `ben` | codec | |
| 60 | +encode/decode BEN/XBEN + xz; relabel/canonicalize/reencode; pcompress bridge | | `bendl` | bundle | |
| 61 | +create / inspect / extract / append `.bendl` containers | |
| 62 | + |
| 63 | +`ben` subcommands: `encode`, `xencode`, `decode`, `xdecode`, `lookup`, `xz-compress`, |
| 64 | +`xz-decompress`, `relabel`, `canonicalize`, `reencode`, `sort-graph`, and `pcompress` (`from-ben` / |
| 65 | +`to-ben` / `to-xben`). The relabel pipeline (decode → transform → re-encode) backs |
| 66 | +`relabel`/`canonicalize`/`reencode`; the PCompress bridge backs `pcompress`. |
| 67 | + |
| 68 | +### Library modules (`ben/src/`) |
| 69 | + |
| 70 | +- **`codec/`**: the heart. `encode`, `decode`, `frames` (`BenEncodeFrame` / `BenDecodeFrame`), and |
| 71 | + `translate` (BEN ↔ ben32 wire form). Frames keep their `raw_bytes` so they can be moved/subsampled |
| 72 | + without eager unpacking. |
| 73 | +- **`io/`**: streaming `reader` / `writer` over buffered, generic IO, and `bundle` (the `.bendl` |
| 74 | + reader/writer/verify/format machinery). |
| 75 | +- **`format/`**: on-disk metadata shared across streams (banners and `FormatError`). |
| 76 | +- **`ops/`**: the higher-level operations `relabel` (the single `relabel_ben_file` driver |
| 77 | + parameterised by `RelabelOptions`) and `extract`. |
| 78 | +- **`json/`**: dual-graph utilities (NetworkX-adjacency IO, MLC and RCM node-ordering algorithms) |
| 79 | + used by the relabel pipeline. |
| 80 | +- **`progress/`**, **`logging/`**, **`util/`**: spinners (`indicatif`), `tracing` setup, and small |
| 81 | + shared helpers (RLE). |
| 82 | + |
| 83 | +### Data flow |
| 84 | + |
| 85 | +```mermaid |
| 86 | +flowchart LR |
| 87 | + JSONL -->|encode| RLE |
| 88 | + RLE -->|bit-pack| frame |
| 89 | + frame -->|concat| ben["stream(.ben)"] |
| 90 | + ben -->|LZMA2 wrap| xben[".xben"] |
| 91 | + ben -->|bundle| bendl[".bendl"] |
| 92 | +``` |
| 93 | + |
| 94 | +Decode reverses this. The relabel subcommands run decode → transform → re-encode in one streaming |
| 95 | +pass. The encoding stack has five named layers (bit-packing, RLE, frame, stream, container); see the |
| 96 | +glossary's "Encoding Stack" table. |
| 97 | + |
| 98 | +## Invariants and cross-cutting concerns |
| 99 | + |
| 100 | +These hold across the codebase and are easy to violate by accident: |
| 101 | + |
| 102 | +- **Format stability is a contract.** Committed fixtures under `ben/tests/fixtures/v<n>/` must keep |
| 103 | + decoding forever within a major version. Never regenerate fixtures in place. See |
| 104 | + `docs/format-stability.md`. |
| 105 | +- **Frames decode lazily.** Keeping `raw_bytes` without unpacking runs is what makes |
| 106 | + subsample-by-skip and random-access reads fast. Don't force eager bit-unpacking on read. |
| 107 | +- **Integrity is checked with CRC32C.** Verifying read paths are the default; checksum-skipping |
| 108 | + variants are explicitly named with an `_unverified` suffix. |
| 109 | +- **Terminology is disciplined.** The glossary governs identifiers and prose; when they disagree, |
| 110 | + the glossary wins and the code changes. |
| 111 | +- **Streaming, not slurping.** Ensembles are too large to hold in memory. |
| 112 | +- **64-bit only** (enforced with `compile_error!` in `lib.rs`). |
| 113 | +- **Illegal states are unrepresentable where practical**: e.g. `XBenVariant` cannot hold `TwoDelta`, |
| 114 | + so BEN32-only paths reject it at compile time. |
| 115 | + |
| 116 | +## Building and testing |
| 117 | + |
| 118 | +The workspace uses a `Taskfile.yml` (the `task` / `go-task` runner) as the single entry point for |
| 119 | +local workflows. CI runs the lightweight gates (formatting + lints) on every PR; the heavy gates |
| 120 | +(full test suites, big-endian emulation) run on demand via the Actions tab or a `/ci-full` / |
| 121 | +`/ci-endian` PR comment from a collaborator. The wheel-publishing workflow is separate and |
| 122 | +tag-triggered. |
| 123 | + |
| 124 | +- `task test`: Rust fast suite + `#[ignore]`-gated slow/stress suite + Python `pytest`. |
| 125 | +- `task format`: `cargo fmt --all` + `ruff format`. |
| 126 | +- `task lint`: `cargo clippy --workspace --all-targets` (warnings denied) + `ruff check`. |
| 127 | +- `task coverage-summary`: combined Rust + Python coverage. |
| 128 | +- `task test-endian`: full ben suite on one big-endian and one little-endian target via `cross` |
| 129 | + (Docker + QEMU), proving wire-format endianness regardless of the development machine. |
| 130 | + `task check-endian` is the no-Docker compile-only tier. |
| 131 | +- `task fuzz`: time-boxed coverage-guided fuzzing (cargo-fuzz/libFuzzer, nightly) of every read |
| 132 | + surface, seeded from the committed fixtures. `FUZZ_SECONDS` bounds each target (default 60). |
| 133 | + |
| 134 | +Python development uses `uv` + `maturin` (`task ben-py-develop`). |
| 135 | + |
| 136 | +## Document map |
| 137 | + |
| 138 | +- **`docs/glossary.md`**: terminology, the source of truth. |
| 139 | +- **`docs/coding-standards.md`**: how code in this repo is written (errors, logging, naming, |
| 140 | + testing, modules, PyO3). |
| 141 | +- **`docs/bendl-format-spec.md`**: the `.bendl` on-disk binary layout. |
| 142 | +- **`docs/format-stability.md`**: the wire-format stability policy. |
| 143 | +- **`README.md`**: user-facing CLI and library usage. |
| 144 | +- **`docs/*-plan.md`**: active design plans, written before implementation. |
0 commit comments