rill-ml 1.3.0

RillML adaptive intelligence core library — lightweight, serializable online machine learning for native and edge applications.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# RillML 1.0 Stability Policy

> **Naming note:** RillML is the official project name. Rill may be used as a
> contextual shorthand. The frozen crate/package/ABI names below (e.g.
> `rill-ml`, `rill-runtime`, `rill-runtime-protocol`, `rill-handler-api`) are
> technical identifiers and are intentionally kept explicit; they are never
> replaced with the shorthand. See [`docs/NAMING.md`]docs/NAMING.md.

This document defines the compatibility surface that RillML 1.x commits to
preserve. It is the single source of truth for what is stable, what is
preview, and how each artifact may evolve.

## Stability matrix

| Artifact | 1.0 status | Stable commitment |
|---|---|---|
| `rill-ml` | Stable | Rust public API; selected serde model state types listed below |
| `rill-runtime-protocol` | Stable | IPC v1/v2 JSON wire schema, release-index schema |
| `rill-handler-api` | Stable | WIT ABI v1, handler ABI constants |
| `rill-runtime` | Stable | Rust public API, CLI, model/handler pack loading |
| `rill-ml-python` | Preview | Python bindings; not covered by the 1.x API freeze |
| `rill-ml-wasm` | Preview | JS/WASM bindings; not covered by the 1.x API freeze |
| `rill-ml-tokio` | Preview | Rust adapter; not covered by the 1.x API freeze |
| `rill-ml-arrow` | Preview | Rust adapter; not covered by the 1.x API freeze |
| `rill-ml-polars` | Preview | Rust adapter; not covered by the 1.x API freeze |
| `rillml-inspect` | Preview (Tooling) | CLI output and arguments; not covered by the 1.x API freeze |

### Rationale for the split

The four Stable crates form the contracts that hosts, handler authors, and
downstream Rust applications compile against. Freezing them lets the runtime,
protocol, handler ABI, and core learning library evolve independently within
1.x.

The Preview crates are adapters and bindings whose surfaces are small but
still iterating. Keeping them at `0.x` avoids forcing a Python wheel matrix,
WASM JS fixture freeze, and adapter API baseline into the 1.0 RC admission
gate. They still ship from this repository and keep their existing security
and test quality, but they do not carry the 1.x no-breaking-change promise.

Preview crates depend on Stable crates via path dependencies. A `0.x` crate
depending on a `1.x` crate is valid in Cargo.

## Rust public API (Stable crates)

The public Rust API of each Stable crate is frozen by an API baseline stored
under `api-baseline/` and enforced by `cargo-semver-checks` in CI.
For the `1.3.0` admission, `1.2.0` is the authoritative recent Stable
baseline; `1.1.0` and `1.0.0` remain historical sanity baselines only.

### `#[non_exhaustive]` policy

Enums and config structs are classified into two groups.

**Non-versioned, extensible types** carry `#[non_exhaustive]` so future
variants or fields can be added without a breaking change:

- `RillError`
- `Optimizer`
- `RegressionLoss`
- `HandlerLoadError`
- `ModelPackError`
- `HandlerPackError`
- `ArchiveError`
- `ReleaseIndexError`
- `InvokeErrorKind`
- `EngineResponse`
- `DriftAction`
- `DriftLevel`
- `WarmupState`
- `Confidence`
- `NewFeaturePolicy`

Application-facing config structs that may gain new optional fields also
carry `#[non_exhaustive]` and provide a `Default` or constructor.

**Versioned wire-schema types** (e.g. `RuntimeRequest`, `RuntimeResponse`,
`RuntimeResponseV2`) are NOT marked `#[non_exhaustive]`. They evolve by
introducing a new versioned type (e.g. `RuntimeResponseV3`), never by
appending variants or fields to an existing frozen type.

### Config strategy

Config structs that are expected to grow carry `#[non_exhaustive]` and a
`Default` implementation. Examples and docs use `Default::default()` or the
provided constructors rather than full struct literals where the field set
may expand.

### Public panic policy

Stable public APIs do not panic on ordinary user input. `Mutex::lock()`
calls in public paths return `Result` rather than `.expect()`. Internal
invariant panics are not reachable from external input. Rustdoc `# Panics`
sections document any remaining condition.

## serde model state

### Version model

