pubsat
SAT-based dependency resolution for semver-shaped constraint problems — the kind that show up in npm, Cargo, Composer, pip, and every other package ecosystem with caret / tilde / hyphen / range syntax.
pubsat is backend-agnostic (defaults to
Varisat) and
ecosystem-agnostic: callers implement PackageRegistry for
their package source, hand the resolver a starting set of
dependencies, and get back a concrete (name, version) selection
or a structured conflict report.
When to reach for this
- You're writing a dependency resolver (private artifact registry, build tool, alternative package manager) and want to skip the semver-range parsing, SAT encoding, and version-preference logic.
- You need an npm-flavour range parser (caret, tilde, hyphen,
x-ranges, dist tags) that's stricter than
semverbut more permissive thannode-semverparsers that reject the punctuation drift real registries contain. - You want a resolver whose preference policy (newest-wins, conservative, custom) is expressible as SAT assumptions rather than baked into the search strategy.
End-to-end example
use Arc;
use DependencyGraphBuilder;
use ;
use DependencyResolver;
use VersionSet;
use Version;
#
# async
A runnable version lives in examples/resolve_mock.rs.
Run it with cargo run --example resolve_mock.
What's in the crate
| Module | What it provides |
|---|---|
version |
Node-semver-compatible range parser. Caret, tilde, hyphen, partial versions, >=/<=/>/< with optional whitespace, x-ranges, unions, dist tags. Plus VersionSet operations (intersection, union, satisfaction). |
constraint |
Ecosystem-independent Constraint vocabulary: Dependency, Conflict, AtMostOne, AtLeastOne, Exactly, Exclude, Implies, OptionalDependency, ConditionalDependency. |
registry |
PackageRegistry trait, single-flighted CachedRegistry wrapper, and in-process MockRegistry for tests and examples. |
graph |
DependencyGraph (petgraph-backed), DependencyNode, DependencyEdge, cycle detection, topological sort, DOT export. |
encoding |
ConstraintEncoder — lowers Constraints to CNF clauses with sequential at-most-one encoding for cardinality and a primable version cache for the hot path. |
builder |
DependencyGraphBuilder — walks a registry concurrently (configurable in-flight cap) to build a graph from a root VersionMetadata. |
resolver |
DependencyResolver — orchestrates graph → encoder → SAT problem → solver → resolution with greedy "prefer newest" SAT assumptions and per-package fallback on UNSAT. |
sat |
Backend-agnostic SAT types (SatProblem, Clause, Literal, SatModel, SatSolution) and the SatSolver trait. A VarisatSolver impl ships behind the default varisat-solver feature. |
error |
ResolutionError, SatError, EncodingResult, ConflictDetail, ConflictReason. |
94 unit tests (including 17 proptest-driven property
tests covering VersionSet algebra, the encoder's CNF semantics,
and resolver output invariants — ~2,300 case executions per cargo test invocation) plus 3 doctests; fmt + clippy (-D warnings) +
cargo-doc (RUSTDOCFLAGS=-Dwarnings) all clean in CI.
Algorithm
pubsat encodes each (package, version) pair as a SAT variable,
adds clauses for dependency edges and version-set membership, and
asks a SAT solver for a model. Version-preference ("newest wins")
is expressed as a set of assumptions on the solver rather than
as part of the algorithm's branching strategy. On UNSAT with all
preferences, the resolver drops preferences one at a time in
deterministic order — a greedy fallback that keeps a maximal
compatible subset rather than collapsing to "any satisfying
version everywhere".
Hot paths the implementation cares about:
- Single-flighted registry cache. Concurrent callers for the
same package share one upstream round-trip (
OnceCellper key, intercepted at all three reads). - Encoder primes its local version map from the resolver's
cache before encoding, so it filters in-process rather than
asking the registry once per
(package, version, dep_edge)tuple. Without this, encodingexpress-sized graphs makes ~7000 redundant fetches. - Sequential at-most-one cardinality encoding. Pairwise is O(n²) clauses — a package with 20 candidates would generate 190 binary clauses. The sequential/ladder encoding emits O(n) with auxiliary variables.
- Deterministic iteration.
HashMaps are sorted before SAT variable allocation so resolutions don't flap between runs (Varisat's branching is order-sensitive).
See docs/design/architecture.md
for the full design discussion (why SAT and not PubGrub, the
trade-offs accepted, the comparison with prior art, and the
"intentionally out of scope" boundary).
Stability
0.1.x: public API may evolve between minor versions until 1.0.
Breaking changes will be called out in CHANGELOG.md
with migration notes. The CNF representation (literal/clause
shapes) is the most stable; the higher-level PackageRegistry
trait and Constraint vocabulary are the layers most likely to
grow.
Features
varisat-solver(default) — pure-Rust Varisat CDCL backendcadical-solver— placeholder; no CaDiCaL Rust binding currently exposes the surface this crate needs. The feature flag and trait abstraction are in place so adding it later is a small change.
License
MIT — see LICENSE.