releases

SolverForge 0.9.x: Manifest-Owned Models and Runtime Tightening

SolverForge 0.9.x makes the scalar/list architecture explicit: planning_model! is the canonical model manifest, scalar metadata is descriptor-addressed, and the patch line tightens scoring and local search.

SolverForge 0.9.x is the runtime line where the model contract becomes manifest-owned and scalar/list terminology becomes explicit. The line starts at 0.9.0 and includes the 0.9.1 runtime patch for indexed existence scoring and local-search startup visibility.

Patch releases are folded into this line note instead of published as separate release-note pages.

This is the release that makes the current SolverForge model contract explicit. The 0.8 line established retained jobs, snapshots, checkpoints, and lifecycle events. The 0.9 line tightens the modeling layer underneath that runtime: non-list planning variables are now consistently scalar, model metadata is owned by a planning_model! manifest, and scalar construction plus local search use declared model capabilities instead of proc-macro expansion order or hidden fallback behavior.

Why this release matters

SolverForge applications increasingly need models that combine scalar and list planning variables: assignment fields, routing lists, sequencing lists, optional slots, retained lifecycle controls, and browser-visible progress all in the same generated service. That only works if the runtime has one deterministic view of the model.

0.9.x moves that responsibility to the model boundary itself:

// src/domain/mod.rs
solverforge::planning_model! {
    root = "src/domain";

    mod employee;
    mod shift;
    mod schedule;

    pub use employee::Employee;
    pub use shift::Shift;
    pub use schedule::Schedule;
}

The manifest preserves ordinary Rust files and exports while giving SolverForge a stable model-owned place to validate entities, facts, planning solutions, scalar hooks, list-shadow wiring, and runtime metadata.

For users starting from solverforge-cli, this is already the normal shape. Current CLI-generated projects target:

  • solverforge-cli 2.0.0
  • solverforge 0.9.0
  • solverforge-ui 0.6.1
  • solverforge-maps 2.1.3

Run solverforge --version to confirm the exact scaffold targets in your installed CLI.

What changed

planning_model! is the canonical manifest

Generated retained-runtime domains now require a planning_model! manifest. The manifest lists the real Rust modules and public exports for the domain, then SolverForge derives deterministic metadata from those modules.

That means module declaration order is no longer a hidden modeling contract. Scalar runtime metadata is resolved by descriptor index and variable name, while the compact generated variable_index remains an internal getter/setter dispatch detail.

The practical result: split your domain into normal files, export the public types from src/domain/mod.rs, and let the manifest own the model contract.

scalar is the public name for non-list variables

0.9.x finishes the terminology cleanup around planning-variable shape.

  • Use scalar for single-value assignment variables.
  • Use list for ordered planning collections.
  • Treat mixed as descriptive shorthand for a model or generated app that has both scalar and list variables. It is not a third planning-variable kind.
  • Do not use standard as a model-shape word. It is only a demo data size label in current scaffolded projects.

The CLI enforces the variable-kind boundary directly: solverforge generate variable --kind accepts scalar or list. The runtime still uses mixed-shape reasoning where a ModelContext contains both variable families, but the public variable families remain exactly scalar and list.

Scalar hooks are declared on the model

Nearby scalar neighborhoods and sorted scalar construction are explicit model capabilities. If a search or construction policy needs distance or ordering logic, declare that on the scalar planning variable:

#[planning_variable(
    value_range = "workers",
    allows_unassigned = true,
    nearby_value_distance_meter = "worker_value_distance",
    nearby_entity_distance_meter = "task_distance",
    construction_entity_order_key = "task_priority",
    construction_value_order_key = "worker_priority"
)]
pub worker: Option<usize>;

Nearby hooks guide local-search neighborhoods. Construction order keys guide construction-phase ordering. SolverForge evaluates construction order hooks against the live working solution at each construction step, so queue-style, weakest-fit, and strongest-fit heuristics track the current model state instead of a phase-start snapshot.

Construction routing is capability-driven

Construction phases now route through validated capabilities rather than broad special cases.

  • Generic first_fit and cheapest_insertion stay on the canonical scalar/list construction engine when matching list work is present.
  • Pure scalar construction routes through the descriptor-scalar construction boundary.
  • Scalar-only sorted heuristics require the matching scalar construction hooks.
  • List-only phases validate their list hook surface before the phase starts.