`Snapshot<T>` carries an outer `format_version` (currently `1`). Each
Stable state schema type listed in the whitelist below implements
`ValidateState` (or an equivalent custom `Deserialize` / derive-based
validator) to enforce type-specific invariants (dimensions, finite values,
non-negative counts, optimizer parameter counts, FTRL `max_features`,
encoder mapping consistency, pipeline dimensions, bandit arm state).

Preview state schema types are serializable but their state schema is not
guaranteed to be compatible across 1.x versions.

### Restore contract

- `Snapshot::into_model()` checks the outer `format_version` only. It is
  safe for trusted state produced by the same code.
- `Snapshot::into_model_with_validation(validate)` checks the version and
  runs the caller-supplied validator before returning the model. This is the
  required path at trust boundaries.
- Python and WASM `from_json` go through validated restore and enforce a
  byte-size limit (`MAX_SNAPSHOT_JSON_BYTES`) before deserialization.

### Cross-version fixtures

Golden state fixtures live under `tests/fixtures/state/`:

- `v0.13.0/` — representative state produced by the immutable `v0.13.0` tag.
- `v1/` — state produced by the 1.0 RC candidate code.

RC and all future 1.x CI must load every fixture for Stable state schema
types. A schema bump requires a migration note in `CHANGELOG.md` and a new
fixture set.

The same golden fixtures are the cross-architecture stability gate: they are
loaded and byte-compared on every supported CPU architecture (native and
Docker + QEMU), so a state written on one architecture restores identically
on another. New stable state DTOs must use fixed-width integers
(`u32`/`u64`/`i32`/`i64`) only. See `PLATFORM_SUPPORT.md` →
*Cross-Architecture State Compatibility*.

### Stable state schema types

The following 26 types are covered by the 1.x state-freeze contract. Each
has full cross-version fixture coverage (`v0.13.0/` + `v1/`) and implements
state validation via one of three modes:

- `validate_state` — implements `ValidateState` trait.
- `deserialize` — custom `Deserialize` impl enforces invariants during
  deserialization.
- `derive` — serde-derived `Deserialize` for simple enums that automatically
  rejects unknown variants.

| Type | Fixture | Validation |
|---|---|---|
| `Mean` | `mean` | `validate_state` |
| `Variance` | `variance` | `validate_state` |
| `ExponentiallyWeightedMean` | `ew_mean` | `validate_state` |
| `StandardScaler` | `standard_scaler` | `validate_state` |
| `OneHotEncoder` | `one_hot_encoder` | `validate_state` |
| `OrdinalEncoder` | `ordinal_encoder` | `validate_state` |
| `FrequencyEncoder` | `frequency_encoder` | `validate_state` |
| `ConstantImputer` | `constant_imputer` | `validate_state` |
| `ForwardFill` | `forward_fill` | `validate_state` |
| `MeanImputer` | `mean_imputer` | `validate_state` |
| `MissingIndicator` | `missing_indicator` | `validate_state` |
| `FeatureHasher` | `feature_hasher` | `deserialize` |
| `SparseFeatures` | `sparse_features` | `deserialize` |
| `RegressionLoss` | `regression_loss` | `derive` |
| `LinearRegression` | `linear_regression` | `validate_state` |
| `MeanRegressor` | `mean_regressor` | `validate_state` |
| `FtrlClassifier` | `ftrl_classifier` | `validate_state` |
| `FtrlRegressor` | `ftrl_regressor` | `validate_state` |
| `GaussianNaiveBayes` | `gaussian_naive_bayes` | `validate_state` |
| `BernoulliNaiveBayes` | `bernoulli_naive_bayes` | `validate_state` |
| `MultinomialNaiveBayes` | `multinomial_naive_bayes` | `validate_state` |
| `Sgd` | `optimizer_sgd` | `validate_state` |
| `EpsilonGreedy` | `epsilon_greedy` | `validate_state` |
| `Ucb1` | `ucb1` | `validate_state` |
| `ThompsonSampling` | `thompson_sampling` | `validate_state` |
| `LinUcb` | `linucb` | `validate_state` |

### Preview state schema types

The following types are serializable but their state schema is not covered
by cross-version fixture coverage. Their state schema may change within
1.x without requiring a major version bump:

