Skip to main content

varve_core/
resolve.rs

1//! Resolution — pin + core → exactly one layer, or a loud failure.
2//!
3//! The load-bearing rule (REQ-PIN-001): a pinned layer resolves *exactly*, or
4//! the command fails with the corrective install command. There is no fallback
5//! layer, no PATH fall-through, no "close enough" — the error type has no
6//! variant for any of those, so the fallback cannot be written.
7//!
8//! And the constraint behind it (REQ-NOUPDATE-001): resolution is a pure
9//! function of (pin, store). Nothing here consults "latest", the network, or
10//! the environment; laying a newer layer down cannot change what an existing
11//! pin resolves to.
12
13use std::path::PathBuf;
14
15use crate::pin::Pin;
16use crate::store::{InstalledLayer, Store, StoreError};
17
18/// A successful resolution: the one layer this pin selects.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct Resolved {
21    pub layer: InstalledLayer,
22    /// The tools this pin exposes (the pin's `tools` subset, or every tool in
23    /// the layer), each with its resolved binary path. Exactly one entry per
24    /// NAME — that is what keeps one shim per name (REQ-REALM2-001 clause 4c).
25    pub tools: Vec<(String, PathBuf)>,
26    /// Every provider of every exposed name, including the ones a bare name
27    /// does NOT dispatch to (REQ-REALM2-001 clause 4b). Addressable as
28    /// `realm/tool`, so "compare our fork against upstream" keeps working
29    /// instead of the unselected binary disappearing.
30    pub qualified: Vec<(crate::compose::ToolProvider, PathBuf)>,
31    /// Runner contracts (REQ-RUNNER-001): tool → (runner tool, prefix args,
32    /// optional per-user-arg flag), from the signed manifest annotations.
33    pub runners: std::collections::BTreeMap<String, RunnerContract>,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct RunnerContract {
38    pub tool: String,
39    pub args: Vec<String>,
40    pub arg_prefix: Option<String>,
41}
42
43/// Resolution failures. Every variant carries what the user must do next.
44#[derive(Debug, thiserror::Error)]
45pub enum ResolveError {
46    #[error("layer {layer} is not installed — run `varve install` in this project to lay it down")]
47    NotInstalled { layer: String },
48    #[error(
49        "pin digest {pinned} is not installed — run `varve install` in this project to lay it down"
50    )]
51    DigestNotInstalled { pinned: String },
52    #[error(
53        "pin names layer {named} but pins digest {pinned}, which is layer {found} — the name is a label, the digest is the artifact; refusing to guess. Fix the pin."
54    )]
55    NameDigestMismatch {
56        named: String,
57        pinned: String,
58        found: String,
59    },
60    #[error(
61        "layer {layer} is installed more than once under different digests ({count} entries) and the pin carries no digest to disambiguate — add `digest = \"sha256:…\"` to the pin"
62    )]
63    Ambiguous { layer: String, count: usize },
64    #[error(
65        "layer {layer} is installed but incomplete: missing {missing:?} — run `varve install` to repair it; refusing to fall back to PATH"
66    )]
67    PartialLayer { layer: String, missing: Vec<String> },
68    #[error(
69        "this project's pin restricts `tools` to {missing:?}, which layer {layer} does not \
70         contain. It exposes: {available}. Re-installing cannot help — the layer is complete, \
71         the pin asks for something that was never in it. Fix the `tools` list in varve.toml, \
72         or drop it to expose everything the layer carries."
73    )]
74    PinNamesUnknownTool {
75        layer: String,
76        missing: Vec<String>,
77        available: String,
78    },
79    #[error(transparent)]
80    Store(#[from] StoreError),
81    #[error(
82        "layer {layer} is installed on channel '{installed}', but this project's pin selects \
83         '{pinned}' — refusing. A qualified line carries a support window and qualification \
84         evidence; a rolling one carries neither. Install the {pinned} layer, or change the \
85         pin deliberately."
86    )]
87    ChannelMismatch {
88        layer: String,
89        installed: String,
90        pinned: String,
91    },
92    #[error(transparent)]
93    Compose(#[from] crate::compose::ComposeError),
94    #[error(
95        "layer {layer} composes layer {missing}{realm}, which is not installed — \
96         `varve install` it, then retry"
97    )]
98    IncludeNotInstalled {
99        layer: String,
100        missing: String,
101        realm: String,
102    },
103}
104
105/// Resolve a pin against the local core. Pure: consults nothing but its
106/// arguments.
107pub fn resolve(pin: &Pin, store: &Store) -> Result<Resolved, ResolveError> {
108    let layer = match &pin.digest {
109        Some(digest) => {
110            let entry = store
111                .get(digest)?
112                .ok_or_else(|| ResolveError::DigestNotInstalled {
113                    pinned: digest.clone(),
114                })?;
115            if entry.layer != pin.layer {
116                return Err(ResolveError::NameDigestMismatch {
117                    named: pin.layer.to_string(),
118                    pinned: digest.clone(),
119                    found: entry.layer.to_string(),
120                });
121            }
122            entry
123        }
124        None => {
125            let matching: Vec<InstalledLayer> = store
126                .list()?
127                .into_iter()
128                .filter(|entry| entry.layer == pin.layer)
129                .collect();
130            match matching.len() {
131                0 => {
132                    return Err(ResolveError::NotInstalled {
133                        layer: pin.layer.to_string(),
134                    });
135                }
136                1 => matching.into_iter().next().expect("len checked"),
137                count => {
138                    return Err(ResolveError::Ambiguous {
139                        layer: pin.layer.to_string(),
140                        count,
141                    });
142                }
143            }
144        }
145    };
146
147    // The pin's channel is part of the pin. `install` refuses a mismatched
148    // fetch, but nothing re-checked an ALREADY-INSTALLED layer, so editing a
149    // pin from `rolling` to `qualified` left `which`, `verify` and `run`
150    // happily resolving the rolling layer — a silent fallback in the one
151    // distinction varve exists to make, and the opposite of what the docs
152    // promise ("a pin resolves exactly or the command fails").
153    if !layer.channel.is_empty() && layer.channel != pin.channel.as_str() {
154        return Err(ResolveError::ChannelMismatch {
155            layer: layer.layer.to_string(),
156            installed: layer.channel.clone(),
157            pinned: pin.channel.as_str().to_string(),
158        });
159    }
160
161    // Composition first (REQ-COMPOSE-001): a pin restricting `tools` may name a
162    // tool that lives in an INCLUDED layer, so the composed set has to be known
163    // before deciding what is missing. Resolving the root first reported
164    // `PartialLayer` for a perfectly resolvable composed tool.
165    let offers: Vec<Offer> = composition_offers(pin, &layer, store)?;
166
167    // What the pin exposes, and where it has CHOSEN a realm for a name
168    // (REQ-REALM2-001 clause 4a).
169    let (tool_names, chosen) = exposed_and_chosen(pin, &offers);
170
171    // The one decision: which provider a BARE name dispatches to. It is made
172    // over every provider the composition has, then narrowed to what the pin
173    // exposes — so a collision the pin never asked about cannot refuse a
174    // command that does not touch it.
175    let exposed: Vec<crate::compose::ToolProvider> = offers
176        .iter()
177        .filter(|o| tool_names.contains(&o.provider.tool))
178        .map(|o| o.provider.clone())
179        .collect();
180    let dispatch = crate::compose::select_tools(&exposed, &chosen)?;
181
182    let mut tools = Vec::new();
183    let mut missing = Vec::new();
184    for name in &tool_names {
185        match dispatch
186            .get(name)
187            .and_then(|p| offers.iter().find(|o| o.provider == *p))
188            .and_then(|o| o.path.clone())
189        {
190            Some(path) => tools.push((name.clone(), path)),
191            None => missing.push(name.clone()),
192        }
193    }
194    if !missing.is_empty() {
195        // A pin restricting `tools` to a name the layer never carried is NOT an
196        // incomplete install, and telling the user to re-install is a fix that
197        // cannot work: install succeeds, changes nothing, and the same error
198        // returns. A ten-persona audit graded this the one true dead end in the
199        // tool — the only error whose stated remedy provably fails.
200        if pin.tools.is_some() {
201            let mut available: Vec<String> = store
202                .manifest_tool_names(&layer)
203                .unwrap_or_default()
204                .into_iter()
205                .chain(offers.iter().map(|o| o.provider.tool.clone()))
206                .collect();
207            available.sort();
208            available.dedup();
209            let unknown: Vec<String> = missing
210                .iter()
211                .filter(|m| !available.contains(m))
212                .cloned()
213                .collect();
214            if !unknown.is_empty() {
215                return Err(ResolveError::PinNamesUnknownTool {
216                    layer: layer.layer.to_string(),
217                    missing: unknown,
218                    available: if available.is_empty() {
219                        "(nothing)".into()
220                    } else {
221                        available.join(", ")
222                    },
223                });
224            }
225        }
226        return Err(ResolveError::PartialLayer {
227            layer: layer.layer.to_string(),
228            missing,
229        });
230    }
231
232    // Clause 4b: every provider of an exposed name stays addressable, chosen
233    // or not. Losing the binary the pin did not pick would be a worse answer
234    // than the refusal this feature replaces.
235    let qualified: Vec<(crate::compose::ToolProvider, PathBuf)> = offers
236        .iter()
237        .filter(|o| tool_names.contains(&o.provider.tool))
238        .filter_map(|o| o.path.clone().map(|p| (o.provider.clone(), p)))
239        .collect();
240
241    // Runner contracts from the stored manifest's entry annotations —
242    // lenient read: legacy layers without them simply have none.
243    let mut runners = std::collections::BTreeMap::new();
244    if let Ok(bytes) = std::fs::read(layer.root.join("layer.json"))
245        && let Ok(json) = serde_json::from_slice::<serde_json::Value>(&bytes)
246        && let Some(entries) = json["manifests"].as_array()
247    {
248        for entry in entries {
249            let ann = &entry["annotations"];
250            if let (Some(tool), Some(runner)) = (
251                ann["eu.pulseengine.tool"].as_str(),
252                ann[crate::bazel::ANN_RUNNER].as_str(),
253            ) {
254                runners.insert(
255                    tool.to_string(),
256                    RunnerContract {
257                        tool: runner.to_string(),
258                        args: ann[crate::bazel::ANN_RUNNER_ARGS]
259                            .as_str()
260                            .map(|a| a.split_whitespace().map(str::to_string).collect())
261                            .unwrap_or_default(),
262                        arg_prefix: ann[crate::bazel::ANN_RUNNER_ARG_PREFIX]
263                            .as_str()
264                            .map(str::to_string),
265                    },
266                );
267            }
268        }
269    }
270    Ok(Resolved {
271        layer,
272        tools,
273        qualified,
274        runners,
275    })
276}
277
278/// The names a pin exposes, and the realm it CHOSE for each name it qualified
279/// (REQ-REALM2-001 clause 4a).
280///
281/// A pin with no `tools` exposes the whole composition and chooses nothing —
282/// which is why an unchosen collision still refuses. A pin with `tools`
283/// exposes exactly those names, in the order written, and every qualified
284/// entry records its realm.
285fn exposed_and_chosen(
286    pin: &Pin,
287    offers: &[Offer],
288) -> (Vec<String>, std::collections::BTreeMap<String, String>) {
289    let Some(subset) = &pin.tools else {
290        let mut names: Vec<String> = offers.iter().map(|o| o.provider.tool.clone()).collect();
291        names.sort();
292        names.dedup();
293        return (names, std::collections::BTreeMap::new());
294    };
295    let mut names = Vec::with_capacity(subset.len());
296    let mut chosen = std::collections::BTreeMap::new();
297    for selector in subset {
298        names.push(selector.name.clone());
299        if let Some(realm) = &selector.realm {
300            chosen.insert(selector.name.clone(), realm.clone());
301        }
302    }
303    (names, chosen)
304}
305
306/// One layer of a composition offering one dispatchable name, and where that
307/// name's bytes are — `None` when the manifest declares a tool whose file is
308/// not on disk, which is an incomplete install rather than a missing tool.
309#[derive(Debug, Clone)]
310struct Offer {
311    provider: crate::compose::ToolProvider,
312    path: Option<PathBuf>,
313}
314
315/// Every dispatchable name the pinned layer's COMPOSITION offers — the root's
316/// own and every included layer's — each labelled with the realm whose root
317/// vouches for it. Included layers must already be installed; fetching them
318/// transitively is deliberately out of scope for v0.23.0 (REQ-COMPOSE-001), so
319/// a missing one is an error naming it and its corrective install.
320///
321/// A name is offered if the SIGNED manifest declares it or a file of that name
322/// sits in the layer's `bin/` — but only if SOME layer of the composition has
323/// it on disk. All three parts earn their place:
324///
325/// * `bin/` alone would let a corrupt install decide dispatch: a root whose
326///   declared `wasm-tools` failed to land would silently hand the bare name to
327///   an included layer, which is exactly the install-state-dependent choice
328///   clause 4c forbids. Reading the signed manifest keeps that a refusal.
329/// * the manifest alone over-reports: a layer's manifest declares a tool for
330///   EVERY platform it ships, and a tool absent for this host (loom ships no
331///   aarch64-apple-darwin) would become an exposed name nothing can resolve.
332/// * so a name no layer has on disk is dropped: on this host it is not a
333///   collision and not a dispatchable name, it is simply not here.
334fn composition_offers(
335    pin: &Pin,
336    layer: &InstalledLayer,
337    store: &Store,
338) -> Result<Vec<Offer>, ResolveError> {
339    let root_realm = pin.realm.clone().unwrap_or_default();
340    let path = layer.root.join("layer.json");
341    let root_view = match std::fs::read(&path) {
342        // A manifest we cannot read is an ERROR, not an empty composition —
343        // an earlier version returned Ok(empty) here and silently resolved a
344        // composed layer to none of its included tools.
345        Ok(bytes) => crate::compose::view(&bytes)?,
346        // No stored manifest at all: a pre-composition layer laid down by an
347        // older varve. Nothing to compose, and nothing hidden.
348        Err(_) => crate::compose::LayerView::default(),
349    };
350    // Every declared include must already be installed. Fetching transitively
351    // is deliberately out of scope (REQ-COMPOSE-001), so name it and its fix.
352    for inc in &root_view.includes {
353        // Look across partitions: a cross-realm include lives under the
354        // INCLUDED realm's fingerprint (REQ-STORE-001).
355        if store.find_anywhere(&inc.digest)?.is_none() {
356            return Err(ResolveError::IncludeNotInstalled {
357                layer: layer.layer.to_string(),
358                missing: inc.layer.clone().unwrap_or_else(|| inc.digest.clone()),
359                realm: inc
360                    .realm
361                    .as_ref()
362                    .map(|r| format!(" from realm '{r}'"))
363                    .unwrap_or_default(),
364            });
365        }
366    }
367    let walked = crate::compose::walk(&layer.digest, &root_realm, &root_view, |digest| {
368        let (_, entry) = store.find_anywhere(digest).ok().flatten()?;
369        let bytes = std::fs::read(entry.root.join("layer.json")).ok()?;
370        crate::compose::view(&bytes).ok()
371    })?;
372
373    let mut out = Vec::new();
374    for step in &walked {
375        // The root is already in hand; an included layer is looked up wherever
376        // its realm partitioned it.
377        let (owner, entry) = if step.digest == layer.digest {
378            (store.clone(), layer.clone())
379        } else {
380            match store.find_anywhere(&step.digest)? {
381                Some(found) => found,
382                None => continue,
383            }
384        };
385        let mut names: Vec<String> = step.view.tools.clone();
386        if let Ok(rd) = std::fs::read_dir(entry.root.join("bin")) {
387            names.extend(
388                rd.filter_map(|e| e.ok())
389                    .filter(|e| e.path().is_file())
390                    .map(|e| e.file_name().to_string_lossy().into_owned()),
391            );
392        }
393        names.sort();
394        names.dedup();
395        for name in names {
396            out.push(Offer {
397                path: owner.tool_path(&entry, &name),
398                provider: crate::compose::ToolProvider {
399                    tool: name,
400                    realm: step.realm.clone(),
401                    layer: entry.layer.to_string(),
402                    digest: step.digest.clone(),
403                },
404            });
405        }
406    }
407    // Drop names no layer of this composition actually has on this host. A
408    // manifest declares a tool for every platform the layer ships, so without
409    // this an unrestricted pin would expose `loom` on a machine whose platform
410    // that release skipped and then refuse to resolve it.
411    let on_disk: std::collections::BTreeSet<String> = out
412        .iter()
413        .filter(|o| o.path.is_some())
414        .map(|o| o.provider.tool.clone())
415        .collect();
416    out.retain(|o| on_disk.contains(&o.provider.tool));
417    Ok(out)
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423    use crate::pin::Pin;
424    use crate::store::{Store, fixtures, manifest_digest};
425
426    fn pin(toml: &str) -> Pin {
427        Pin::parse(toml, "varve.toml").unwrap()
428    }
429
430    fn qualified_pin(layer: &str) -> Pin {
431        pin(&format!(
432            "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"{layer}\"\n"
433        ))
434    }
435
436    fn store() -> (tempfile::TempDir, Store) {
437        let tmp = tempfile::tempdir().unwrap();
438        let store = Store::at(tmp.path().join("varve-root"));
439        (tmp, store)
440    }
441
442    /// A manifest that composes another layer by digest.
443    fn manifest_composing(layer: &str, tools: &[&str], include: &str) -> Vec<u8> {
444        let mut entries: Vec<String> = tools
445            .iter()
446            .map(|t| {
447                format!(
448                    r#"{{"digest":"sha256:{t}","annotations":{{"eu.pulseengine.tool":"{t}"}}}}"#
449                )
450            })
451            .collect();
452        entries.push(format!(
453            r#"{{"digest":"{include}","annotations":{{"eu.pulseengine.varve.kind":"layer"}}}}"#
454        ));
455        format!(
456            r#"{{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","artifactType":"application/vnd.pulseengine.varve.layer.v1+json","annotations":{{"eu.pulseengine.varve.layer":"{layer}","eu.pulseengine.varve.channel":"qualified"}},"manifests":[{}]}}"#,
457            entries.join(",")
458        )
459        .into_bytes()
460    }
461
462    // rivet: verifies REQ-CHANNEL-001
463    #[test]
464    fn a_pin_selecting_qualified_refuses_an_installed_rolling_layer() {
465        // THE distinction varve exists to make. `install` refused a mismatched
466        // FETCH, but nothing re-checked an already-installed layer — so editing
467        // a pin from rolling to qualified left which/verify/run resolving the
468        // rolling layer and reporting success. A safety-critical consumer would
469        // have pinned `qualified` and silently received an unqualified
470        // toolchain, with verify saying OK.
471        let (_tmp, store) = store();
472        store
473            .lay_down(
474                &fixtures::manifest("2026.07.0", "rolling"),
475                &[("synth", b"s")],
476            )
477            .unwrap();
478        let err = resolve(&qualified_pin("2026.07.0"), &store).unwrap_err();
479        match err {
480            ResolveError::ChannelMismatch {
481                installed, pinned, ..
482            } => {
483                assert_eq!(installed, "rolling");
484                assert_eq!(pinned, "qualified");
485            }
486            other => panic!("expected ChannelMismatch, got {other}"),
487        }
488    }
489
490    // rivet: verifies REQ-CHANNEL-001
491    #[test]
492    fn a_matching_channel_still_resolves() {
493        // The guard must not break the ordinary case.
494        let (_tmp, store) = store();
495        store
496            .lay_down(
497                &fixtures::manifest("2026.07.0", "qualified"),
498                &[("synth", b"s")],
499            )
500            .unwrap();
501        assert!(resolve(&qualified_pin("2026.07.0"), &store).is_ok());
502    }
503
504    // rivet: verifies REQ-CHANNEL-001
505    #[test]
506    fn the_channel_refusal_names_both_channels_and_what_they_cost() {
507        // An independent review replaced the whole #[error(...)] with the text
508        // "channel mismatch" and the entire workspace suite stayed GREEN: the
509        // existing tests assert the struct's FIELDS and never render the
510        // message a user actually reads. The clause is that the error names
511        // both channels AND what each means, so the test renders it.
512        let (_tmp, store) = store();
513        store
514            .lay_down(
515                &fixtures::manifest("2026.07.0", "rolling"),
516                &[("synth", b"s")],
517            )
518            .unwrap();
519        let msg = resolve(&qualified_pin("2026.07.0"), &store)
520            .unwrap_err()
521            .to_string();
522        assert!(msg.contains("rolling"), "names what is installed: {msg}");
523        assert!(msg.contains("qualified"), "names what is pinned: {msg}");
524        // …and why the difference is the point, not a label mismatch.
525        assert!(
526            msg.contains("support window"),
527            "says what qualified carries: {msg}"
528        );
529        assert!(
530            msg.contains("qualification evidence"),
531            "says what rolling lacks: {msg}"
532        );
533    }
534
535    // rivet: verifies REQ-CHANNEL-001
536    #[test]
537    fn a_layer_predating_channel_annotations_is_not_refused() {
538        // Deleting `!layer.channel.is_empty() &&` from the guard left the whole
539        // suite green, because every fixture in the workspace writes a channel.
540        // Without the exemption, every layer deposited before channels existed
541        // becomes unresolvable — a silent break of installed toolchains.
542        let (_tmp, store) = store();
543        store
544            .lay_down(
545                &fixtures::manifest_without_channel("2026.07.0"),
546                &[("synth", b"s")],
547            )
548            .unwrap();
549        let resolved = resolve(&qualified_pin("2026.07.0"), &store);
550        assert!(
551            resolved.is_ok(),
552            "a layer that states no channel contradicts no pin: {:?}",
553            resolved.err()
554        );
555    }
556
557    // rivet: verifies REQ-COMPOSE-001
558    #[test]
559    fn resolve_returns_the_composed_layers_tools() {
560        // The unit-level guard on composition. Mutation testing kills with
561        // `--workspace --lib`, so the CLI integration tests cannot protect this
562        // — `compose_tools -> Ok(vec![])` survived until this existed, meaning
563        // nothing noticed a composition silently resolving to nothing.
564        let (_tmp, store) = store();
565        let up = store
566            .lay_down(
567                &fixtures::manifest("2026.08.0", "qualified"),
568                &[("wasm-tools", b"w")],
569            )
570            .unwrap();
571        store
572            .lay_down(
573                &manifest_composing("2026.07.0", &["rivet"], &up),
574                &[("rivet", b"r")],
575            )
576            .unwrap();
577        let resolved = resolve(&qualified_pin("2026.07.0"), &store).unwrap();
578        let names: Vec<&str> = resolved.tools.iter().map(|(n, _)| n.as_str()).collect();
579        assert!(names.contains(&"rivet"), "own tool missing: {names:?}");
580        assert!(
581            names.contains(&"wasm-tools"),
582            "composed tool missing — the composition resolved to nothing: {names:?}"
583        );
584    }
585
586    // rivet: verifies REQ-COMPOSE-001
587    #[test]
588    fn resolve_refuses_a_tool_exposed_by_both_layers() {
589        // Kills the `n == &name` -> `!=` mutant: with the comparison inverted,
590        // the duplicate would slip through instead of being refused.
591        let (_tmp, store) = store();
592        let up = store
593            .lay_down(
594                &fixtures::manifest("2026.08.0", "qualified"),
595                &[("wasm-tools", b"u")],
596            )
597            .unwrap();
598        store
599            .lay_down(
600                &manifest_composing("2026.07.0", &["wasm-tools"], &up),
601                &[("wasm-tools", b"r")],
602            )
603            .unwrap();
604        let err = resolve(&qualified_pin("2026.07.0"), &store).unwrap_err();
605        assert!(
606            matches!(err, ResolveError::Compose(_)),
607            "a tool in two layers must be refused, got {err}"
608        );
609    }
610
611    // rivet: verifies REQ-COMPOSE-001
612    #[test]
613    fn a_pin_restricting_tools_also_restricts_the_composition() {
614        // Kills the `!subset.contains(..)` -> `subset.contains(..)` mutant:
615        // inverted, the pin would admit exactly the tools it excluded.
616        let (_tmp, store) = store();
617        let up = store
618            .lay_down(
619                &fixtures::manifest("2026.08.0", "qualified"),
620                &[("wasm-tools", b"w"), ("wkg", b"k")],
621            )
622            .unwrap();
623        store
624            .lay_down(
625                &manifest_composing("2026.07.0", &["rivet"], &up),
626                &[("rivet", b"r")],
627            )
628            .unwrap();
629        let pinned = pin(
630            "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\ntools = [\"rivet\", \"wasm-tools\"]\n",
631        );
632        let resolved = resolve(&pinned, &store).unwrap();
633        let names: Vec<&str> = resolved.tools.iter().map(|(n, _)| n.as_str()).collect();
634        assert!(
635            names.contains(&"wasm-tools"),
636            "selected composed tool: {names:?}"
637        );
638        assert!(
639            !names.contains(&"wkg"),
640            "the pin did not select wkg, so the composition must not add it: {names:?}"
641        );
642    }
643
644    // rivet: verifies REQ-PIN-001
645    #[test]
646    fn resolves_the_pinned_layer_by_name() {
647        let (_tmp, store) = store();
648        store
649            .lay_down(
650                &fixtures::manifest("2026.07.0", "qualified"),
651                &[("synth", b"s"), ("rivet", b"r")],
652            )
653            .unwrap();
654        let resolved = resolve(&qualified_pin("2026.07.0"), &store).unwrap();
655        assert_eq!(resolved.layer.layer.to_string(), "2026.07.0");
656        let names: Vec<&str> = resolved.tools.iter().map(|(n, _)| n.as_str()).collect();
657        assert_eq!(names, ["rivet", "synth"], "all tools, stable order");
658    }
659
660    // rivet: verifies REQ-PIN-001
661    #[test]
662    fn missing_layer_fails_with_the_corrective_command() {
663        let (_tmp, store) = store();
664        let err = resolve(&qualified_pin("2026.07.0"), &store).unwrap_err();
665        assert!(matches!(&err, ResolveError::NotInstalled { layer } if layer == "2026.07.0"));
666        assert!(
667            err.to_string().contains("varve install"),
668            "error must carry the fix: {err}"
669        );
670    }
671
672    // rivet: verifies REQ-PIN-001
673    #[test]
674    fn partial_layer_is_an_error_not_a_fallback() {
675        // The layer's MANIFEST declares both tools; only one was laid down.
676        // That is a genuinely incomplete install, and re-installing is the
677        // right advice. The fixture must declare them, or this case cannot be
678        // told apart from the one below.
679        let (_tmp, store) = store();
680        store
681            .lay_down(
682                &crate::manifest::fixtures::manifest_with_tools(
683                    "2026.07.0",
684                    "qualified",
685                    1,
686                    "2026-07-01T00:00:00Z",
687                    &[("rivet", "sha256:aa"), ("synth", "sha256:bb")],
688                ),
689                &[("rivet", b"r")],
690            )
691            .unwrap();
692        let p = pin(
693            "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\ntools = [\"rivet\", \"synth\"]\n",
694        );
695        match resolve(&p, &store).unwrap_err() {
696            ResolveError::PartialLayer { missing, .. } => {
697                assert_eq!(missing, vec!["synth".to_string()]);
698            }
699            other => panic!("expected PartialLayer, got: {other}"),
700        }
701    }
702
703    // rivet: verifies REQ-PIN-001
704    #[test]
705    fn a_pin_naming_a_tool_the_layer_never_had_is_not_told_to_reinstall() {
706        // A ten-persona audit graded this the ONE true dead end in the tool:
707        // `tools = ["notathing"]` produced "run `varve install` to repair it",
708        // install succeeded and changed nothing, and the same error returned.
709        // A fix that provably cannot work is worse than no advice, because the
710        // user doubts their machine rather than their pin.
711        let (_tmp, store) = store();
712        store
713            .lay_down(
714                &crate::manifest::fixtures::manifest_with_tools(
715                    "2026.07.0",
716                    "qualified",
717                    1,
718                    "2026-07-01T00:00:00Z",
719                    &[("rivet", "sha256:aa")],
720                ),
721                &[("rivet", b"r")],
722            )
723            .unwrap();
724        let p = pin(
725            "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\ntools = [\"notathing\"]\n",
726        );
727        match resolve(&p, &store).unwrap_err() {
728            ResolveError::PinNamesUnknownTool {
729                missing, available, ..
730            } => {
731                assert_eq!(missing, vec!["notathing".to_string()]);
732                assert!(
733                    available.contains("rivet"),
734                    "names what IS there: {available}"
735                );
736            }
737            other => panic!("expected PinNamesUnknownTool, got: {other}"),
738        }
739    }
740
741    // rivet: verifies REQ-PIN-001
742    #[test]
743    fn pinned_digest_wins_and_a_mismatching_name_is_a_hard_failure() {
744        let (_tmp, store) = store();
745        let july = fixtures::manifest("2026.07.0", "qualified");
746        store.lay_down(&july, &[("synth", b"s")]).unwrap();
747        let d_july = manifest_digest(&july);
748
749        // Pin says layer 2026.08.0 but pins July's digest: refuse loudly.
750        let hex = d_july.strip_prefix("sha256:").unwrap();
751        let p = pin(&format!(
752            "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.08.0\"\ndigest = \"sha256:{hex}\"\n"
753        ));
754        let err = resolve(&p, &store).unwrap_err();
755        assert!(
756            matches!(&err, ResolveError::NameDigestMismatch { named, found, .. }
757                if named == "2026.08.0" && found == "2026.07.0"),
758            "got: {err}"
759        );
760    }
761
762    // rivet: verifies REQ-PIN-001
763    #[test]
764    fn matching_digest_pin_resolves() {
765        let (_tmp, store) = store();
766        let july = fixtures::manifest("2026.07.0", "qualified");
767        store.lay_down(&july, &[("synth", b"s")]).unwrap();
768        let hex = manifest_digest(&july)
769            .strip_prefix("sha256:")
770            .unwrap()
771            .to_string();
772        let p = pin(&format!(
773            "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\ndigest = \"sha256:{hex}\"\n"
774        ));
775        let resolved = resolve(&p, &store).unwrap();
776        assert_eq!(resolved.layer.layer.to_string(), "2026.07.0");
777    }
778
779    // rivet: verifies REQ-NOUPDATE-001
780    #[test]
781    fn a_newer_layer_in_the_core_cannot_change_what_a_pin_resolves_to() {
782        let (_tmp, store) = store();
783        store
784            .lay_down(
785                &fixtures::manifest("2026.07.0", "qualified"),
786                &[("synth", b"july")],
787            )
788            .unwrap();
789        let p = qualified_pin("2026.07.0");
790        let before = resolve(&p, &store).unwrap();
791
792        // A newer layer arrives. The pin must not move.
793        store
794            .lay_down(
795                &fixtures::manifest("2026.08.0", "qualified"),
796                &[("synth", b"august")],
797            )
798            .unwrap();
799        let after = resolve(&p, &store).unwrap();
800        assert_eq!(
801            before, after,
802            "resolution is a pure function of (pin, store entry)"
803        );
804        assert_eq!(after.layer.layer.to_string(), "2026.07.0");
805    }
806
807    // rivet: verifies REQ-NOUPDATE-001
808    #[test]
809    fn ambiguous_name_fails_closed_instead_of_choosing() {
810        let (_tmp, store) = store();
811        // Same layer name, two different manifests (e.g. differing counters):
812        // without a digest in the pin, refusing is the only honest answer.
813        let a = fixtures::manifest("2026.07.0", "qualified");
814        let mut b = a.clone();
815        b.extend_from_slice(b"\n");
816        store.lay_down(&a, &[]).unwrap();
817        store.lay_down(&b, &[]).unwrap();
818        let err = resolve(&qualified_pin("2026.07.0"), &store).unwrap_err();
819        assert!(
820            matches!(&err, ResolveError::Ambiguous { count: 2, .. }),
821            "got: {err}"
822        );
823    }
824
825    // rivet: verifies REQ-SCOPE-001
826    #[test]
827    fn resolution_and_listing_never_write_to_the_core() {
828        fn tree_snapshot(root: &std::path::Path) -> Vec<(String, Vec<u8>)> {
829            let mut out = Vec::new();
830            if !root.exists() {
831                return out;
832            }
833            let mut stack = vec![root.to_path_buf()];
834            while let Some(dir) = stack.pop() {
835                let mut entries: Vec<_> = std::fs::read_dir(&dir)
836                    .unwrap()
837                    .map(|e| e.unwrap().path())
838                    .collect();
839                entries.sort();
840                for path in entries {
841                    if path.is_dir() {
842                        stack.push(path);
843                    } else {
844                        out.push((path.display().to_string(), std::fs::read(&path).unwrap()));
845                    }
846                }
847            }
848            out.sort();
849            out
850        }
851
852        let (_tmp, store) = store();
853        store
854            .lay_down(
855                &fixtures::manifest("2026.07.0", "qualified"),
856                &[("synth", b"s")],
857            )
858            .unwrap();
859        let before = tree_snapshot(store.root());
860        let _ = resolve(&qualified_pin("2026.07.0"), &store).unwrap();
861        let _ = store.list().unwrap();
862        let _ = resolve(&qualified_pin("2026.09.0"), &store).unwrap_err();
863        let after = tree_snapshot(store.root());
864        assert_eq!(
865            before, after,
866            "select/verify/report must never mutate the core"
867        );
868    }
869
870    // rivet: verifies REQ-REALM2-001
871    #[test]
872    fn a_tool_the_manifest_declares_for_other_platforms_is_not_exposed_here() {
873        // A layer's manifest declares a tool for EVERY platform it ships. loom
874        // ships no aarch64-apple-darwin, so on that host the name is declared
875        // and no file lands. Exposing it would make an unrestricted pin refuse
876        // to resolve at all — caught by the two-realm system gate, where
877        // `varve verify` failed with `missing ["loom"]` on a layer that had
878        // deliberately omitted it.
879        let (_tmp, store) = store();
880        let manifest = format!(
881            r#"{{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","artifactType":"application/vnd.pulseengine.varve.layer.v1+json","annotations":{{"eu.pulseengine.varve.layer":"2026.07.0","eu.pulseengine.varve.channel":"qualified"}},"manifests":[
882{{"digest":"sha256:aa","annotations":{{"eu.pulseengine.tool":"rivet","eu.pulseengine.platform":"{here}"}}}},
883{{"digest":"sha256:bb","annotations":{{"eu.pulseengine.tool":"loom","eu.pulseengine.platform":"some-other-triple"}}}}]}}"#,
884            here = crate::platform::host_platform()
885        );
886        // Only `rivet` lands, exactly as the installer would place it.
887        store
888            .lay_down(manifest.as_bytes(), &[("rivet", b"r")])
889            .unwrap();
890        let resolved = resolve(&qualified_pin("2026.07.0"), &store).unwrap();
891        let names: Vec<&str> = resolved.tools.iter().map(|(n, _)| n.as_str()).collect();
892        assert_eq!(names, ["rivet"], "loom is declared, not laid down here");
893    }
894
895    // rivet: verifies REQ-REALM2-001
896    #[test]
897    fn a_declared_tool_whose_bytes_are_missing_still_collides_with_a_composed_one() {
898        // The other half of the same rule, and the reason it reads the SIGNED
899        // manifest at all. If the root's `wasm-tools` failed to land, deciding
900        // from `bin/` alone would hand the bare name to the included layer
901        // without a word — dispatch chosen by install state, which is exactly
902        // what clause 4c forbids. The composed layer HAS the name on disk, so
903        // the collision is real and must still be refused.
904        let (_tmp, store) = store();
905        let up = manifest_composing("2026.08.0", &["wasm-tools"], "sha256:none");
906        // Strip the include from the upstream layer: it is a leaf.
907        let up = String::from_utf8(up).unwrap().replace(
908            r#",{"digest":"sha256:none","annotations":{"eu.pulseengine.varve.kind":"layer"}}"#,
909            "",
910        );
911        let up_digest = store
912            .lay_down(up.as_bytes(), &[("wasm-tools", b"upstream")])
913            .unwrap();
914        // The root DECLARES wasm-tools and lays down only rivet.
915        let root = manifest_composing("2026.07.0", &["wasm-tools", "rivet"], &up_digest);
916        store.lay_down(&root, &[("rivet", b"r")]).unwrap();
917
918        let err = resolve(&qualified_pin("2026.07.0"), &store).unwrap_err();
919        let msg = err.to_string();
920        assert!(
921            msg.contains("provided by more than one layer"),
922            "a half-installed root must not silently yield the name: {msg}"
923        );
924    }
925}