# Release Notes

Release notes for the `ommx` crate, covering the 3.0 line.

## 3.0

The 3.0.0 line is a major revision of the Rust SDK:

- **Indicator / one-hot / SOS1** are first-class constraint types alongside
  regular `Constraint`, with their own ID types and collection slots on
  `Instance`.
- The resulting constraint-kind × lifecycle combinatorics is tamed by a
  shared [`ConstraintType`](crate::ConstraintType) abstraction and a
  [`Stage`](crate::Stage) type parameter, so each kind is one generic
  struct rather than four hand-written ones.
- Constraints are grouped into generic
  [`ConstraintCollection<T>`](crate::ConstraintCollection) /
  [`EvaluatedCollection<T>`](crate::EvaluatedCollection) /
  [`SampledCollection<T>`](crate::SampledCollection) wrappers on
  `Instance`, `ParametricInstance`, `Solution`, and `SampleSet`;
  because the `id` field is gone from individual constraints, the
  **host is the natural unit of serialization**
  (`Instance::to_bytes`, `ParametricInstance::to_bytes`,
  `Solution::to_bytes`, `SampleSet::to_bytes`).
- Metadata (`name`, `subscripts`, `parameters`, `description`, plus
  `provenance` on constraints) moves off each constraint, decision
  variable, and named function into per-collection **Struct-of-Arrays
  metadata stores**, queried through narrow per-host accessors
  (`instance.constraint_metadata()`, `instance.variable_metadata()`,
  `instance.named_function_metadata()`, …). One canonical store per
  collection, two views on top: per-id wrapper getters for one-off
  reads and `*_df` for bulk analysis.
- A **capability model** lets adapters declare what they natively
  support and auto-converts unsupported kinds at the boundary, so a
  valid OMMX instance can be fed to any adapter (the conversion path
  is fallible and surfaces as `Err(ommx::Error)`, e.g. on non-finite
  bounds in the Big-M encoding).
- The default **error surface** is a single
  [`ommx::Result<T>`](crate::Result) (re-export of `anyhow::Result`)
  with diagnostic context emitted through `tracing` from the new
  fail-site macros. A handful of typed signal types
  (`BoundError`, `DecisionVariableError`, `DuplicatedSampleIDError`,
  `SubstitutionError`, …) stay typed at their public-API entry points
  for callers that recover by discriminant.
- The long-running migration away from the proto-generated `v1_ext`
  helpers finishes: domain types (`Instance`, `Constraint`,
  `DecisionVariable`, …) are the primary API, and `v1::*` is reserved for
  wire-format interop.

See the [migration guide](crate::doc::migration_guide) for the detailed
v2 → v3 upgrade path. This page is a topic-oriented summary of what changed and
why.

## First-class special constraint types ([#790](https://github.com/Jij-Inc/ommx/pull/790), [#798](https://github.com/Jij-Inc/ommx/pull/798))

Special-structure constraints are now first-class domain objects, parallel to
the regular [`Constraint`](crate::Constraint) rather than metadata hanging off
it:

- [`IndicatorConstraint`](crate::IndicatorConstraint) — encoding
  `indicator_variable = 1 → f(x) {=,≤} 0`. **New in v3.**
- [`OneHotConstraint`](crate::OneHotConstraint) — exactly one of a set of
  binary variables is 1. Previously expressed as a
  `ConstraintHints::OneHot` hint on a regular equality constraint; now a
  constraint type in its own right.
- [`Sos1Constraint`](crate::Sos1Constraint) — at most one of a set of
  variables is non-zero. Previously `ConstraintHints::Sos1`; now first-class.

Each kind has its own ID type
([`IndicatorConstraintID`](crate::IndicatorConstraintID),
[`OneHotConstraintID`](crate::OneHotConstraintID),
[`Sos1ConstraintID`](crate::Sos1ConstraintID)) to prevent accidental
cross-type lookups, and lives in its own collection slot on
[`Instance`](crate::Instance).

## Stage parameter and the `ConstraintType` trait ([#789](https://github.com/Jij-Inc/ommx/pull/789), [#795](https://github.com/Jij-Inc/ommx/pull/795), [#796](https://github.com/Jij-Inc/ommx/pull/796), [#806](https://github.com/Jij-Inc/ommx/pull/806))