- `PageHinkley` (drift detector)
- `Adwin` (drift detector)
- `Kswin` (drift detector)
- `DriftAwareModel` (composite drift-aware model)
- `DriftEvent`
- `StaticStrategy`
- `TimeDecayedMean`
- `LearningRateScheduler`
- `FixedWindowBuffer`
- `LogisticRegression`
- `ExponentiallyWeightedMeanRegressor`
- `LastValueRegressor`
- `DriftConsensus`
- `P2Quantile`
- `P2Quantiles`
- `ClippedMean`
- `RollingMedianMad`
- `LinUcbFast`
- `DecisionLedger`
- `FeatureSchema`
- `ModelDescriptor`
- `WeightedMean`
- `WeightedVariance`
- `WeightedExponentiallyWeightedMean`
- `WeightedMae`
- `WeightedMse`
- `DecisionReplayHarness`

### State schema manifest

The authoritative source of truth for the Stable/Preview state schema
classification is `state-schema-manifest.toml`. The
`scripts/check_state_fixture_coverage.py` script validates in CI that:

- Every Stable type has a `v0.13.0` fixture.
- Every Stable type has a `v1` fixture.
- Every Stable type implements the declared validation mode.
- Fixture files exist on disk.
- No duplicate entries in either group.
- No overlap between Stable and Preview groups.

### Stable versioned portable detector state

The detector structs `PageHinkley`, `Adwin`, and `Kswin` retain their Preview
internal serde layouts. Stable continuity is instead provided by three
explicit versioned DTOs: `PageHinkleyPortableStateV1`,
`AdwinPortableStateV1`, and `KswinPortableStateV1`. Their `version = 1`
schemas, exact configuration matching, bounded windows, finite-value checks,
counter invariants, and golden fixtures under
`tests/fixtures/state/portable-v1/` are compatibility commitments. A future
breaking portable schema must introduce a new `V2` DTO; it must not mutate a
`V1` field or meaning.

`DriftConsensus`, `P2Quantile`, `P2Quantiles`, `ClippedMean`,
`RollingMedianMad`, `LinUcbFast`, `DecisionLedger`, the descriptor DTOs,
weighted statistics, and `DecisionReplayHarness` are additive Rust APIs whose
serialized implementation states remain Preview. `RollingMedianMad` is exact
only within its bounded FIFO window; it is not a lifetime-distribution state
contract. `LinUcbFast` does not change or replace the frozen `LinUcb` state
fields. Feature-schema hashes and their golden fixture are deterministic
identity checks, not a promise that every descriptor DTO field is frozen for
all 1.x releases.

IPC V3 and Stateful Handler ABI v2 are separate, opt-in Preview protocol
surfaces. They do not alter the frozen IPC v1/v2 JSON schemas, the top-level
`RUNTIME_API_VERSION = 2`, or the WIT v1 package/world/hash. Stateful v2 state
snapshots are runtime-owned and checksum-validated, but their ABI and snapshot
format are not yet Stable.

## IPC v1/v2

The v1 (`RuntimeResponse`) and v2 (`RuntimeResponseV2`) wire schemas are
permanently frozen for 1.x. New fields or semantics use a new versioned type
(e.g. `RuntimeResponseV3`), never an additive change to an existing type.

### Stable error codes

Error responses use the stable `error_code` constants defined in
`rill-runtime-protocol::error_code`. Existing codes are frozen:

- `invalidJson` — request body was not valid protocol JSON
- `invalidRequestId``requestId` was missing, empty, or too long
- `incompatibleApiVersion``apiVersion` was outside the supported range
- `invalidClientIdentity``clientName` / `clientVersion` failed validation
- `unsupportedCapability``Invoke` capability is not in the effective set
- `noInvokeHandler``Invoke` was issued but no handler is registered
- `handlerTimeout` — handler exceeded the wall-clock deadline (retryable)
- `handlerTrap` — handler trapped (unreachable, OOB, stack overflow, …)
- `handlerOutputTooLarge` — handler output exceeded the host-side size limit
- `handlerInvalidOutput` — handler output was not valid JSON
- `handlerInternalError` — handler reported an internal error (covers all
  four WIT `handler-error` variants on the wire for backwards compatibility)

New codes may be added (additive) but existing codes are never renamed.

### Protocol evolution

