n4m 0.1.5

Official thin Rust binding for the libn4m C ABI
# n4m Rust binding

This is the official Rust binding for the stable `libn4m` C ABI. It is a thin
ownership/serialization layer: numerical fitting and optimizer logic stay in
`libn4m`. `Context`, `SearchSpace`, and `Optimizer` are `!Send + !Sync`; create
one `Context` per thread. `SearchSpace` maps all native typed axes and
constraints; `Optimizer` exposes native ask/ask-batch/tell/intermediate/best,
borrowed `Trial` accessors, and owning rich `TrialSnapshot` traces. Batch errors
retain every committed borrowed trial in `AskBatchError::Partial`.

`Pipeline::snv_savgol` and `Config::set_snv_savgol_pipeline` provide the safe,
owning Rust path for the bounded native SNV-to-Savitzky-Golay pipeline; the
opaque pipeline handle remains alive through every `Model::fit` call and is
released with its owner. `Config` + `Model::fit` call `n4m_model_fit` directly.
`Model::predict_into`
uses caller-owned row-major storage (`n4m_model_predict`); `Model::predict`
uses core-owned storage (`n4m_model_predict_alloc`) and copies it before
calling `n4m_array_free`. `Model::export_n4mm`/`import_n4mm` own N4MM bytes, and
`inspect_n4mm` accepts raw-model v1 and bounded SNV/SG pipeline v2 payloads;
`SerializedModelInfo::pipeline` is a typed optional descriptor containing the
validated operator order, versioned row-wise SNV ddof-0 and SG-interp semantic
profile, canonical SG parameters, raw/model widths and stable FNV-1a-64 plan
fingerprint. `has_pipeline()` reflects that authoritative
descriptor rather than inferring a plan from host metadata.
`Optimizer::save_n4mopt`/`load_n4mopt` own N4MOPT bytes. Checkpoint envelopes
are preflighted to the native 64 MiB N4MOPT cap before the binding allocates a
copy; native loading remains the authoritative decoder. Optimizer snapshots are
copied from the native result, preserve native parameter declaration order in
`parameter_order`, and remain usable after the optimizer is dropped.

`ValidationPlan` plus `finetune_estimator` expose the native regression
selection driver. It selects the best candidate and returns an owning trace;
it is deliberately selection-only and never performs a final full-data model
refit. Call `Model::fit` explicitly after selecting parameters. The native API
rejects unsupported estimators, pruners, metrics, conditional axes, and search
space schemas rather than broadening this binding's scope.

This crate is binding work only: crate version 0.1.4 tracks the additive ABI-2.5
inspection surface and is not an independent numerical-engine release. It
requires a prebuilt `libn4m`. The default
`linked` feature validates every Rust extern declaration against the installed
public headers at build time; it is the development and CI mode.

## Publication

The crates.io identity is [`n4m`](https://crates.io/crates/n4m), versioned
independently from the Methods engine. Maintainers publish only through
`.github/workflows/release-n4m-crate.yml` with an exact component tag matching
the manifest, for example `n4m-v0.1.4`. A manual workflow dispatch is always a
dry run: it builds `libn4m`, runs `cargo package --locked`, uploads the `.crate`
and file inventory as GitHub Actions artifacts, and records build provenance,
but it has no publication path.

The tag-triggered publish job uses the protected `crates-io` GitHub environment
and requires its `CARGO_REGISTRY_TOKEN` secret. A missing credential fails
explicitly. Do not publish this crate with a Methods-wide `v*` tag, and do not
reuse the archived `bindings/_archive/rust` proof of concept: it remains frozen
and is a different package history.

## License

The crate is dual-licensed as `CECILL-2.1 OR AGPL-3.0-or-later`, at your
option, in line with the
[repository licensing policy](https://github.com/GBeurier/nirs4all-methods/blob/main/LICENSING.md).
It packages the complete texts in
`LICENSES/CeCILL-2.1.txt` and `LICENSES/AGPL-3.0-or-later.txt`. This is
intentional: the repository-root `LICENSE` contains the AGPL text only and is
not presented as the CeCILL text. Verify the package file set with:

```sh
cargo package --locked -p n4m --list --allow-dirty
```

Build libn4m first, then run:

```sh
N4M_LIB_DIR="$PWD/build/dev-debug/cpp/src" \
N4M_RUNTIME_RPATH="$PWD/build/dev-debug/cpp/src" \
cargo test -p n4m
```

`N4M_LIB_DIR` is required and must contain the target shared-library artifact.
The build probe reads the public headers from `cpp/include` plus CMake's generated
`build/<preset>/generated`; installed layouts can set `N4M_INCLUDE_DIR` and
`N4M_GENERATED_INCLUDE_DIR` explicitly. The crate does not embed a default
absolute rpath. Set `N4M_RUNTIME_RPATH` only when the target platform needs an
explicit runtime-loader path (Linux/macOS); on Windows place `n4m.dll` beside the
executable or on `PATH`.

The CI sanitizer job uses the repository's `ci-{asan,ubsan,asan_ubsan}` native
presets. It builds the Rust test harness with `clang-16`, links the matching
clang sanitizer runtime, and verifies that runtime before tests run. Locally
those presets require `clang-16` and its sanitizer runtime; when that compiler
is unavailable, use the normal `dev-debug` command above rather than claiming a
sanitizer run.

## Packaged runtime loading

For a distributed host that already owns the exact native artifact, compile
without the default feature and enable `dynamic` instead. This mode does not
consult `N4M_LIB_DIR`, does not add an rpath, and never searches the current
directory. Before creating a `Context`, select the exact shared-library file:

```rust
n4m::configure_library("/absolute/path/to/libn4m.so.2")?;
let context = n4m::Context::new()?;
```

Alternatively set `N4M_LIBRARY_PATH` to that exact file before the first
`Context::new()`. The choice is process-wide and one-shot: reconfiguring to a
different library is rejected before any native handle can be mixed. A missing
or malformed runtime fails closed with an ABI error. This dynamic mode exposes
the same model and optimizer/HPO API; it is not a Python callback or a reduced
prediction-only binding.