pubsat 0.1.0

Building blocks for SAT-based dependency resolvers: a node-semver-compatible range parser, an ecosystem-independent constraint vocabulary, and a backend-agnostic SAT problem/solver abstraction with a Varisat backend.
Documentation
//! Constraint types describing dependency relationships.
//!
//! `pubsat`'s [`Constraint`] enum is the input vocabulary for the
//! SAT encoder. Each variant is an ecosystem-independent
//! statement about packages and versions — "package A version X
//! depends on package B in some version range," "at most one
//! version of package C may be selected," etc.
//!
//! Ecosystem-specific concepts (npm's `workspace:` protocol,
//! pnpm-style hoisting, Cargo features) live in consumer crates
//! and lower themselves to the primitives here. Keeping
//! `pubsat`'s vocabulary minimal is what makes it reusable across
//! ecosystems.

use semver::Version;

use crate::version::VersionSet;

/// A statement the SAT encoder must enforce.
///
/// All variants are *hard* constraints (must hold in any solution)
/// unless explicitly described otherwise. Soft preferences (e.g.
/// "prefer newer versions") are expressed via assumptions on the
/// solver, not via `Constraint` variants.
#[derive(Debug, Clone)]
pub enum Constraint {
    /// `dependent` (if present) depends on `package` in a version
    /// satisfying `version_set`. If `dependent` is `None`, this
    /// is a top-level / root dependency.
    Dependency {
        package: String,
        version_set: VersionSet,
        dependent: Option<String>,
    },

    /// `package1@version1` and `package2@version2` may not both be
    /// selected. Two-clause exclusion.
    Conflict {
        package1: String,
        version1: Version,
        package2: String,
        version2: Version,
    },

    /// At most one of the listed versions of `package` may be
    /// selected. The standard "one version per package" rule for
    /// most ecosystems.
    AtMostOne {
        package: String,
        versions: Vec<Version>,
    },

    /// At least one of the listed versions of `package` must be
    /// selected. Used when a dependency edge has multiple
    /// satisfying versions and we want the solver to pick *some*
    /// version.
    AtLeastOne {
        package: String,
        versions: Vec<Version>,
    },

    /// Exactly this `version` of `package` must be selected.
    /// Used for pinning (e.g. lockfile-frozen installs).
    Exactly { package: String, version: Version },

    /// This specific `(package, version)` pair must not be
    /// selected. Used for security advisories and explicit denies.
    Exclude { package: String, version: Version },

    /// If `premise_package@premise_version` is selected, then
    /// `conclusion_package` must be selected with a version
    /// satisfying `conclusion_version_set`. The base shape of a
    /// dependency edge in CNF.
    Implies {
        premise_package: String,
        premise_version: Version,
        conclusion_package: String,
        conclusion_version_set: VersionSet,
    },

    /// Like [`Self::Dependency`] but marked as not-required: the
    /// solver should satisfy it if it can, and skip it without
    /// error if it can't.
    OptionalDependency {
        package: String,
        version_set: VersionSet,
        dependent: Option<String>,
        /// Platform-specific condition (e.g., "os=linux",
        /// "arch=x64"). Interpretation is up to the caller; the
        /// SAT encoder treats it as opaque metadata.
        condition: Option<String>,
    },

    /// Like [`Self::Dependency`] but only active when `condition`
    /// is true. Caller is responsible for evaluating `condition`
    /// and either including or omitting this constraint
    /// accordingly; the SAT encoder treats it as a regular
    /// dependency when present.
    ConditionalDependency {
        package: String,
        version_set: VersionSet,
        dependent: Option<String>,
        condition: String,
    },
}

impl Constraint {
    /// Create a dependency constraint.
    pub fn dependency(package: String, version_set: VersionSet, dependent: Option<String>) -> Self {
        Self::Dependency {
            package,
            version_set,
            dependent,
        }
    }

    /// Create an optional-dependency constraint.
    pub fn optional_dependency(
        package: String,
        version_set: VersionSet,
        dependent: Option<String>,
        condition: Option<String>,
    ) -> Self {
        Self::OptionalDependency {
            package,
            version_set,
            dependent,
            condition,
        }
    }

    /// Create a conditional-dependency constraint.
    pub fn conditional_dependency(
        package: String,
        version_set: VersionSet,
        dependent: Option<String>,
        condition: String,
    ) -> Self {
        Self::ConditionalDependency {
            package,
            version_set,
            dependent,
            condition,
        }
    }

    /// Return the primary package name this constraint references,
    /// if there is one canonical answer. For variants with
    /// multiple packages (conflicts, workspaces), returns the
    /// "subject" package per the variant's docs.
    pub fn primary_package(&self) -> Option<&str> {
        match self {
            Self::Dependency { package, .. }
            | Self::AtMostOne { package, .. }
            | Self::AtLeastOne { package, .. }
            | Self::Exactly { package, .. }
            | Self::Exclude { package, .. }
            | Self::OptionalDependency { package, .. }
            | Self::ConditionalDependency { package, .. } => Some(package),
            Self::Conflict { package1, .. } => Some(package1),
            Self::Implies {
                premise_package, ..
            } => Some(premise_package),
        }
    }

    /// True if this constraint is a soft preference rather than a
    /// hard requirement (i.e. it's allowed to be unsatisfied
    /// without flagging UNSAT).
    pub fn is_optional(&self) -> bool {
        matches!(self, Self::OptionalDependency { .. })
    }

    /// True if this constraint's activation depends on a
    /// caller-evaluated condition.
    pub fn is_conditional(&self) -> bool {
        matches!(self, Self::ConditionalDependency { .. })
    }
}