- v1/v2 types and wire schemas are permanently frozen.
- New required fields use a new versioned type.
- The runtime supports v1 and v2 throughout 1.x.
- Removing an old protocol version requires 2.0 or the end of the stated
  support period.

## WIT ABI v1

The WIT package `rill:handler@1.0.0` is frozen. `HANDLER_API_VERSION = 1`
and `WIT_VERSION = "1.0.0"` are enforced by `scripts/check_wit_abi.py`,
which verifies four independent declarations of the ABI version agree:

1. `crates/rill-handler-api/wit/rill-handler.wit` — the canonical WIT source.
2. `crates/rill-handler-api/src/lib.rs` — Rust constants
   (`HANDLER_API_VERSION`, `WIT_PACKAGE`, `WIT_VERSION`, `WIT_WORLD`).
3. `crates/rill-runtime-protocol/src/lib.rs` — the protocol constant
   `HANDLER_API_VERSION` used in handler-pack manifest validation.
4. SHA-256 of the normalised WIT text (frozen:
   `108a68dfd6bcf86e3b63ad630508b2bbf407d00e8634067366e53dbc257cc90c`).

A prebuilt v1 component fixture
(`crates/rill-runtime/tests/fixtures/handler-v1-component.wasm`) is committed
to the repository and loaded by `tests/wit_v1_component.rs` to prove the
current runtime still loads a component built from the `v0.13.0` tag using
the frozen WIT. CI does **not** rebuild this fixture from current WIT. The
fixture's SHA-256
(`6cfb4bf2eac5d0d5a4644c56f58b2fd679fc989893bc381a8ae31b410852011b`) is
verified before each load so silent replacement is detected.

### WIT evolution

- v1 types and the wire schema are permanently frozen. The frozen set
  includes: `handler-metadata`, `handler-error`, the `invoke-handler`
  world, and the three exported functions (`metadata`, `configure`,
  `invoke`).
- Additive changes within v1 are limited to new resources or new
  non-required interface functions; they must not break existing guests
  and must not change the normalised WIT hash without an explicit review
  that updates `FROZEN_WIT_SHA256` in `scripts/check_wit_abi.py`.