Promoting indicator / one-hot / SOS1 to first-class types alongside
regular `Constraint` would have multiplied the number of concrete
constraint structs by the number of lifecycle states each kind can be
in (created / evaluated / sampled, with removal handled separately).
Hand-writing the resulting 4 × 3 = 12 concrete struct definitions
wasn't going to scale.

The core refactor of 3.0 collapses the matrix: every constraint kind
is one generic struct parameterized by a [`Stage`](crate::Stage)
marker, and the [`ConstraintType`](crate::ConstraintType) trait names
the concrete `Created` / `Evaluated` / `Sampled` types per kind so
generic code (iteration, feasibility checks, DataFrame rendering,
adapter conversion) is written once and applied uniformly. The
[`EvaluatedConstraintBehavior`](crate::EvaluatedConstraintBehavior) /
[`SampledConstraintBehavior`](crate::SampledConstraintBehavior) traits
expose the per-kind feasibility surface in the same style.

Two knock-on simplifications:

- **No `Removed` stage.** Removal is collection-level state — see the
  next section.
- **No `id` field on the struct.** A constraint's ID lives on the
  enclosing `BTreeMap` key, which was already the single source of
  truth.

The migration guide's [Constraint Field Access](crate::doc::migration_guide#1-constraint-field-access)
and [New Types](crate::doc::migration_guide#new-types) sections cover
the struct shapes and the trait family in full.

## Collections and serialization ([#789](https://github.com/Jij-Inc/ommx/pull/789), [#795](https://github.com/Jij-Inc/ommx/pull/795), [#806](https://github.com/Jij-Inc/ommx/pull/806))

A trio of generic collection wrappers holds constraints uniformly
across every kind and every stage:
[`ConstraintCollection<T>`](crate::ConstraintCollection) on
`Instance` / `ParametricInstance` (active + removed paired with a
[`RemovedReason`](crate::RemovedReason)),
[`EvaluatedCollection<T>`](crate::EvaluatedCollection) on
[`Solution`](crate::Solution) (evaluated + removed-reason map), and
[`SampledCollection<T>`](crate::SampledCollection) on
[`SampleSet`](crate::SampleSet) (per-sample variants). Iteration,
feasibility checks, and `RemovedReason` handling work the same way
across every constraint kind at every stage, so adapter / Solution /
SampleSet code doesn't need to special-case the four kinds.

**Serialization moves to the host level.** With the `id` field gone
from individual constraints and metadata living in a per-collection
SoA store (next section), a single element can no longer round-trip
on its own. Per-element `to_bytes` / `from_bytes` are not provided on
any constraint kind or its evaluated / sampled counterpart; use
`Instance::to_bytes` / `from_bytes`,
`ParametricInstance::to_bytes` / `from_bytes`,
`Solution::to_bytes` / `from_bytes`, or
`SampleSet::to_bytes` / `from_bytes` as the entry points — each
encodes every constraint kind together with IDs and metadata in one
`v1::*` protobuf message.

The migration guide's [ConstraintCollection](crate::doc::migration_guide#constraintcollection)
and [EvaluatedCollection / SampledCollection](crate::doc::migration_guide#evaluatedcollection--sampledcollection)
reference cards list the public methods on each.

## Metadata storage: SoA store on the enclosing collection ([#843](https://github.com/Jij-Inc/ommx/pull/843), [#848](https://github.com/Jij-Inc/ommx/pull/848), [#850](https://github.com/Jij-Inc/ommx/pull/850), [#853](https://github.com/Jij-Inc/ommx/pull/853))

Constraints, decision variables, and named functions used to carry
their metadata (`name`, `subscripts`, `parameters`, `description`, and
— for constraints only — `provenance`) inline on each element. In v3
the same fact lives in **one canonical place per collection**: a
Struct-of-Arrays metadata store keyed by ID, riding alongside the
constraint / variable / named-function map. Per-element structs shrink
to their intrinsic data and the SoA store is the canonical source for
both per-id reads and bulk DataFrame analysis.

Three store families share one shape:
[`ConstraintMetadataStore<ID>`](crate::ConstraintMetadataStore) on
every constraint-kind collection (the same store rides through
[`EvaluatedCollection<T>`](crate::EvaluatedCollection) and
[`SampledCollection<T>`](crate::SampledCollection) so metadata is
available at every stage),
[`VariableMetadataStore`](crate::VariableMetadataStore) as a sibling
field on `Instance` / `ParametricInstance` / `Solution` / `SampleSet`
(no separate `DecisionVariableCollection` was introduced), and
[`NamedFunctionMetadataStore`](crate::NamedFunctionMetadataStore) the
same way for named functions.

The split lets the type system enforce invariants more tightly. The
raw active/removed map mutators on `ConstraintCollection<T>` are
`pub(crate)` and `Instance` never hands out
`&mut ConstraintCollection<T>`, so external callers must go through
the validating `Instance::add_*` / `relax_*` / `restore_*` family —
which keep variable-id validity (every `id` referenced by a constraint
exists in `decision_variables`) and active/removed disjointness as
crate-internal invariants. Metadata mutation rides on its own `_mut()`
accessor and can't break either.

The Python side wraps the same SoA store with two parallel
user-facing changes:

- `instance.constraints[id]` and the parallel constraint / variable
  accessors return live `AttachedX` write-through handles
  ([#849](https://github.com/Jij-Inc/ommx/pull/849),
  [#850](https://github.com/Jij-Inc/ommx/pull/850),
  [#852](https://github.com/Jij-Inc/ommx/pull/852)).
- `*_df()` methods (with `kind=` / `include=` / `removed=` parameters)
  plus six long-format sidecar DataFrames serve bulk analysis directly
  off the SoA store ([#846](https://github.com/Jij-Inc/ommx/pull/846),
  [#847](https://github.com/Jij-Inc/ommx/pull/847)).

See [`PYTHON_SDK_MIGRATION_GUIDE.md`](https://github.com/Jij-Inc/ommx/blob/main/PYTHON_SDK_MIGRATION_GUIDE.md)
§9–11 for the user-facing version.

The migration guide's [Metadata stores](crate::doc::migration_guide#metadata-stores)
section has the per-host accessor list and the store API reference.

## Capability model ([#790](https://github.com/Jij-Inc/ommx/pull/790), [#805](https://github.com/Jij-Inc/ommx/pull/805), [#810](https://github.com/Jij-Inc/ommx/pull/810), [#811](https://github.com/Jij-Inc/ommx/pull/811), [#814](https://github.com/Jij-Inc/ommx/pull/814))

First-class special constraints raise a deployment question: not every
solver supports indicator / one-hot / SOS1 natively, and some only
support a subset. v3 answers this with an explicit capability model
built into the domain layer.

[`AdditionalCapability`](crate::AdditionalCapability) is an enum of the
non-standard constraint kinds, and [`Capabilities`](crate::Capabilities)
is a sorted set of them. Two `Instance` methods anchor the model:

- [`Instance::required_capabilities()`](crate::Instance::required_capabilities)
  returns the capabilities an instance actually uses (only active
  constraints are considered; removed constraints are excluded since
  they are never passed to the solver).
- [`Instance::reduce_capabilities(&supported)`](crate::Instance::reduce_capabilities)
  takes the capabilities a given solver can handle and converts every
  unsupported kind into regular constraints via the canonical encodings
  (Big-M for indicator / SOS1, linear equality for one-hot). On return,
  `required_capabilities()` is a subset of `supported`.

Adapters declare what they natively support and call
`reduce_capabilities` before handing the instance to the underlying
solver, so an adapter can accept any valid OMMX `Instance` without
asking users to manually encode special constraints themselves. The
conversion is fallible — e.g. the Big-M encoding requires finite
bounds — and returns `Err(ommx::Error)` when the instance can't be
reduced; otherwise each conversion is emitted as an INFO-level
`tracing` event in the `reduce_capabilities` span for observability.

## Unified error surface ([#832](https://github.com/Jij-Inc/ommx/pull/832))

The default error type is [`ommx::Result<T>`](crate::Result) /
[`ommx::Error`](crate::Error), re-exports of `anyhow::Result<T>` and
`anyhow::Error` so downstream crates can propagate with `?` without
taking an `anyhow` dependency themselves. New crate-level fail-site
macros — [`bail!`](crate::bail) / [`error!`](crate::error!) /
[`ensure!`](crate::ensure) — emit a `tracing::error!` event alongside
producing the `anyhow::Error`, so diagnostic context lands in the
configured tracing subscriber rather than being stacked via
`anyhow::Error::context(...)` at the fail site.

The previous discriminant-style error enums (`InstanceError`,
`MpsParseError`, `StateValidationError`, `LogEncodingError`,
`UnknownSampleIDError`, the variants of `QplibParseError`, …) have
been removed — downstream code never matched on their variants in
practice. A handful of typed surfaces are deliberately kept:

- A curated set of **signal types** ([`InfeasibleDetected`](crate::InfeasibleDetected),
  [`BoundError`](crate::BoundError),
  [`DecisionVariableError`](crate::DecisionVariableError),
  [`DuplicatedSampleIDError`](crate::DuplicatedSampleIDError),
  [`SubstitutionError`](crate::SubstitutionError), …) returned typed
  by their entry-point APIs (`Bound::new`, `Sampled::append`,
  `Substitute::*`, …) for callers that recover by discriminant.
- Two narrow-domain parser errors that carry *positional* breadcrumbs
  ([`ParseError`](crate::ParseError),
  [`qplib::QplibParseError`](crate::qplib::QplibParseError)),
  converted to `ommx::Error` at the domain boundary.

The curated signal-type list and downcast / propagation patterns live
in the [error handling tutorial](crate::doc::tutorial::error_handling).

## Tracing-first observability ([#816](https://github.com/Jij-Inc/ommx/pull/816), [#826](https://github.com/Jij-Inc/ommx/pull/826))

Internal logging has moved off `log` to `tracing`, and span coverage has been
broadened across parsing, evaluation, substitution, and solver adapter
entry points. Subscribers (including `tracing-opentelemetry`) pick up
structured fields and span context directly from the crate. New
internal fail sites use the crate-level `bail!` / `error!` / `ensure!`
macros instead of stacking context via `anyhow::Error::context(...)`,
so diagnostic information lands as a `tracing::error!` event at the
moment it's produced rather than being attached to the error chain
(legacy `.context(...)` call sites in narrow-domain parsers and the
artifact layer remain).

## Domain types replace `v1_ext` ([#799](https://github.com/Jij-Inc/ommx/pull/799), [#801](https://github.com/Jij-Inc/ommx/pull/801), [#803](https://github.com/Jij-Inc/ommx/pull/803), [#804](https://github.com/Jij-Inc/ommx/pull/804))

The proto-generated `v1::Instance` / `v1::Constraint` / `v1::Function` types
are now reserved for wire-format interop. All in-memory operations — QUBO/HUBO
conversions, slack helpers, relaxation, propagation, evaluation — are defined
on the domain types ([`Instance`](crate::Instance),
[`Constraint`](crate::Constraint), [`Function`](crate::Function), …) in the
crate root. The `v1_ext` helper module has been removed.

Two new domain traits accompany this shift:

- [`Propagate`](crate::Propagate) performs unit-propagation-style constraint
  reasoning and returns a [`PropagateOutcome`](crate::PropagateOutcome) —
  `Active(T)` (constraint shrunk in place), `Consumed(T)` (fully determined
  by the state, move to removed), or `Transformed { original, new }` (kind
  change, e.g. an indicator constraint promoted to a regular constraint).
  Callers that need provenance bookkeeping append a `Provenance` entry to
  the host's [`ConstraintMetadataStore`](crate::ConstraintMetadataStore)
  when they apply the outcome.
- [`Substitute`](crate::Substitute) performs symbolic variable substitution,
  with an acyclic fast path and full cycle detection.

## Other notable changes

- `ommx-derive` introduces `#[derive(LogicalMemoryProfile)]` for structural
  memory profiling
  ([#800](https://github.com/Jij-Inc/ommx/pull/800)).
- `ommx::doc` is now the entry point on docs.rs for long-form prose
  (this page, the [migration guide](crate::doc::migration_guide), and the
  [tutorial](crate::doc::tutorial)).
