releases
SolverForge 0.13.x: Streaming Defaults, Typed Search, and Explicit Scoring
SolverForge 0.13.x publishes streaming model-aware search defaults, typed custom search registration, match-shape collectors, direct cross-join grouping, and the explicit scoring terminal surface.
SolverForge 0.13.x was the previous core runtime line. It starts with 0.13.0, published on 2026-05-12, with API docs on docs.rs. The 0.13.x patch is 0.13.1, published on 2026-05-14, with API docs at docs.rs.
The release was not a CLI scaffold refresh. At the time,
solverforge-cli 2.0.4 scaffolded solverforge 0.11.1,
solverforge-ui 0.6.5, and
solverforge-maps 2.1.4. Direct Cargo projects and deliberately upgraded
generated apps can target the published solverforge 0.13.1 crate.
What Changed
Scoring terminals are explicit
Constraint streams no longer expose the older helper family such as
penalize_hard, penalize_with, reward_soft, or reward_hard_with. Use
penalize(score), reward(score), or a typed dynamic closure:
type Streams = ConstraintFactory<Schedule, HardSoftScore>;
Streams::new()
.for_each(Schedule::shifts())
.filter(|shift: &Shift| shift.employee_idx.is_none())
.penalize(HardSoftScore::ONE_HARD)
.named("Unassigned shift");
Streams::new()
.for_each(Schedule::shifts())
.penalize(|shift: &Shift| HardSoftScore::of_soft(shift.preference_penalty()))
.named("Preference penalty");
Dynamic closure weights are non-hard metadata by default. Wrap a fixed or
dynamic weight with hard_weight(...) when analysis metadata and
conflict-repair matching should classify the constraint as hard:
type Streams = ConstraintFactory<Schedule, HardSoftScore>;
Streams::new()
.for_each(Schedule::shifts())
.penalize(hard_weight(|shift: &Shift| {
HardSoftScore::of_hard(shift.overtime_hours() as i64)
}))
.named("Overtime");
Collectors own grouped payloads
collect_vec(...) is now part of the public collector surface. It retains
mapped values once and exposes them as CollectedVec<T>, so grouped payloads do
not need Copy, Clone, or PartialEq just to be collected.
indexed_presence(...) adds a stock ordinal-presence collector for rules that
need covered and missing ranges. It pairs with the existing count, sum,
load_balance, and consecutive_runs collectors.
In 0.13.1, collectors are generic over the stream match shape. The public
collector trait is Collector<Input>, so the same stock collector protocol
works for unary rows, projected rows, and joined pairs. Direct cross joins can
now group joined pairs without materializing projected rows first:
type Streams = ConstraintFactory<Plan, HardSoftScore>;
Streams::new()
.for_each(Plan::assignments())
.join((
Streams::new().for_each(Plan::capacities()),
equal_bi(
|assignment: &Assignment| assignment.capacity_id,
|capacity: &Capacity| Some(capacity.id),
),
))
.group_by(
|assignment: &Assignment, capacity: &Capacity| (assignment.id, capacity.id),
sum(|(assignment, capacity): (&Assignment, &Capacity)| {
capacity.amount - assignment.demand
}),
)
.penalize(hard_weight(|_key: &(usize, usize), shortage: &i64| {
HardSoftScore::of_hard((-*shortage).max(0))
}))
.named("Capacity shortage");
Projected grouped streams can also continue into complement(...) or
complement_with_key(...), so supply/demand rules can group retained scoring
rows and still produce explicit rows for missing keys.
Joined filters use source indexes
0.13.1 fixes the last placeholder index path in joined filters. Low-level
Bi/Tri/Quad/Penta filter traits now receive semantic source indexes for the
rows being tested. Same-source joins use entity indexes, cross joins use left
and right source indexes, flattened rows use the left source index and owning
right-side source index, and projected self-joins use each projected row’s
primary owner entity index.
Normal fluent .filter(|a, b| ...) predicates remain entity-oriented. This
matters when you implement low-level scoring extensions, inspect retained match
identity, or depend on localized incremental updates across direct cross,
projected, flattened, and higher-arity joins.
Default search is streaming-first
When full phases are omitted, the runtime builds model-aware construction and
then one streaming local-search phase. When an explicit acceptor_forager
local-search phase omits move_selector, SolverForge now resolves typed
defaults from declared model capabilities:
- nearby scalar change/swap selectors when nearby hooks are present
- plain scalar change/swap fallbacks for non-assignment-owned scalar slots
- grouped scalar selectors for declared scalar groups
- conflict-repair selectors only when repair providers are registered
- nearby list change/swap, sublist change/swap, and reverse selectors for list variables, with k-opt and list ruin only when their hooks exist
Assignment-owned scalar variables stay on the grouped scalar path. Generic scalar selectors and default conflict-repair selectors exclude those slots.
VND is a local-search type
Variable Neighborhood Descent remains available, but it is configured as a
local_search_type on a local-search phase:
[[phases]]
type = "local_search"
local_search_type = "variable_neighborhood_descent"
[[phases.neighborhoods]]
type = "change_move_selector"
variable_name = "employee_idx"
[[phases.neighborhoods]]
type = "swap_move_selector"
variable_name = "employee_idx"
The old standalone type = "vnd" phase shape is not the current config
surface.
Accepted-count is a real horizon
The accepted-count forager now means “collect this many accepted candidates in
the current step, then choose the best among them.” Use best_score when the
search policy should scan the whole neighborhood.
Typed custom search replaces class-name loading
Solutions can compile in custom search code with
#[planning_solution(search = "...")]. The search function receives a
SearchContext, registers typed phase names, and solver.toml orders those
names:
[[phases]]
type = "custom"
name = "weekend_repair"
SolverForge does not load arbitrary custom_phase_class strings or use an
erased plugin registry.
Partitioned and exact search are typed surfaces
partitioned_search requires a named typed SolutionPartitioner; it does not
infer safe partitions from a count. Exact tree search is exposed through typed
runtime APIs such as ExhaustiveSearchPhase, ExhaustiveSearchConfig,
ExplorationType, and SimpleDecider, and should be registered as custom
search when an application owns the concrete decider.
Install And Scaffold Status
For direct Cargo projects:
solverforge = { version = "0.13.1", features = ["serde", "console"] }
If you write custom incremental constraints that need lower-level identities, the companion workspace crates are also published at the same 0.13.x patch:
solverforge-core = "0.13.1"
For generated apps, confirm the installed CLI target:
solverforge --version
At the time of this release, solverforge-cli 2.0.4 reported:
CLI version: 2.0.4
Scaffold runtime target: SolverForge crate target 0.11.1
Scaffold UI target: solverforge-ui 0.6.5
Scaffold maps target: solverforge-maps 2.1.4
Runtime source: crates.io: solverforge 0.11.1
UI source: crates.io: solverforge-ui 0.6.5
Maps source: crates.io: solverforge-maps 2.1.4
Generated apps created with that CLI start on solverforge 0.11.1. Move a
generated app to solverforge 0.13.x only when you are deliberately upgrading
that app’s runtime dependency and validating the generated code against the
newer core crate.
Patch History
| Version | Date | Notes |
|---|---|---|
0.13.1 |
2026-05-14 | Generalizes collectors to Collector<Input>, adds direct cross-join grouping, allows projected grouped complements, and preserves semantic source indexes for joined filters. |
0.13.0 |
2026-05-12 | Adds collect_vec and indexed_presence, exposes explicit weight wrappers, restores generated constraint-stream convenience traits, moves VND under local search, adds typed custom search registration, and makes default search streaming-first. |
Documentation Changes
At publication, the docs tree was updated for the 0.13.x runtime surface:
- Constraint Streams
shows the current
penalize(...)/reward(...)terminal API, direct cross-join grouping, and the index-aware joined filter contract. - Collectors documents
collect_vec(...),indexed_presence(...), and the genericCollector<Input>match-shape contract beside the existing collectors. - Projected Scoring Rows covers projected grouped complements.
- Configuration covers VND as
local_search_type, typedcustomphase names, and named partitioners. - Local Search explains the accepted-count horizon and fair union selector ordering.
- Status & Roadmap separates the published
solverforge 0.13.1runtime from the then-currentsolverforge-cli 2.0.4scaffold target that was current at publication time.