- Breaking changes (renaming or removing a type, changing a function
  signature, altering a variant's payload) require a new
  `rill:handler@2.x` package and a `HANDLER_API_VERSION` bump.
- The 1.x runtime continues to load v1 handlers for the entire 1.x cycle.
- `HANDLER_API_VERSION` and the WIT package major version are coupled:
  `rill:handler@1.x``HANDLER_API_VERSION = 1`,
  `rill:handler@2.x``HANDLER_API_VERSION = 2`, and so on.

## Model and handler packages

`.rillpack` (model pack) format version 1 and `.rillhandler` (handler pack)
format version 1 are frozen. `MODEL_PACK_FORMAT_VERSION = 1` and
`HANDLER_PACKAGE_FORMAT_VERSION = 1`. A format bump requires a migration
note and a new versioned manifest type.

## Runtime CLI

The `rill-runtime` CLI subcommands and arguments are frozen for 1.x:

- `serve``--pack`, `--model-trust-key` (primary), `--trust-key`
  (deprecated alias), `--handler-trust-key`, `--handler`,
  `--builtin-handler`.
- `inspect-pack``--pack`, `--model-trust-key`.
- `inspect-handler``--handler`, `--handler-trust-key`.

The `--builtin-handler linear-regression` path is retained as an explicit
compatibility option. The implicit fallback to the built-in handler when no
`--handler`/`--builtin-handler` is passed has been removed: the runtime
fails to start instead.

`rillml-inspect` CLI is Preview (Tooling) and may change.

## Default features

`rill-runtime` ships with `default = ["wasm"]` so that
`cargo install rill-runtime` matches the official GitHub binary behavior.
Builds that omit the feature must document that `.rillhandler` packs cannot
be loaded.

## MSRV

The Minimum Supported Rust Version for Stable crates is **1.94.0**,
enforced in CI. An MSRV bump is a minor breaking change and requires a
`CHANGELOG.md` note and a CI matrix update.

## Platform support

| Platform | Runtime | Core library |
|---|---|---|
| Linux x86_64 | Stable | Stable |
| Windows x86_64 | Stable | Stable |
| Windows ARM64 (aarch64) | Stable | Stable |
| macOS aarch64 (Apple Silicon) | Stable (unsigned) | Stable |
| macOS x86_64 (Intel) | Not published | Stable |

### macOS unsigned policy (permanent)

macOS official runtime assets are Apple Silicon only. The project does not
have an Apple Developer ID Application certificate and does not require one
for any release — RC, candidate, or final stable. This is a permanent project
decision that applies to the entire 1.x cycle and beyond.

When Apple Developer ID secrets are not configured in the release workflow
(the permanent default), the macOS aarch64 runtime is:

- **Always compiled.** The build step runs unconditionally; it is never
  skipped due to missing signing secrets.
- **Uploaded as unsigned.** The binary is not codesigned or notarized.
  A sidecar metadata file (`*.unsigned.json`) is uploaded alongside the
  binary so the release index and release notes can accurately reflect the
  unsigned status without modifying the frozen release-index schema.
- **Included in the channel index.** The signed index contains the macOS
  runtime artifact, its size, and SHA-256. The separate
  `*.unsigned.json` release asset records `codeSigning: "unsigned"` and
  `notarization: false`; these fields are intentionally not added to the
  frozen release-index schema.
- **Not a release blocker.** The absence of Apple Developer ID is never a
  blocking condition for any release.

macOS users may need to authorize the unsigned binary through standard
macOS controls:

1. **Finder method:** Right-click the binary in Finder and select "Open".
   A confirmation dialog appears; click "Open" again to confirm.
2. **System Settings method:** After the first launch attempt fails, open
   "System Settings → Privacy & Security" and click "Allow Anyway" next to
   the blocked binary notice.
3. **Quarantine removal (optional):** Advanced users can remove the
   quarantine extended attribute manually:
   ```bash
   xattr -d com.apple.quarantine /path/to/rill-runtime
   ```

Optional codesign remains supported: if Apple Developer ID secrets are
configured in the repository, the release workflow performs codesign.
Notarization is not currently automated. The presence or absence of
signing secrets does not change the build, upload, or index inclusion
behaviour.

## Deprecation policy

- A deprecated Stable API remains available for at least one minor 1.x
  release before removal.
- Removals happen only in a new minor release, never a patch.
- Deprecations are recorded in `CHANGELOG.md`.

## 1.x breaking-change policy

Breaking changes to Stable artifacts require a new major version (2.0):
- Rust public API
- serde model state schema (a new `format_version` is not breaking if a
  migration path exists, but removing support for an old version is)
- IPC v1/v2 wire schema
- WIT ABI v1
- model/handler pack format
- CLI subcommands and argument names

Additive changes (new Stable APIs, new error codes, new config fields via
`#[non_exhaustive]`) are allowed within 1.x.

## Release channels

| Channel | Index file | Pointer tag | Purpose |
|---|---|---|---|
| stable | `stable-index.json` | `local-ai-stable` | Final 1.0.0 and later |
| candidate | `candidate-index.json` | `local-ai-candidate` | 1.x prereleases (e.g. `1.2.0-rc.N`) |

A candidate release never updates the stable pointer. A final release advances
the stable pointer only after its immutable public assets pass the independent
released-asset host smoke.

The release-index is a versioned Stable protocol: a published schema version
remains immutable, so a later 1.x minor can evolve it by introducing a new
schema version, and older readers fail closed on a schema version they do not
recognize.

## Prerelease versioning

Prerelease versions follow SemVer `1.2.0-rc.N`. The version tooling
(`scripts/sync_version.py`, `scripts/release_version.py`,
`scripts/release_version_compare.py`) accepts prerelease identifiers
via strict SemVer 2.0 regex patterns. `build-release-index.py` routes
the signed index to the candidate channel via `--channel candidate`.
The release workflow (`pipeline.yml`) detects prerelease versions
(contains a `-` after the patch number), passes `--prerelease` to
`gh release create`, uploads `candidate-index.json` to the
`local-ai-candidate` pointer release, and never touches
`local-ai-stable`. The Python crate is in the Preview version group and is
published to PyPI only when its independent Preview version changes. The
WASM bindings (`rill-ml-wasm`) are likewise in the Preview version group and
are published to npm only when the WASM version matches the release tag;
otherwise the `publish-wasm` job in `pipeline.yml` skips the upload.