This gives configuration errors a clearer cause: the model either declares the capability a configured phase needs, or the phase is rejected up front.

Move selectors are cursor-based

Move selectors now use cursor/materialization semantics. open_cursor() yields stable candidate indices and borrowable candidates, and the runtime materializes owned moves only for the selected winner.

That matters for cartesian neighborhoods. They need preview-safe child selectors and cannot rely on broad owned-stream helpers as the public selector contract. The result is cleaner ownership, safer previews, and less accidental move allocation in search hot paths.

Selector and tabu correctness is tighter

The 0.9.0 fix set focuses heavily on selector legality and move identity:

  • cartesian composites validate legality and stay preview-safe
  • scalar nearby metadata matches ruin-recreate legality
  • seeded scalar ruin-recreate streams are honored
  • tabu signatures normalize self-inverse and undo identities
  • acceptor hooks compare candidate moves against the correct undo signatures
  • selector limits prune scalar nearby candidates before limiting

The theme is the same as the manifest work: the runtime should reason from one canonical model and one canonical move identity, not from incidental construction details.

Exact usize existence keys use indexed storage

Since 0.9.1, if_exists(...) and if_not_exists(...) keep the same public constraint-stream shape while exact usize join keys use dense indexed bookkeeping for direct and flattened existence constraints.

Other key shapes, including Option<usize>, newtype IDs, strings, and composite keys, keep hashed storage. This makes common route-ID and assignment-ID checks cheaper without changing application constraint code.

Local-search phase starts show the score

The console now shows the current score when local search starts:

0.002s ▶ Local Search started │ 0hard/-50soft

That score is calculated before local search begins, so the first local-search line shows exactly what construction handed to the search phase.

List ruin skips empty owners

The list-ruin selector samples only entities whose list is non-empty. Empty owners can still receive elements during ordinary list-change, sublist-change, or rebuild-style search; this only removes wasted ruin candidates.

Breaking changes

There are two public breaking areas.

First, generated retained-runtime domains now require planning_model!. If you maintain a hand-written domain that previously relied only on individual derive macros, add a domain manifest and export the model types there.

Second, direct solver APIs that build scalar runtime machinery now require variable-index-aware access metadata. This affects scalar getter/setter callbacks, ScalarVariableContext::new, scalar move constructors, scalar selector constructors, and RuinMoveSelector. Most application code goes through the facade, macros, or CLI scaffold and should not touch these APIs directly.

Upgrade notes

For new applications:

cargo install solverforge-cli --force
solverforge new my-project
cd my-project
solverforge --version

For existing applications:

  1. Add or update src/domain/mod.rs to use solverforge::planning_model!.
  2. Replace stale variable-kind wording with scalar or list; use mixed only as a description for models or apps that contain both.
  3. Declare nearby and construction hooks on scalar variables when your solver.toml policy uses those capabilities.
  4. Update custom direct scalar-runtime code to the variable-index-aware APIs.
  5. Re-run the generated app tests and any retained lifecycle checks.

The latest 0.9.x patch is solverforge 0.9.1:

solverforge = { version = "0.9.1", features = ["serde", "console"] }

The crate metadata for solverforge 0.9.0 requires Rust 1.92. Projects using the console, serde, decimal, or verbose-logging feature flags should keep those feature selections explicit in Cargo.toml.

Patch History

Version Date Notes
0.9.1 2026-04-26 Adds indexed exact-usize existence storage, local-search startup score output, and empty-owner filtering for list ruin.
0.9.0 2026-04-24 Introduces planning_model!, scalar/list public terminology, descriptor-addressed scalar metadata, and capability-driven construction routing.

The current path

The recommended starting point remains the CLI:

cargo install solverforge-cli --force
solverforge new my-scheduler
cd my-scheduler
solverforge generate fact employee --field skill:String
solverforge generate entity shift --field starts_at:String --field ends_at:String
solverforge generate variable employee_idx --entity Shift --kind scalar --range employees --allows-unassigned
solverforge generate data --size standard
solverforge server

Use the solverforge-cli manual for command details, the current architecture article for the product shape, and the hospital scheduling use case for a complete retained-job application built on the current stack.

SolverForge 0.9.x is the line where that stack becomes stricter in the right place: the model owns its metadata, the runtime reads one canonical scalar/list contract, and generated applications start from the same surface users extend.