diff --git a/Cargo.lock b/Cargo.lock index 245173c..db6d52b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,6 +25,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -198,6 +199,7 @@ dependencies = [ "addr", "anyhow", "base32", + "base58", "bimap", "blake3", "bstr", @@ -206,6 +208,7 @@ dependencies = [ "eka-root-macro", "gix 0.73.0", "hex", + "ignore", "insta", "lazy-regex", "nix-compat", @@ -213,6 +216,7 @@ dependencies = [ "path-clean 1.0.1", "pathdiff", "prodash 30.0.1", + "resolvo", "semver", "serde", "snix-castore", @@ -309,12 +313,34 @@ dependencies = [ "tower-service", ] +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base256emoji" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" +dependencies = [ + "const-str", + "match-lookup", +] + [[package]] name = "base32" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" +[[package]] +name = "base58" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" + [[package]] name = "base64" version = "0.21.7" @@ -380,8 +406,7 @@ dependencies = [ [[package]] name = "birdcage" version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "848df95320021558dd6bb4c26de3fe66724cdcbdbbf3fa720150b52b086ae568" +source = "git+https://github.com/nrdxp/birdcage?branch=clone_fs#fdcbe8b344bc2ae4d98fedec706644c967d00d35" dependencies = [ "bitflags 2.10.0", "libc", @@ -405,6 +430,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake3" version = "1.8.2" @@ -652,6 +689,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "config" version = "0.1.0" @@ -694,6 +740,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-str" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -825,6 +877,12 @@ dependencies = [ "crossterm", ] +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.6" @@ -952,6 +1010,26 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +[[package]] +name = "data-encoding-macro" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" +dependencies = [ + "data-encoding", + "syn 2.0.109", +] + [[package]] name = "der" version = "0.7.10" @@ -1123,9 +1201,13 @@ dependencies = [ "anyhow", "atom", "clap", + "config", "either", "gix 0.73.0", + "json-digest", + "nixec", "semver", + "serde_json", "tempfile", "thiserror 2.0.17", "tokio", @@ -1146,6 +1228,15 @@ dependencies = [ "syn 2.0.109", ] +[[package]] +name = "elsa" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9abf33c656a7256451ebb7d0082c5a471820c31269e49d807c538c252352186e" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "encode_unicode" version = "0.3.6" @@ -1210,6 +1301,17 @@ dependencies = [ "windows-sys 0.61.1", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + [[package]] name = "fastcdc" version = "3.2.1" @@ -1328,6 +1430,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "fuse-backend-rs" version = "0.12.1" @@ -3504,6 +3612,19 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + [[package]] name = "h2" version = "0.4.12" @@ -3936,6 +4057,22 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "imara-diff" version = "0.1.8" @@ -4159,6 +4296,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json-digest" +version = "0.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7196711951c49c12eb01341a7ff9b85fddae7fa9fbdcdd48e93a451953618aef" +dependencies = [ + "anyhow", + "multibase", + "rand 0.8.5", + "serde", + "serde_json", + "tiny-keccak", + "unicode-normalization", +] + [[package]] name = "kstring" version = "2.0.2" @@ -4390,6 +4542,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "match-lookup" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "matchers" version = "0.2.0" @@ -4551,6 +4714,18 @@ dependencies = [ "windows-sys 0.61.1", ] +[[package]] +name = "multibase" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" +dependencies = [ + "base-x", + "base256emoji", + "data-encoding", + "data-encoding-macro", +] + [[package]] name = "multimap" version = "0.10.1" @@ -4642,6 +4817,7 @@ dependencies = [ name = "nixec" version = "0.1.0" dependencies = [ + "anyhow", "birdcage", "thiserror 2.0.17", ] @@ -4798,6 +4974,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -4888,6 +5070,18 @@ dependencies = [ "indexmap 2.12.0", ] +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset 0.5.7", + "hashbrown 0.15.5", + "indexmap 2.12.0", + "serde", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -5312,6 +5506,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "radix_trie" version = "0.2.1" @@ -5577,6 +5777,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "resolvo" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670175f9a825ad2419bea0e14bfe74e5dcc0227ec7a652a655b1c11e2b911754" +dependencies = [ + "ahash", + "bitvec", + "elsa", + "event-listener", + "futures", + "indexmap 2.12.0", + "itertools 0.14.0", + "petgraph 0.8.3", + "tracing", +] + [[package]] name = "ring" version = "0.17.14" @@ -5644,7 +5861,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -5854,6 +6071,7 @@ version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ + "indexmap 2.12.0", "itoa", "memchr", "ryu", @@ -6546,6 +6764,12 @@ dependencies = [ "unicode-width 0.2.1", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.44" @@ -6697,6 +6921,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -7712,15 +7945,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" @@ -7969,6 +8193,15 @@ name = "wu-manber" version = "0.1.0" source = "git+https://github.com/tvlfyi/wu-manber.git#0d5b22bea136659f7de60b102a7030e0daaa503d" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "xattr" version = "1.6.1" diff --git a/Cargo.toml b/Cargo.toml index e187c9a..4c85c2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ version = "0.3.0" [profile.release] codegen-units = 1 lto = true -opt-level = 3 +opt-level = "z" strip = true [dependencies] @@ -32,7 +32,12 @@ tracing-appender.workspace = true tracing-indicatif.workspace = true tracing-subscriber.workspace = true -atom.path = "crates/atom" +atom.path = "crates/atom" +config.path = "crates/config" +nixec.path = "crates/nixec" + +json-digest = "0.0.16" +serde_json = "1.0.145" [workspace.dependencies] anyhow = "^1" @@ -83,6 +88,7 @@ prodash = { version = "30.0.1", features = [ ] } [patch.crates-io] +birdcage = { git = "https://github.com/nrdxp/birdcage", branch = "clone_fs" } tracing = { git = "https://github.com/nrdxp/tracing", branch = "hierarchical" } tracing-appender = { git = "https://github.com/nrdxp/tracing", branch = "hierarchical" } tracing-core = { git = "https://github.com/nrdxp/tracing", branch = "hierarchical" } diff --git a/adrs/0015-sat-resolution-implementation.md b/adrs/0015-sat-resolution-implementation.md new file mode 100644 index 0000000..f52d2da --- /dev/null +++ b/adrs/0015-sat-resolution-implementation.md @@ -0,0 +1,1533 @@ +# ADR-0015: SAT-Based Transitive Resolution Implementation Plan + +## Status + +Draft - Ready for Implementation + +## Abstract + +This ADR provides the detailed implementation plan for transitioning eka from direct-dependency-only resolution to full SAT-based transitive resolution using the resolvo crate. It builds upon ADR-0014's architectural decisions and specifies explicit algorithms, data structures, and integration points. + +--- + +## Part 1: Executive Summary + +### 1.1 Current State + +The current resolution system (`crates/atom/src/package/resolve/mod.rs`) only resolves direct dependencies: + +```rust +// Current: Only processes manifest's direct deps +fn synchronize_atoms(&mut self, manifest: &ValidManifest) -> Result<(), DocError> { + for (set_tag, set) in manifest.as_ref().deps().from() { + for (label, req) in set { + self.synchronize_atom(req.to_owned(), id.to_owned(), set_tag.to_owned())?; + } + } +} +``` + +### 1.2 Target State + +Full transitive resolution with lazy manifest fetching: +- SAT solver determines globally consistent version selection +- Manifests fetched only when SAT solver needs dependency information +- Complete transitive closure captured in lock file v2 +- Per-dependency `requires` field enables graph reconstruction + +### 1.3 Key Design Decisions + +1. **Use resolvo's CDCL SAT solver** - Production-tested, async-native, supports lazy clause generation +2. **Implement custom `AtomDependencyProvider`** - Maps atom semantics to resolvo's trait +3. **Leverage `hint_dependencies_available`** - Signal cached vs requires-fetch atoms +4. **Cheap ref queries for candidate discovery** - Use `gix::Url::get_refs()` (metadata only) +5. **Lazy manifest fetch for dependencies** - Only fetch when solver needs deps + +--- + +## Part 2: Data Structure Design + +### 2.1 Atom Identity Mapping + +**Problem**: Resolvo uses `NameId` for packages and `SolvableId` for versions. Atoms have two-level identity: `(Root, Label)`. + +**Solution**: Use the existing `AtomId` type directly as the package name: + +```rust +// AtomId already captures (Root, Label) - no need for a new type +// The existing implementation in crates/atom/src/id/mod.rs works perfectly: +// +// pub struct AtomId { +// root: R, +// label: Label, +// } +// +// It implements Clone + PartialEq + Eq + Hash via derives, +// satisfying resolvo's PackageName trait requirements. + +// For display in error messages: +impl std::fmt::Display for AtomId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}::{}", self.root, self.label) + } +} +``` + +The existing `AtomId` already has the right semantics: +- `Root` = genesis commit hash (repository identity) +- `Label` = atom name within that repository +- Implements `Clone + Eq + Hash` as required by resolvo's `PackageName` trait + +### 2.2 Version Set Implementation + +**Problem**: Resolvo's `VersionSet` trait requires `Clone + Eq + Hash`. Semver ranges satisfy this. + +**Solution**: Use `semver::VersionReq` directly as the version set: + +```rust +/// Wrapper for semver::VersionReq that implements resolvo's VersionSet trait +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct SemverVersionSet(pub semver::VersionReq); + +impl resolvo::utils::VersionSet for SemverVersionSet { + type V = semver::Version; +} + +impl SemverVersionSet { + pub fn matches(&self, version: &semver::Version) -> bool { + self.0.matches(version) + } +} +``` + +### 2.3 Solvable Record Type + +Each solvable (specific version of an atom) needs associated metadata: + +```rust +/// Metadata for a specific atom version in the resolver pool +#[derive(Clone, Debug)] +pub struct AtomSolvableRecord { + /// The concrete semantic version + pub version: semver::Version, + /// Git revision (commit hash) for this version + pub rev: metadata::GitDigest, + /// Whether we have the manifest cached (deps available without fetch) + pub manifest_cached: bool, + /// Machine-computed atom ID for verification + pub atom_id: id::AtomDigest, +} +``` + +### 2.4 Lock File v2 Format + +Extend `AtomDep` with transitive dependency information. Key changes from v1: +- **Remove `mirror` field** - nix reads from local store only (per ADR-0014) +- **`requires` uses `AtomDigest`** - concise reference by computed hash +- **Add `direct` flag** - distinguishes root's direct deps from transitives + +```rust +/// Represents a locked atom dependency with its own requirements +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] +#[serde(deny_unknown_fields)] +pub struct AtomDepV2 { + /// The unique identifier of the atom + label: Label, + /// The semantic version of the atom + version: Version, + /// Repository identity (root hash) + set: GitDigest, + /// Git revision (commit hash) for verification + rev: GitDigest, + /// Machine-computed cryptographic identity (BLAKE3 hash of AtomId) + id: AtomDigest, + /// NEW: Direct dependencies of this atom, referenced by their AtomDigest + /// Enables transitive closure reconstruction as a hypergraph + #[serde(default, skip_serializing_if = "Vec::is_empty")] + requires: Vec, + /// NEW: Whether this is a direct dependency of the root manifest + #[serde(default, skip_serializing_if = "crate::package::metadata::manifest::not")] + direct: bool, +} + +// Note: AtomRef removed - we use AtomDigest directly for conciseness +// The AtomDigest uniquely identifies an atom and can be looked up in the deps table + +/// Lock file with version field for migration +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct LockfileV2 { + /// Schema version - 2 for transitive resolution + pub version: u16, + /// Set definitions with mirrors (used during resolution/store population only) + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub sets: BTreeMap, + /// Composer configuration + pub compose: Using, + /// Full transitive closure of dependencies + #[serde(default, skip_serializing_if = "DepMap::is_empty")] + pub deps: DepMap, +} +``` + +**Lock file v2 as hypergraph**: The structure naturally encodes a directed hypergraph where: +- **Nodes**: `AtomDigest` values (each resolved atom) +- **Edges**: Each dep's `requires` field forms hyperedges from that atom to its dependencies + +This can be deserialized into `hyperdep::HyperGraph` for traversal operations like topological sorting, cycle detection, and impact analysis. + +--- + +## Part 3: AtomDependencyProvider Implementation + +### 3.1 Core State Structure + +```rust +/// Resolution context holding all state needed for dependency resolution +pub struct AtomResolver<'a, S: LocalStorage> { + /// The resolvo pool for interning - uses AtomId directly as package name + pool: Pool>, + + /// Mapping from AtomId to available versions discovered via ref queries + discovered_candidates: HashMap, Vec>, + + /// Cache of fetched manifests: (AtomId, Version) -> parsed dependencies + manifest_cache: HashMap<(AtomId, Version), Vec>, + + /// Set of solvables whose manifests are locally cached (cheap deps access) + locally_available: HashSet, + + /// Reference to resolved sets from manifest processing + resolved_sets: &'a ResolvedSets<'a, S>, + + /// Storage backend for git operations + storage: &'a S, + + /// Transports for remote operations (reusable connections) + transports: HashMap>, +} + +#[derive(Clone, Debug)] +struct DiscoveredVersion { + version: Version, + rev: GitDigest, + solvable_id: SolvableId, +} + +#[derive(Clone, Debug)] +struct AtomDependency { + /// Target atom identity (Root + Label) + target: AtomId, + /// Version requirement + version_req: VersionReq, +} +``` + +### 3.2 DependencyProvider Implementation + +```rust +impl<'a, S: LocalStorage> DependencyProvider for AtomResolver<'a, S> { + /// Filter candidates by checking if version satisfies the version set + async fn filter_candidates( + &self, + candidates: &[SolvableId], + version_set: VersionSetId, + inverse: bool, + ) -> Vec { + let vs = self.pool.resolve_version_set(version_set); + candidates + .iter() + .filter(|&&solvable_id| { + let solvable = self.pool.resolve_solvable(solvable_id); + let matches = vs.matches(&solvable.record.version); + if inverse { !matches } else { matches } + }) + .copied() + .collect() + } + + /// Get all candidate versions for a package + /// This uses CHEAP ref queries - no manifest fetching + async fn get_candidates(&self, name: NameId) -> Option { + let package_name = self.pool.resolve_package_name(name); + + // Check if we've already discovered candidates for this package + if let Some(versions) = self.discovered_candidates.get(package_name) { + let candidate_ids: Vec = versions + .iter() + .map(|v| v.solvable_id) + .collect(); + + // Build hint_dependencies_available bitmap + let available_bitmap = self.build_availability_bitmap(&candidate_ids); + + return Some(Candidates { + candidates: candidate_ids, + favored: None, + locked: None, + hint_dependencies_available: available_bitmap, + excluded: vec![], + }); + } + + // Need to discover candidates via cheap ref query + match self.discover_candidates_for_package(package_name).await { + Ok(candidates) => Some(candidates), + Err(e) => { + tracing::warn!( + package = %package_name, + error = %e, + "failed to discover candidates" + ); + None + } + } + } + + /// Sort candidates by version descending (latest first) + async fn sort_candidates( + &self, + _solver: &resolvo::SolverCache, + solvables: &mut [SolvableId], + ) { + solvables.sort_by(|&a, &b| { + let va = &self.pool.resolve_solvable(a).record.version; + let vb = &self.pool.resolve_solvable(b).record.version; + vb.cmp(va) // Descending order: latest first + }); + } + + /// Get dependencies for a specific solvable + /// This is where LAZY MANIFEST FETCHING happens + async fn get_dependencies(&self, solvable: SolvableId) -> Dependencies { + let solvable_data = self.pool.resolve_solvable(solvable); + let package_name = self.pool.resolve_package_name(solvable_data.name); + let version = &solvable_data.record.version; + + // Check manifest cache first + let cache_key = (package_name.clone(), version.clone()); + if let Some(deps) = self.manifest_cache.get(&cache_key) { + return self.convert_deps_to_resolvo(deps); + } + + // Need to fetch manifest - this is the expensive operation + match self.fetch_and_parse_manifest(package_name, version).await { + Ok(deps) => { + // Cache for future use + self.manifest_cache.insert(cache_key, deps.clone()); + self.convert_deps_to_resolvo(&deps) + } + Err(e) => { + let reason = self.pool.intern_string(format!( + "manifest fetch failed: {}", + e + )); + Dependencies::Unknown(reason) + } + } + } +} +``` + +### 3.3 Candidate Discovery (Cheap Ref Query) + +```rust +impl<'a, S: LocalStorage> AtomResolver<'a, S> { + /// Discover available versions for a package using cheap ref queries + /// This does NOT fetch manifests - only refs + async fn discover_candidates_for_package( + &mut self, + package: &AtomId, + ) -> Result { + // Construct ref query pattern: refs/eka/atoms/