releases
solverforge 0.19.8: Three-Source Keyed Joins
solverforge 0.19.8 adds a third join source to keyed cross streams and the retained cross-tri incremental constraint that scores (A, B, C) rows, built on one arity-generic cross-join engine shared with the existing cross-bi constraint.
solverforge 0.19.8 is published on crates.io and released on GitHub
(2026-10-02). At publication, cargo add solverforge installed 0.19.8; its
versioned docs.rs API remains available. The previous package, solverforge 0.19.7, put every workspace
version surface under the release tooling. This release keeps the Rust 1.95
toolchain floor and the compiled runtime-graph execution model unchanged, and
moves the scoring surface, not the runtime contract.
What Changed
Historical 0.19.8 architecture
The engine names and shared-key example below describe the 0.19.8 tag, not
the current API. In 0.19.9 these engines were removed in favor of JoinNode
and OperatorTerminal; independent-key chaining uses equal_on(...).
Keyed cross joins reach a third source
Cross-bi streams dead-ended at arity 2. Their surface had filter,
flatten_last, group_by, project, and weighting, but no way to introduce a
third source, so a rule over three keyed sources had to denormalize a joined
pair into a synthetic row type first — or fall back to a hand-written
constraint.
The bi stream now carries an inherent .join((extractor_c, key_c)) that
extends the joined (A, B) pairs with a third source C:
type Streams = ConstraintFactory<Schedule, SoftScore>;
Streams::new()
.for_each(Schedule::shifts())
.join((
Streams::new().for_each(Schedule::employees()),
equal_bi(
|shift: &Shift| shift.employee_idx,
|employee: &Employee| Some(employee.index),
),
))
.join((Schedule::unavailability(), |u: &Unavailability| u.employee_idx))
.penalize(|_shift: &Shift, _employee: &Employee, _u: &Unavailability| SoftScore::of(1))
.named("Shift falls on an unavailability day")
The bi stream’s own keys stay authoritative for A and B, and the join target
carries only the new source and its key, so the engine keeps probing one shared
key domain. The retained bi filter is lifted into the tri chain rather than
dropped, which is what preserves the filter contract and the semantic source
indexes across the arity change.
Three-source joins are also reachable through #[solverforge_constraints].
Raw join chains flow through the attribute in passthrough mode and need no
compiler change; a compile-pass fixture now builds a
uni -> cross bi -> cross tri -> penalize chain through the macro and asserts
its score instead of only asserting that it compiles.
One arity-generic engine behind cross-bi and cross-tri
The retained cross-bi constraint kept its match rows, per-source key indexes,
and swap_remove bucket maintenance as hand-written fields. Generalizing to
higher arities by hand-copying that bookkeeping per arity is where drift bugs
live, so the state moved into a single CrossJoinEngine<N, K, Sc> under a
hidden constraint::cross_incremental module: match rows keyed by
[usize; N] tuples, per-source key indexes, and per-source inverted row
buckets, all parameterized by source count.
cross_bi_incremental::Bi is now that engine at N = 2, and the new
cross_tri_incremental::Tri is the same engine at N = 3. The bi constraint’s
public API, match semantics, and change localization are unchanged;
interleaved performance counters show roughly a 0.3% instruction delta, inside
the project’s 5% hot-path gate.
Cross-tri rows localize symmetric in any source
A retained (A, B, C) row requires key_a(a) == key_b(b) == key_c(c) plus the
tri filter. A change to any of the three sources probes that source’s retained
key index and its candidate set, so incremental cost does not depend on which
source mutated. Filters receive semantic per-source slice indexes
(a_idx, b_idx, c_idx), matching the joined-filter-index contract the lower
arity joins already document, and change localization follows the same
assert_localizes contract — including failing explicitly on unlocalizable
callbacks rather than silently recomputing.
The build gate runs the Rust checks CI runs
make ci-local previously ran a narrower set of Cargo commands than the
checked-in CI workflow: an all-feature workspace build and Clippy, but not the
all-target Clippy pass, the integration tests, or the doctests. A local run
could therefore pass while the same commit failed in CI. The Makefile now runs
the same validation commands as the workflow, keeping the change entirely in the
existing Makefile with no new scripts or dependencies.
Published Version Boundaries
| Surface | Version | Notes |
|---|---|---|
| Core runtime | solverforge 0.19.8 |
Rust 1.95; docs.rs serves the 0.19.8 API; adds keyed cross-tri joins |
| Core runtime (previous) | solverforge 0.19.7 |
Every workspace version surface owned by the release tooling |
| Core runtime (previous) | solverforge 0.19.6 |
Planning pin predicate and enforcement |
| Scaffold target | solverforge-cli 3.3.3 |
Still scaffolds solverforge 0.19.7; the scaffold line moves only when a CLI release deliberately retargets it |
Patch History
| Version | Date | Notes |
|---|---|---|
0.19.9 |
2026-10-08 | Independent-key joins and Chain past Penta, grouped joins, composable low-level row producers over the common operator tree, and the lost resume wakeup fix. |
0.19.8 |
2026-10-02 | Adds a third join source to keyed cross streams and the retained cross-tri incremental constraint, both on one arity-generic cross-join engine; the local build gate runs the Rust checks CI runs. |
0.19.7 |
2026-09-25 | Owns every workspace version surface from the release tooling, so the workspace, crates, and tags are cut from one declaration. |
0.19.6 |
2026-09-25 | Exposes the typed planning pin predicate, rejects scalar edits to pinned entities, preserves pinned list owners across construction and search, retains pinned assignments in exhaustive search, and skips pin scanning for models without pins. |
0.19.5 |
2026-09-15 | Retains the complete solution on cancellation, initializes score history with a slice fill, and preserves complete Clarke-Wright construction. |
Upgrade Checklist
- Run
cargo update -p solverforgeand confirm the resolved version is0.19.8. - Replace any three-source rule that denormalized a joined pair into a synthetic
row type with a chained
.join((a, equal_bi(...))).join((c, key_c))and score the(A, B, C)tuple directly. - Two-source cross joins need no change; their surface, semantics, and change localization are unchanged, and the underlying engine was only reparented.
- Generated CLI projects still scaffold
solverforge 0.19.7; move an app to 0.19.8 deliberately and re-run its model, constraint, and solve tests, since the scaffold target and the current core release move independently.
Where to read next
Use the SolverForge overview for the current crate
surface, the constraint streams
page for the join chain, and the
joiners page for equal and
equal_bi.