bosk 0.1.0

Pure-Rust LightGBM inference: parses the text model format directly — no FFI, zero deps. Optional ONNX Runtime and CatBoost backends behind the same small Model trait.
Documentation
# bosk

*A bosk is a small thicket of trees — here, an ensemble of decision trees.*

Gradient-boosting model **inference** behind one small trait — load a
trained LightGBM, ONNX, or CatBoost model and call `predict`.

The headline is the **LightGBM backend: it parses the `.lgb` text format
directly**, with no C dependency and no FFI to `lib_lightgbm`. Building with
`--no-default-features` gives you a LightGBM predictor with **zero dependencies**
— trivial to cross-compile, embed, or drop into a `musl` target.

```rust
use bosk::{load_model, Model};

let model = load_model(std::path::Path::new("model.lgb"))?;
let p = model.predict(&features)?;           // probability for a binary classifier
let ps = model.predict_batch(&flat, n)?;     // row-major batch, n features per row
```

## Backends

| Extension | Backend | Feature flag | Extra deps |
|-----------|---------|--------------|------------|
| `.lgb` / `.txt` | pure-Rust LightGBM text parser | always on | none (zero deps) |
| `.onnx` | ONNX Runtime via [`ort`] | `onnx` (default) | prebuilt ONNX Runtime |
| `.cbm` | CatBoost via `catboost-rust` | `catboost` (default) | native CatBoost |

`load_model` auto-detects the format by extension; `supported_extensions()`
reports what the current build can load. The default build enables `onnx` and
`catboost`. For the zero-C-dependency LightGBM-only build:

```toml
bosk = { version = "0.1", default-features = false }
```

A `.cbm` loaded through `load_model` is assumed to be a **binary classifier**
(the CatBoost C API does not expose the trained loss function). For a CatBoost
regression or ranking model, construct the backend explicitly:
`CatBoostModel::load(path, Output::Raw)`.

## The `Model` trait

```rust
pub trait Model: Send + Sync {
    fn predict(&self, features: &[f64]) -> bosk::Result<f64>;
    fn predict_batch(&self, flat: &[f64], n_features: usize) -> bosk::Result<Vec<f64>> { /* default */ }
}
```

`predict_batch` takes the samples row-major in one flat slice — no per-row
allocation — and backends override it with a single native batched run (ONNX,
CatBoost). `predict` checks the feature count against the model (LightGBM's
`max_feature_idx`, CatBoost's float-feature count, the ONNX graph's declared
input dimension when static) and refuses a wrong-length vector with
`Error::FeatureCount` instead of silently treating the tail as missing. On
the LightGBM path that refusal is the only possible error — evaluation
itself cannot fail once the model is loaded and validated. Where an
infallible call matters more than the length check, hold a concrete
`LgbModel` and use `predict_unchecked` (out-of-range feature indices are
then evaluated as missing; the objective transform is still applied — it is
not a raw-score prediction).

## LightGBM coverage

The pure-Rust parser reproduces LightGBM's own predictions **bit-for-bit**
(verified against LightGBM 4.6 across the full objective matrix):

- numerical splits with full missing-value semantics (default direction +
  missing type: none/zero/NaN, including the `kZeroThreshold` zero band);
- **categorical splits** (bitset lookup), including unseen, negative, and NaN
  categories;
- random-forest averaging (`average_output`);
- the output transform of every single-output objective: `binary` (honoring
  its `sigmoid` parameter), `cross_entropy`, `cross_entropy_lambda`,
  `poisson` / `gamma` / `tweedie` (exponential link), the regression family
  (including `reg_sqrt`), and ranking (raw scores).

Models it cannot evaluate faithfully are **rejected at load** with
`Error::Unsupported` rather than mispredicted silently: multiclass
(`num_class > 1`), linear trees (`is_linear=1`), unrecognised objectives, and
unrecognised objective tokens (a token like `sqrt` can change the output
transform, so an unknown one is never skipped). The token whitelist is
audited against every LightGBM release from 2.1 through 4.6 — the complete
vocabulary those versions can write is `sqrt`, `sigmoid:`, and `num_class:`
— so a token refusal can only occur for a model written by a future LightGBM
version.
Each tree's structure is validated at load — including that the child
pointers form an actual tree, so a corrupted file cannot send `predict` into
an infinite loop or a panic.

## Testing

```bash
cargo test --no-default-features   # pure-Rust LightGBM path + parity fixtures
cargo test                         # + onnx and catboost backend parity tests
```

Every backend is tested against its reference implementation: the LightGBM
parity fixtures assert the pure-Rust prediction matches LightGBM 4.6 to
`1e-9` (including missing-value, zero-as-missing, categorical, and
`reg_sqrt` models),
the CatBoost fixtures pin `Output::Probability` / `Output::Raw` against
CatBoost 1.2.10, and the ONNX fixture checks the exported graph end to end.
See [`tests/fixtures/README.md`](tests/fixtures/README.md) to regenerate
fixtures.

## License

Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or
[MIT license](LICENSE-MIT) at your option.

[`ort`]: https://crates.io/crates/ort