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.
24    pub tools: Vec<(String, PathBuf)>,
25    /// Runner contracts (REQ-RUNNER-001): tool → (runner tool, prefix args,
26    /// optional per-user-arg flag), from the signed manifest annotations.
27    pub runners: std::collections::BTreeMap<String, RunnerContract>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct RunnerContract {
32    pub tool: String,
33    pub args: Vec<String>,
34    pub arg_prefix: Option<String>,
35}
36
37/// Resolution failures. Every variant carries what the user must do next.
38#[derive(Debug, thiserror::Error)]
39pub enum ResolveError {
40    #[error("layer {layer} is not installed — run `varve install` in this project to lay it down")]
41    NotInstalled { layer: String },
42    #[error(
43        "pin digest {pinned} is not installed — run `varve install` in this project to lay it down"
44    )]
45    DigestNotInstalled { pinned: String },
46    #[error(
47        "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."
48    )]
49    NameDigestMismatch {
50        named: String,
51        pinned: String,
52        found: String,
53    },
54    #[error(
55        "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"
56    )]
57    Ambiguous { layer: String, count: usize },
58    #[error(
59        "layer {layer} is installed but incomplete: missing {missing:?} — run `varve install` to repair it; refusing to fall back to PATH"
60    )]
61    PartialLayer { layer: String, missing: Vec<String> },
62    #[error(
63        "this project's pin restricts `tools` to {missing:?}, which layer {layer} does not \
64         contain. It exposes: {available}. Re-installing cannot help — the layer is complete, \
65         the pin asks for something that was never in it. Fix the `tools` list in varve.toml, \
66         or drop it to expose everything the layer carries."
67    )]
68    PinNamesUnknownTool {
69        layer: String,
70        missing: Vec<String>,
71        available: String,
72    },
73    #[error(transparent)]
74    Store(#[from] StoreError),
75    #[error(
76        "layer {layer} is installed on channel '{installed}', but this project's pin selects \
77         '{pinned}' — refusing. A qualified line carries a support window and qualification \
78         evidence; a rolling one carries neither. Install the {pinned} layer, or change the \
79         pin deliberately."
80    )]
81    ChannelMismatch {
82        layer: String,
83        installed: String,
84        pinned: String,
85    },
86    #[error(transparent)]
87    Compose(#[from] crate::compose::ComposeError),
88    #[error(
89        "layer {layer} composes layer {missing}{realm}, which is not installed — \
90         `varve install` it, then retry"
91    )]
92    IncludeNotInstalled {
93        layer: String,
94        missing: String,
95        realm: String,
96    },
97}
98
99/// Resolve a pin against the local core. Pure: consults nothing but its
100/// arguments.
101pub fn resolve(pin: &Pin, store: &Store) -> Result<Resolved, ResolveError> {
102    let layer = match &pin.digest {
103        Some(digest) => {
104            let entry = store
105                .get(digest)?
106                .ok_or_else(|| ResolveError::DigestNotInstalled {
107                    pinned: digest.clone(),
108                })?;
109            if entry.layer != pin.layer {
110                return Err(ResolveError::NameDigestMismatch {
111                    named: pin.layer.to_string(),
112                    pinned: digest.clone(),
113                    found: entry.layer.to_string(),
114                });
115            }
116            entry
117        }
118        None => {
119            let matching: Vec<InstalledLayer> = store
120                .list()?
121                .into_iter()
122                .filter(|entry| entry.layer == pin.layer)
123                .collect();
124            match matching.len() {
125                0 => {
126                    return Err(ResolveError::NotInstalled {
127                        layer: pin.layer.to_string(),
128                    });
129                }
130                1 => matching.into_iter().next().expect("len checked"),
131                count => {
132                    return Err(ResolveError::Ambiguous {
133                        layer: pin.layer.to_string(),
134                        count,
135                    });
136                }
137            }
138        }
139    };
140
141    // The pin's channel is part of the pin. `install` refuses a mismatched
142    // fetch, but nothing re-checked an ALREADY-INSTALLED layer, so editing a
143    // pin from `rolling` to `qualified` left `which`, `verify` and `run`
144    // happily resolving the rolling layer — a silent fallback in the one
145    // distinction varve exists to make, and the opposite of what the docs
146    // promise ("a pin resolves exactly or the command fails").
147    if !layer.channel.is_empty() && layer.channel != pin.channel.as_str() {
148        return Err(ResolveError::ChannelMismatch {
149            layer: layer.layer.to_string(),
150            installed: layer.channel.clone(),
151            pinned: pin.channel.as_str().to_string(),
152        });
153    }
154
155    // Composition first (REQ-COMPOSE-001): a pin restricting `tools` may name a
156    // tool that lives in an INCLUDED layer, so the composed set has to be known
157    // before deciding what is missing. Resolving the root first reported
158    // `PartialLayer` for a perfectly resolvable composed tool.
159    let composed: Vec<(String, std::path::PathBuf)> = compose_tools(&layer, store)?;
160
161    let tool_names: Vec<String> = match &pin.tools {
162        Some(subset) => subset.clone(),
163        None => {
164            let bin = layer.root.join("bin");
165            let mut names: Vec<String> = match std::fs::read_dir(&bin) {
166                Ok(entries) => entries
167                    .filter_map(|e| e.ok())
168                    .filter(|e| e.path().is_file())
169                    .map(|e| e.file_name().to_string_lossy().into_owned())
170                    .collect(),
171                Err(_) => Vec::new(),
172            };
173            // Unrestricted pins expose the whole composition.
174            names.extend(composed.iter().map(|(n, _)| n.clone()));
175            names.sort();
176            names.dedup();
177            names
178        }
179    };
180
181    let mut tools = Vec::new();
182    let mut missing = Vec::new();
183    for name in tool_names {
184        // The root layer wins only because a duplicate is refused outright
185        // below; there is never a silent shadowing choice.
186        let own = store.tool_path(&layer, &name);
187        let from_composition = composed.iter().find(|(n, _)| n == &name);
188        match (own, from_composition) {
189            (Some(_), Some(_)) => {
190                return Err(ResolveError::Compose(
191                    crate::compose::ComposeError::AmbiguousTool {
192                        tool: name,
193                        first: layer.digest.clone(),
194                        second: "an included layer".into(),
195                    },
196                ));
197            }
198            (Some(path), None) => tools.push((name, path)),
199            (None, Some((_, path))) => tools.push((name, path.clone())),
200            (None, None) => missing.push(name),
201        }
202    }
203    if !missing.is_empty() {
204        // A pin restricting `tools` to a name the layer never carried is NOT an
205        // incomplete install, and telling the user to re-install is a fix that
206        // cannot work: install succeeds, changes nothing, and the same error
207        // returns. A ten-persona audit graded this the one true dead end in the
208        // tool — the only error whose stated remedy provably fails.
209        if pin.tools.is_some() {
210            let mut available: Vec<String> = store
211                .manifest_tool_names(&layer)
212                .unwrap_or_default()
213                .into_iter()
214                .chain(composed.iter().map(|(n, _)| n.clone()))
215                .collect();
216            available.sort();
217            available.dedup();
218            let unknown: Vec<String> = missing
219                .iter()
220                .filter(|m| !available.contains(m))
221                .cloned()
222                .collect();
223            if !unknown.is_empty() {
224                return Err(ResolveError::PinNamesUnknownTool {
225                    layer: layer.layer.to_string(),
226                    missing: unknown,
227                    available: if available.is_empty() {
228                        "(nothing)".into()
229                    } else {
230                        available.join(", ")
231                    },
232                });
233            }
234        }
235        return Err(ResolveError::PartialLayer {
236            layer: layer.layer.to_string(),
237            missing,
238        });
239    }
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        runners,
274    })
275}
276
277/// Resolve the tools an installed layer's COMPOSITION exposes, excluding the
278/// layer's own. Included layers must already be installed; fetching them
279/// transitively is deliberately out of scope for v0.23.0 (REQ-COMPOSE-001), so
280/// a missing one is an error naming it and its corrective install.
281fn compose_tools(
282    layer: &InstalledLayer,
283    store: &Store,
284) -> Result<Vec<(String, std::path::PathBuf)>, ResolveError> {
285    let path = layer.root.join("layer.json");
286    let Ok(bytes) = std::fs::read(&path) else {
287        // No stored manifest at all: a pre-composition layer laid down by an
288        // older varve. Nothing to compose, and nothing hidden.
289        return Ok(Vec::new());
290    };
291    // A manifest we cannot read is an ERROR, not an empty composition — the
292    // earlier version returned Ok(empty) here and silently resolved a composed
293    // layer to none of its included tools.
294    let root_view = crate::compose::view(&bytes)?;
295    if root_view.includes.is_empty() {
296        return Ok(Vec::new());
297    }
298    // Every declared include must already be installed. Fetching transitively
299    // is deliberately out of scope (REQ-COMPOSE-001), so name it and its fix.
300    for inc in &root_view.includes {
301        // Look across partitions: a cross-realm include lives under the
302        // INCLUDED realm's fingerprint (REQ-STORE-001).
303        if store.find_anywhere(&inc.digest)?.is_none() {
304            return Err(ResolveError::IncludeNotInstalled {
305                layer: layer.layer.to_string(),
306                missing: inc.layer.clone().unwrap_or_else(|| inc.digest.clone()),
307                realm: inc
308                    .realm
309                    .as_ref()
310                    .map(|r| format!(" from realm '{r}'"))
311                    .unwrap_or_default(),
312            });
313        }
314    }
315    let walked = crate::compose::walk(&layer.digest, &root_view, |digest| {
316        let (_, entry) = store.find_anywhere(digest).ok().flatten()?;
317        let bytes = std::fs::read(entry.root.join("layer.json")).ok()?;
318        crate::compose::view(&bytes).ok()
319    })?;
320    // Refuse a name exposed by more than one layer, before resolving any path.
321    crate::compose::union_tools(&walked)?;
322
323    let mut out = Vec::new();
324    for (digest, _) in walked.iter().skip(1) {
325        let Some((owner, entry)) = store.find_anywhere(digest)? else {
326            continue;
327        };
328        let bin = entry.root.join("bin");
329        let Ok(rd) = std::fs::read_dir(&bin) else {
330            continue;
331        };
332        let mut names: Vec<String> = rd
333            .filter_map(|e| e.ok())
334            .filter(|e| e.path().is_file())
335            .map(|e| e.file_name().to_string_lossy().into_owned())
336            .collect();
337        names.sort();
338        for name in names {
339            if let Some(path) = owner.tool_path(&entry, &name) {
340                out.push((name, path));
341            }
342        }
343    }
344    Ok(out)
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350    use crate::pin::Pin;
351    use crate::store::{Store, fixtures, manifest_digest};
352
353    fn pin(toml: &str) -> Pin {
354        Pin::parse(toml, "varve.toml").unwrap()
355    }
356
357    fn qualified_pin(layer: &str) -> Pin {
358        pin(&format!(
359            "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"{layer}\"\n"
360        ))
361    }
362
363    fn store() -> (tempfile::TempDir, Store) {
364        let tmp = tempfile::tempdir().unwrap();
365        let store = Store::at(tmp.path().join("varve-root"));
366        (tmp, store)
367    }
368
369    /// A manifest that composes another layer by digest.
370    fn manifest_composing(layer: &str, tools: &[&str], include: &str) -> Vec<u8> {
371        let mut entries: Vec<String> = tools
372            .iter()
373            .map(|t| {
374                format!(
375                    r#"{{"digest":"sha256:{t}","annotations":{{"eu.pulseengine.tool":"{t}"}}}}"#
376                )
377            })
378            .collect();
379        entries.push(format!(
380            r#"{{"digest":"{include}","annotations":{{"eu.pulseengine.varve.kind":"layer"}}}}"#
381        ));
382        format!(
383            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":[{}]}}"#,
384            entries.join(",")
385        )
386        .into_bytes()
387    }
388
389    // rivet: verifies REQ-CHANNEL-001
390    #[test]
391    fn a_pin_selecting_qualified_refuses_an_installed_rolling_layer() {
392        // THE distinction varve exists to make. `install` refused a mismatched
393        // FETCH, but nothing re-checked an already-installed layer — so editing
394        // a pin from rolling to qualified left which/verify/run resolving the
395        // rolling layer and reporting success. A safety-critical consumer would
396        // have pinned `qualified` and silently received an unqualified
397        // toolchain, with verify saying OK.
398        let (_tmp, store) = store();
399        store
400            .lay_down(
401                &fixtures::manifest("2026.07.0", "rolling"),
402                &[("synth", b"s")],
403            )
404            .unwrap();
405        let err = resolve(&qualified_pin("2026.07.0"), &store).unwrap_err();
406        match err {
407            ResolveError::ChannelMismatch {
408                installed, pinned, ..
409            } => {
410                assert_eq!(installed, "rolling");
411                assert_eq!(pinned, "qualified");
412            }
413            other => panic!("expected ChannelMismatch, got {other}"),
414        }
415    }
416
417    // rivet: verifies REQ-CHANNEL-001
418    #[test]
419    fn a_matching_channel_still_resolves() {
420        // The guard must not break the ordinary case.
421        let (_tmp, store) = store();
422        store
423            .lay_down(
424                &fixtures::manifest("2026.07.0", "qualified"),
425                &[("synth", b"s")],
426            )
427            .unwrap();
428        assert!(resolve(&qualified_pin("2026.07.0"), &store).is_ok());
429    }
430
431    // rivet: verifies REQ-CHANNEL-001
432    #[test]
433    fn the_channel_refusal_names_both_channels_and_what_they_cost() {
434        // An independent review replaced the whole #[error(...)] with the text
435        // "channel mismatch" and the entire workspace suite stayed GREEN: the
436        // existing tests assert the struct's FIELDS and never render the
437        // message a user actually reads. The clause is that the error names
438        // both channels AND what each means, so the test renders it.
439        let (_tmp, store) = store();
440        store
441            .lay_down(
442                &fixtures::manifest("2026.07.0", "rolling"),
443                &[("synth", b"s")],
444            )
445            .unwrap();
446        let msg = resolve(&qualified_pin("2026.07.0"), &store)
447            .unwrap_err()
448            .to_string();
449        assert!(msg.contains("rolling"), "names what is installed: {msg}");
450        assert!(msg.contains("qualified"), "names what is pinned: {msg}");
451        // …and why the difference is the point, not a label mismatch.
452        assert!(
453            msg.contains("support window"),
454            "says what qualified carries: {msg}"
455        );
456        assert!(
457            msg.contains("qualification evidence"),
458            "says what rolling lacks: {msg}"
459        );
460    }
461
462    // rivet: verifies REQ-CHANNEL-001
463    #[test]
464    fn a_layer_predating_channel_annotations_is_not_refused() {
465        // Deleting `!layer.channel.is_empty() &&` from the guard left the whole
466        // suite green, because every fixture in the workspace writes a channel.
467        // Without the exemption, every layer deposited before channels existed
468        // becomes unresolvable — a silent break of installed toolchains.
469        let (_tmp, store) = store();
470        store
471            .lay_down(
472                &fixtures::manifest_without_channel("2026.07.0"),
473                &[("synth", b"s")],
474            )
475            .unwrap();
476        let resolved = resolve(&qualified_pin("2026.07.0"), &store);
477        assert!(
478            resolved.is_ok(),
479            "a layer that states no channel contradicts no pin: {:?}",
480            resolved.err()
481        );
482    }
483
484    // rivet: verifies REQ-COMPOSE-001
485    #[test]
486    fn resolve_returns_the_composed_layers_tools() {
487        // The unit-level guard on composition. Mutation testing kills with
488        // `--workspace --lib`, so the CLI integration tests cannot protect this
489        // — `compose_tools -> Ok(vec![])` survived until this existed, meaning
490        // nothing noticed a composition silently resolving to nothing.
491        let (_tmp, store) = store();
492        let up = store
493            .lay_down(
494                &fixtures::manifest("2026.08.0", "qualified"),
495                &[("wasm-tools", b"w")],
496            )
497            .unwrap();
498        store
499            .lay_down(
500                &manifest_composing("2026.07.0", &["rivet"], &up),
501                &[("rivet", b"r")],
502            )
503            .unwrap();
504        let resolved = resolve(&qualified_pin("2026.07.0"), &store).unwrap();
505        let names: Vec<&str> = resolved.tools.iter().map(|(n, _)| n.as_str()).collect();
506        assert!(names.contains(&"rivet"), "own tool missing: {names:?}");
507        assert!(
508            names.contains(&"wasm-tools"),
509            "composed tool missing — the composition resolved to nothing: {names:?}"
510        );
511    }
512
513    // rivet: verifies REQ-COMPOSE-001
514    #[test]
515    fn resolve_refuses_a_tool_exposed_by_both_layers() {
516        // Kills the `n == &name` -> `!=` mutant: with the comparison inverted,
517        // the duplicate would slip through instead of being refused.
518        let (_tmp, store) = store();
519        let up = store
520            .lay_down(
521                &fixtures::manifest("2026.08.0", "qualified"),
522                &[("wasm-tools", b"u")],
523            )
524            .unwrap();
525        store
526            .lay_down(
527                &manifest_composing("2026.07.0", &["wasm-tools"], &up),
528                &[("wasm-tools", b"r")],
529            )
530            .unwrap();
531        let err = resolve(&qualified_pin("2026.07.0"), &store).unwrap_err();
532        assert!(
533            matches!(err, ResolveError::Compose(_)),
534            "a tool in two layers must be refused, got {err}"
535        );
536    }
537
538    // rivet: verifies REQ-COMPOSE-001
539    #[test]
540    fn a_pin_restricting_tools_also_restricts_the_composition() {
541        // Kills the `!subset.contains(..)` -> `subset.contains(..)` mutant:
542        // inverted, the pin would admit exactly the tools it excluded.
543        let (_tmp, store) = store();
544        let up = store
545            .lay_down(
546                &fixtures::manifest("2026.08.0", "qualified"),
547                &[("wasm-tools", b"w"), ("wkg", b"k")],
548            )
549            .unwrap();
550        store
551            .lay_down(
552                &manifest_composing("2026.07.0", &["rivet"], &up),
553                &[("rivet", b"r")],
554            )
555            .unwrap();
556        let pinned = pin(
557            "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\ntools = [\"rivet\", \"wasm-tools\"]\n",
558        );
559        let resolved = resolve(&pinned, &store).unwrap();
560        let names: Vec<&str> = resolved.tools.iter().map(|(n, _)| n.as_str()).collect();
561        assert!(
562            names.contains(&"wasm-tools"),
563            "selected composed tool: {names:?}"
564        );
565        assert!(
566            !names.contains(&"wkg"),
567            "the pin did not select wkg, so the composition must not add it: {names:?}"
568        );
569    }
570
571    // rivet: verifies REQ-PIN-001
572    #[test]
573    fn resolves_the_pinned_layer_by_name() {
574        let (_tmp, store) = store();
575        store
576            .lay_down(
577                &fixtures::manifest("2026.07.0", "qualified"),
578                &[("synth", b"s"), ("rivet", b"r")],
579            )
580            .unwrap();
581        let resolved = resolve(&qualified_pin("2026.07.0"), &store).unwrap();
582        assert_eq!(resolved.layer.layer.to_string(), "2026.07.0");
583        let names: Vec<&str> = resolved.tools.iter().map(|(n, _)| n.as_str()).collect();
584        assert_eq!(names, ["rivet", "synth"], "all tools, stable order");
585    }
586
587    // rivet: verifies REQ-PIN-001
588    #[test]
589    fn missing_layer_fails_with_the_corrective_command() {
590        let (_tmp, store) = store();
591        let err = resolve(&qualified_pin("2026.07.0"), &store).unwrap_err();
592        assert!(matches!(&err, ResolveError::NotInstalled { layer } if layer == "2026.07.0"));
593        assert!(
594            err.to_string().contains("varve install"),
595            "error must carry the fix: {err}"
596        );
597    }
598
599    // rivet: verifies REQ-PIN-001
600    #[test]
601    fn partial_layer_is_an_error_not_a_fallback() {
602        // The layer's MANIFEST declares both tools; only one was laid down.
603        // That is a genuinely incomplete install, and re-installing is the
604        // right advice. The fixture must declare them, or this case cannot be
605        // told apart from the one below.
606        let (_tmp, store) = store();
607        store
608            .lay_down(
609                &crate::manifest::fixtures::manifest_with_tools(
610                    "2026.07.0",
611                    "qualified",
612                    1,
613                    "2026-07-01T00:00:00Z",
614                    &[("rivet", "sha256:aa"), ("synth", "sha256:bb")],
615                ),
616                &[("rivet", b"r")],
617            )
618            .unwrap();
619        let p = pin(
620            "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\ntools = [\"rivet\", \"synth\"]\n",
621        );
622        match resolve(&p, &store).unwrap_err() {
623            ResolveError::PartialLayer { missing, .. } => {
624                assert_eq!(missing, vec!["synth".to_string()]);
625            }
626            other => panic!("expected PartialLayer, got: {other}"),
627        }
628    }
629
630    // rivet: verifies REQ-PIN-001
631    #[test]
632    fn a_pin_naming_a_tool_the_layer_never_had_is_not_told_to_reinstall() {
633        // A ten-persona audit graded this the ONE true dead end in the tool:
634        // `tools = ["notathing"]` produced "run `varve install` to repair it",
635        // install succeeded and changed nothing, and the same error returned.
636        // A fix that provably cannot work is worse than no advice, because the
637        // user doubts their machine rather than their pin.
638        let (_tmp, store) = store();
639        store
640            .lay_down(
641                &crate::manifest::fixtures::manifest_with_tools(
642                    "2026.07.0",
643                    "qualified",
644                    1,
645                    "2026-07-01T00:00:00Z",
646                    &[("rivet", "sha256:aa")],
647                ),
648                &[("rivet", b"r")],
649            )
650            .unwrap();
651        let p = pin(
652            "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\ntools = [\"notathing\"]\n",
653        );
654        match resolve(&p, &store).unwrap_err() {
655            ResolveError::PinNamesUnknownTool {
656                missing, available, ..
657            } => {
658                assert_eq!(missing, vec!["notathing".to_string()]);
659                assert!(
660                    available.contains("rivet"),
661                    "names what IS there: {available}"
662                );
663            }
664            other => panic!("expected PinNamesUnknownTool, got: {other}"),
665        }
666    }
667
668    // rivet: verifies REQ-PIN-001
669    #[test]
670    fn pinned_digest_wins_and_a_mismatching_name_is_a_hard_failure() {
671        let (_tmp, store) = store();
672        let july = fixtures::manifest("2026.07.0", "qualified");
673        store.lay_down(&july, &[("synth", b"s")]).unwrap();
674        let d_july = manifest_digest(&july);
675
676        // Pin says layer 2026.08.0 but pins July's digest: refuse loudly.
677        let hex = d_july.strip_prefix("sha256:").unwrap();
678        let p = pin(&format!(
679            "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.08.0\"\ndigest = \"sha256:{hex}\"\n"
680        ));
681        let err = resolve(&p, &store).unwrap_err();
682        assert!(
683            matches!(&err, ResolveError::NameDigestMismatch { named, found, .. }
684                if named == "2026.08.0" && found == "2026.07.0"),
685            "got: {err}"
686        );
687    }
688
689    // rivet: verifies REQ-PIN-001
690    #[test]
691    fn matching_digest_pin_resolves() {
692        let (_tmp, store) = store();
693        let july = fixtures::manifest("2026.07.0", "qualified");
694        store.lay_down(&july, &[("synth", b"s")]).unwrap();
695        let hex = manifest_digest(&july)
696            .strip_prefix("sha256:")
697            .unwrap()
698            .to_string();
699        let p = pin(&format!(
700            "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\ndigest = \"sha256:{hex}\"\n"
701        ));
702        let resolved = resolve(&p, &store).unwrap();
703        assert_eq!(resolved.layer.layer.to_string(), "2026.07.0");
704    }
705
706    // rivet: verifies REQ-NOUPDATE-001
707    #[test]
708    fn a_newer_layer_in_the_core_cannot_change_what_a_pin_resolves_to() {
709        let (_tmp, store) = store();
710        store
711            .lay_down(
712                &fixtures::manifest("2026.07.0", "qualified"),
713                &[("synth", b"july")],
714            )
715            .unwrap();
716        let p = qualified_pin("2026.07.0");
717        let before = resolve(&p, &store).unwrap();
718
719        // A newer layer arrives. The pin must not move.
720        store
721            .lay_down(
722                &fixtures::manifest("2026.08.0", "qualified"),
723                &[("synth", b"august")],
724            )
725            .unwrap();
726        let after = resolve(&p, &store).unwrap();
727        assert_eq!(
728            before, after,
729            "resolution is a pure function of (pin, store entry)"
730        );
731        assert_eq!(after.layer.layer.to_string(), "2026.07.0");
732    }
733
734    // rivet: verifies REQ-NOUPDATE-001
735    #[test]
736    fn ambiguous_name_fails_closed_instead_of_choosing() {
737        let (_tmp, store) = store();
738        // Same layer name, two different manifests (e.g. differing counters):
739        // without a digest in the pin, refusing is the only honest answer.
740        let a = fixtures::manifest("2026.07.0", "qualified");
741        let mut b = a.clone();
742        b.extend_from_slice(b"\n");
743        store.lay_down(&a, &[]).unwrap();
744        store.lay_down(&b, &[]).unwrap();
745        let err = resolve(&qualified_pin("2026.07.0"), &store).unwrap_err();
746        assert!(
747            matches!(&err, ResolveError::Ambiguous { count: 2, .. }),
748            "got: {err}"
749        );
750    }
751
752    // rivet: verifies REQ-SCOPE-001
753    #[test]
754    fn resolution_and_listing_never_write_to_the_core() {
755        fn tree_snapshot(root: &std::path::Path) -> Vec<(String, Vec<u8>)> {
756            let mut out = Vec::new();
757            if !root.exists() {
758                return out;
759            }
760            let mut stack = vec![root.to_path_buf()];
761            while let Some(dir) = stack.pop() {
762                let mut entries: Vec<_> = std::fs::read_dir(&dir)
763                    .unwrap()
764                    .map(|e| e.unwrap().path())
765                    .collect();
766                entries.sort();
767                for path in entries {
768                    if path.is_dir() {
769                        stack.push(path);
770                    } else {
771                        out.push((path.display().to_string(), std::fs::read(&path).unwrap()));
772                    }
773                }
774            }
775            out.sort();
776            out
777        }
778
779        let (_tmp, store) = store();
780        store
781            .lay_down(
782                &fixtures::manifest("2026.07.0", "qualified"),
783                &[("synth", b"s")],
784            )
785            .unwrap();
786        let before = tree_snapshot(store.root());
787        let _ = resolve(&qualified_pin("2026.07.0"), &store).unwrap();
788        let _ = store.list().unwrap();
789        let _ = resolve(&qualified_pin("2026.09.0"), &store).unwrap_err();
790        let after = tree_snapshot(store.root());
791        assert_eq!(
792            before, after,
793            "select/verify/report must never mutate the core"
794        );
795    }
796}