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 solverforge and confirm the resolved version is 0.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.

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.