Skip to main content

caixa_resolver/
resolve.rs

1//! Top-level resolver — turn a [`Caixa`] into a [`Lacre`] with BLAKE3
2//! fechamento hashes over the full transitive closure.
3
4use std::collections::{BTreeMap, HashSet, VecDeque};
5use std::path::{Path, PathBuf};
6
7use caixa_core::{Caixa, Dep, DepSource};
8use caixa_lacre::{Lacre, LacreEntry, closure_hash, hash_bytes};
9use thiserror::Error;
10
11use crate::cache::CacheDir;
12use crate::config::ResolverConfig;
13use crate::git::{self, GitError};
14use crate::url::expand_shorthand;
15
16#[derive(Debug, Error)]
17pub enum ResolveError {
18    #[error("io: {0}")]
19    Io(#[from] std::io::Error),
20    #[error("git: {0}")]
21    Git(#[from] GitError),
22    #[error("lisp: {0}")]
23    Lisp(#[from] tatara_lisp::LispError),
24    /// A resolved dep's `caixa.lisp` did not read as a package manifest.
25    ///
26    /// Distinct from [`Self::Lisp`] because the interesting case is
27    /// `LeituraError::DialetoEstrangeiro`: a dep whose manifest is a
28    /// repo-surface declaration rather than a package manifest is not a
29    /// syntax error in that dep, it is the wrong KIND of file, and a resolver
30    /// that flattened both into "lisp: …" would send the author hunting for a
31    /// typo that is not there.
32    #[error("manifest: {0}")]
33    Manifesto(#[from] caixa_core::LeituraError),
34    #[error("dep '{nome}' expected a pin (:tag or :rev); got neither")]
35    MissingPin { nome: String },
36    #[error("dep '{nome}' path source {path} does not exist")]
37    MissingPath { nome: String, path: PathBuf },
38    #[error("cyclic dependency detected involving '{0}'")]
39    Cycle(String),
40}
41
42impl ResolveError {
43    /// Substrate primitive constructor for the per-`Dep`
44    /// `:fonte (:tipo path :caminho …)` on-disk-absence refusal
45    /// diagnostic. Folds the pre-lift open-coded four-line
46    /// `Err(ResolveError::MissingPath { nome: dep.nome().to_string(),
47    /// path })` two-slot struct-literal inside [`fetch_path`] onto one
48    /// dispatch. Peer with the sibling one-slot `MissingPin` `{ nome:
49    /// String }` variant on the same envelope, which the paired
50    /// [`Self::missing_pin`] ctor now folds onto its own substrate
51    /// primitive on [`fetch_git`]'s `.ok_or_else` closure-form emission
52    /// arm. Every future consumer that surfaces the same
53    /// on-disk-absence diagnostic outside `fetch_path` — a deferred
54    /// `feira lock --path`-only pre-flight probe, a per-lacre overlay
55    /// resolver rejecting a cluster-local `:caminho` overlay that
56    /// vanished between resolve passes, the M4 `mesh.pleme.io/v1alpha1/
57    /// Caixa` CR admission webhook re-checking a per-`:fonte`-patched
58    /// candidate before the closure walker re-fires — reaches the
59    /// variant through one call rather than re-inlining the two-slot
60    /// struct-literal in lockstep with [`fetch_path`]'s wire-up.
61    ///
62    /// The `nome: &str` parameter accepts `&str` literals and `&String`
63    /// via Deref coercion so the sole in-crate wire-up threads
64    /// `dep.nome()` (a `&str` accessor via [`Dep::nome`]) through the
65    /// ctor without a pre-conversion; the `path: impl Into<PathBuf>`
66    /// parameter takes the observed on-disk-absent [`PathBuf`] built
67    /// at the caller from `PathBuf::from(caminho)` and forwards it
68    /// through the [`From<PathBuf>`] identity into the variant slot
69    /// without an extra allocation.
70    #[must_use]
71    pub fn missing_path(nome: &str, path: impl Into<PathBuf>) -> ResolveError {
72        ResolveError::MissingPath {
73            nome: nome.to_string(),
74            path: path.into(),
75        }
76    }
77
78    /// Substrate primitive constructor for the per-`Dep`
79    /// `:fonte (:tipo git …)`-arm sole-set-pin absence refusal
80    /// diagnostic. Folds the pre-lift open-coded three-line
81    /// `ResolveError::MissingPin { nome: dep.nome().to_string() }`
82    /// one-slot struct-literal inside [`fetch_git`]'s `.ok_or_else`
83    /// closure-form emission arm onto one dispatch. Peer with the
84    /// sibling two-slot [`Self::missing_path`] ctor on the same
85    /// envelope's on-disk-absence refusal axis — this ctor closes the
86    /// paired closure-form emission axis on the `:fonte (:tipo git
87    /// …)`-arm's pin-precedence projection ([`DepSource::sole_pin`])
88    /// through which every author-omitted-and-default-host-shorthand-
89    /// expanded Git source (`expand_fonte(dep, default_host)` on a
90    /// `Dep::simple(...)` bare dep) reaches [`fetch_git`] with all
91    /// three `tag`/`rev`/`branch` pins `None`. Every future consumer
92    /// that surfaces the same sole-set-pin absence diagnostic outside
93    /// [`fetch_git`] — a deferred `feira lock --dry-run` pre-flight
94    /// per-`:deps` pin-presence probe that refuses before the
95    /// closure walker fires a single `git clone`, a per-lacre overlay
96    /// resolver rejecting a cluster-local `:fonte` overlay whose pin
97    /// axis was dropped between resolve passes, the M4
98    /// `mesh.pleme.io/v1alpha1/Caixa` CR admission webhook re-checking
99    /// a per-`:fonte`-patched candidate before the closure walker
100    /// re-fires — reaches the variant through one call rather than
101    /// re-inlining the one-slot struct-literal in lockstep with
102    /// [`fetch_git`]'s closure-form emission arm.
103    ///
104    /// The `nome: &str` parameter accepts `&str` literals and `&String`
105    /// via Deref coercion so the sole in-crate wire-up threads
106    /// `dep.nome()` (a `&str` accessor via [`Dep::nome`]) through the
107    /// ctor without a pre-conversion, byte-parallel to the sibling
108    /// [`Self::missing_path`] ctor's `nome` parameter.
109    #[must_use]
110    pub fn missing_pin(nome: &str) -> ResolveError {
111        ResolveError::MissingPin {
112            nome: nome.to_string(),
113        }
114    }
115}
116
117/// Resolve a root caixa's deps into a canonical lacre, offline if the cache
118/// is warm, otherwise cloning/fetching from git.
119pub fn resolve_lacre(
120    root: &Caixa,
121    cfg: &ResolverConfig,
122    cache: &CacheDir,
123) -> Result<Lacre, ResolveError> {
124    // Direct deps from the root caixa. Read both the runtime `:deps`
125    // and dev-only `:deps-dev` lists through the typed
126    // [`Caixa::deps`] / [`Caixa::deps_dev`] `&[Dep]`-return slice
127    // accessors so every projection of the outer-`Caixa`
128    // dependency-slot axes in the top-level closure resolver's
129    // queue-initialization surface routes through one typed
130    // dispatch — any future accessor extension (a per-scope alias
131    // table, per-cluster canary-version overlay, per-Aplicacao
132    // dev-closure-audit projection the M4 CR materializer's
133    // per-CR reconcile pass consumes) reaches both walks by
134    // construction. Peer of the sibling per-`:deps` /
135    // `:deps-dev` accessor family the ad34b4e / f7fd81e lifts
136    // opened on the outer-`Caixa` slot, extended here onto the
137    // caixa-resolver top-level closure resolver's outermost
138    // queue-seeding walks.
139    //
140    // The paired parent-`:nome` label carried on the queue's `from`
141    // arm — the byte-string the cycle-diagnostic re-labeler's
142    // `ResolveError::Cycle(from.clone())` map_err arm surfaces on any
143    // future extension that promotes the closure walker's per-target
144    // cycle-collapse into a first-class refusal — routes through the
145    // typed [`caixa_core::Caixa::nome`] `&str`-return accessor rather
146    // than the raw `root.nome.clone()` `String`-carry read. Byte-equal
147    // today (`Caixa::nome` is `&self.nome`; `.to_string()` on the
148    // borrowed `&str` allocates exactly one `String` byte-equal to the
149    // prior raw `.nome.clone()` read); the queue's per-transitive-target
150    // sibling push at line 77 already routes through the peer
151    // [`caixa_core::Dep::nome`] accessor (`dep.nome().to_string()`), so
152    // this converge closes the last unlifted parent-`:nome` field-read
153    // in the closure walker's queue-carried `from` axis. Peer with every
154    // prior per-`Caixa` universal-axis converge on the outer-`Caixa`
155    // `:nome` scalar (caixa-helm 22461ef / a7420bd, caixa-flux 4a363bf /
156    // 162e2e2, caixa-mesh 54bf2f3 / 980c059, caixa-crd 61d3429,
157    // caixa-tatara e73b19f, caixa-feira 5131203 / 4b05240 / 3219a42 /
158    // 5bc5178) and sibling to 5ce1b94's `Caixa::deps` / `Caixa::deps_dev`
159    // converge in this same crate on the paired outer-`Caixa`
160    // dependency-slot axis.
161    let mut queue: VecDeque<(Dep, String)> = VecDeque::new();
162    let mut seen: HashSet<String> = HashSet::new();
163    for dep in root.deps() {
164        queue.push_back((dep.clone(), root.nome().to_string()));
165    }
166    if cfg.include_dev {
167        for dep in root.deps_dev() {
168            queue.push_back((dep.clone(), root.nome().to_string()));
169        }
170    }
171
172    // Resolved entries keyed by nome, preserving deterministic deps_diretas.
173    let mut resolved: BTreeMap<String, ResolvedDep> = BTreeMap::new();
174
175    while let Some((dep, from)) = queue.pop_front() {
176        if !seen.insert(dep.nome().to_string()) {
177            continue;
178        }
179        let fetched = fetch_dep(&dep, cfg, cache).map_err(|e| match e {
180            ResolveError::Cycle(_) => ResolveError::Cycle(from.clone()),
181            other => other,
182        })?;
183        for t in &fetched.child_deps {
184            queue.push_back((t.clone(), dep.nome().to_string()));
185        }
186        resolved.insert(
187            dep.nome().to_string(),
188            ResolvedDep {
189                dep,
190                child_deps: fetched.child_deps,
191                resolved_fonte: fetched.resolved_fonte,
192                concrete_versao: fetched.concrete_versao,
193                conteudo: fetched.conteudo,
194            },
195        );
196    }
197
198    // Compute closure hashes in reverse-topological order.
199    let mut fechamento: BTreeMap<String, String> = BTreeMap::new();
200    // Simple fixpoint: re-run until all are hashable (acyclic → terminates).
201    let names: Vec<_> = resolved.keys().cloned().collect();
202    for _ in 0..names.len() {
203        let mut all_done = true;
204        for name in &names {
205            if fechamento.contains_key(name) {
206                continue;
207            }
208            let r = &resolved[name];
209            let child_closures: Option<Vec<String>> = r
210                .child_deps
211                .iter()
212                .map(|c| fechamento.get(c.nome()).cloned())
213                .collect();
214            if let Some(closures) = child_closures {
215                fechamento.insert(name.clone(), closure_hash(&r.conteudo, &closures));
216            } else {
217                all_done = false;
218            }
219        }
220        if all_done {
221            break;
222        }
223    }
224
225    // Build entries in sorted-name order.
226    let entries: Vec<LacreEntry> = resolved
227        .values()
228        .map(|r| LacreEntry {
229            nome: r.dep.nome().to_string(),
230            versao: r.concrete_versao.clone(),
231            fonte: r.resolved_fonte.clone(),
232            conteudo: r.conteudo.clone(),
233            fechamento: fechamento
234                .get(r.dep.nome())
235                .cloned()
236                .unwrap_or_else(|| hash_bytes(b"unresolved")),
237            deps_diretas: r.child_deps.iter().map(|c| c.nome().to_string()).collect(),
238        })
239        .collect();
240
241    Ok(Lacre::from_entries(entries))
242}
243
244struct ResolvedDep {
245    dep: Dep,
246    child_deps: Vec<Dep>,
247    resolved_fonte: DepSource,
248    concrete_versao: String,
249    conteudo: String,
250}
251
252struct FetchedDep {
253    child_deps: Vec<Dep>,
254    resolved_fonte: DepSource,
255    concrete_versao: String,
256    conteudo: String,
257}
258
259fn fetch_dep(
260    dep: &Dep,
261    cfg: &ResolverConfig,
262    cache: &CacheDir,
263) -> Result<FetchedDep, ResolveError> {
264    // Expand :fonte — None → default host shorthand.
265    //
266    // Route the per-`Dep` `:fonte` presence-projection through the
267    // lifted [`expand_fonte`] helper rather than an inline
268    // `dep.fonte.clone().unwrap_or_else(...)` cascade. The helper reads
269    // through the typed [`caixa_core::Dep::fonte`] `Option<&DepSource>`
270    // accessor and materializes the `github:<org>/<nome>`-shaped
271    // `default_host`-derived fallback for the author-omitted arm,
272    // keeping the closure-walker's two per-`Dep` `:fonte`-expansion
273    // consumers (this crate's [`fetch_dep`], caixa-feira/src/cmd/lock.rs's
274    // `resolve_stub` — 33e5d9a converged the peer site onto the same
275    // accessor) both routed through the substrate-primitive typed
276    // dispatch rather than each open-coding its own `.clone()` cascade.
277    let fonte = expand_fonte(dep, &cfg.default_host);
278
279    match &fonte {
280        DepSource::Path { caminho } => fetch_path(dep, caminho),
281        // Route the per-`DepSource::Git`-arm sole-set-pin projection
282        // through the lifted [`DepSource::sole_pin`] substrate
283        // accessor rather than passing broken-out `tag`/`rev`/`branch`
284        // `Option<&str>` triples down for `fetch_git` to re-inline the
285        // `rev.or(tag).or(branch)` cascade on — the two pre-lift
286        // consumers of the sole-set-pin projection (this crate's
287        // `fetch_git` `git checkout` target, caixa-crd's
288        // `dep_into_ref` `CaixaSource.git_ref` fill) now key off
289        // exactly one typed dispatch on the substrate primitive, so
290        // any future rebrand on the precedence axis (a `:commit` pin
291        // peer once signed-commit-verification lands, a `:ref` pin the
292        // M4 operator resolves per-cluster, a promotion of the plain
293        // `Option<String>` pins to a typed `GitPin` newtype) migrates
294        // as a single caixa-core edit rather than a coordinated
295        // rewrite of both consumer sites.
296        DepSource::Git { repo, .. } => fetch_git(dep, repo, fonte.sole_pin(), cache, fonte.clone()),
297    }
298}
299
300fn fetch_path(dep: &Dep, caminho: &str) -> Result<FetchedDep, ResolveError> {
301    let path = PathBuf::from(caminho);
302    if !path.exists() {
303        return Err(ResolveError::missing_path(dep.nome(), path));
304    }
305    let manifest = std::fs::read_to_string(path.join("caixa.lisp"))?;
306    let target = Caixa::from_lisp(&manifest)?;
307    Ok(FetchedDep {
308        // Route the fetched child's `:deps` `Vec<Dep>`-carry projection
309        // through the typed [`Caixa::deps`] `&[Dep]`-return slice
310        // accessor so the transitive walk keys off the same typed
311        // dispatch the outer queue-seeding read at [`resolve_lacre`]
312        // already routes through — a `.to_vec()` on the accessor's
313        // borrowed slice allocates exactly one `Vec<Dep>` per fetched
314        // target, byte-equal in element order to the prior raw
315        // `target.deps.clone()` read.
316        child_deps: target.deps().to_vec(),
317        resolved_fonte: DepSource::Path {
318            caminho: caminho.to_string(),
319        },
320        // Route the fetched child's `:versao` `String`-carry projection
321        // through the typed [`Caixa::versao`] `&str`-return accessor —
322        // sibling to the paired `target.deps().to_vec()` accessor-route
323        // above; a `.to_string()` on the accessor's borrowed `&str`
324        // allocates exactly one `String` byte-equal to the prior raw
325        // `target.versao.clone()` read. Closes the last unlifted per-
326        // `Caixa` universal-axis raw-field-access `String`-carry site on
327        // the caixa-resolver closure-walker's per-fetched-target
328        // [`FetchedDep`] emit surface, sibling to the peer per-`Caixa`
329        // universal-axis converges every peer per-kind renderer (caixa-
330        // helm eb912de, caixa-flux 2fc5f81, caixa-mesh 980c059, caixa-
331        // tatara e73b19f, caixa-crd 41ab9a3) already routes its `:versao`
332        // `String`-carry through.
333        concrete_versao: target.versao().to_string(),
334        conteudo: format!("path:{caminho}"),
335    })
336}
337
338fn fetch_git(
339    dep: &Dep,
340    repo: &str,
341    sole_pin: Option<&str>,
342    cache: &CacheDir,
343    original_fonte: DepSource,
344) -> Result<FetchedDep, ResolveError> {
345    let gitref = sole_pin.ok_or_else(|| ResolveError::missing_pin(dep.nome()))?;
346    let full_url = expand_shorthand(repo);
347    let key_bytes = format!("{full_url}#{gitref}");
348    let key = hash_bytes(key_bytes.as_bytes());
349    let short = &key["blake3:".len()..][..16];
350    let dest = cache.source_dir(short);
351
352    git::clone_or_fetch(&full_url, &dest)?;
353    git::checkout(&dest, gitref)?;
354    let sha = git::head_sha(&dest)?;
355    let conteudo = format!("git:{sha}");
356
357    let manifest_path = dest.join("caixa.lisp");
358    let manifest = std::fs::read_to_string(&manifest_path)?;
359    let target = Caixa::from_lisp(&manifest)?;
360
361    // Freeze :fonte into the lacre with the resolved commit — lock files
362    // are reproducible even if the upstream moves the tag.
363    let resolved = match original_fonte {
364        DepSource::Git {
365            repo: r,
366            tag: t,
367            branch: b,
368            ..
369        } => DepSource::Git {
370            repo: r,
371            tag: t,
372            rev: Some(sha),
373            branch: b,
374        },
375        other => other,
376    };
377
378    Ok(FetchedDep {
379        // Same accessor-route as the sibling [`fetch_path`] arm — the
380        // git-fetched child's `:deps` `Vec<Dep>`-carry projection
381        // reaches for the typed [`Caixa::deps`] `&[Dep]`-return slice
382        // accessor so both fetch-arm branches of the transitive walk
383        // key off the same typed dispatch as the outer
384        // [`resolve_lacre`] queue-seeding read.
385        child_deps: target.deps().to_vec(),
386        resolved_fonte: resolved,
387        // Sibling `:versao` `String`-carry converge to the paired
388        // [`fetch_path`] arm above — the git-fetched child's `:versao`
389        // scalar routes through the typed [`Caixa::versao`] `&str`-
390        // return accessor rather than the raw `.versao.clone()` field
391        // access, so both fetch-arm branches of the closure walker land
392        // the concrete-versao carry through the same typed dispatch.
393        concrete_versao: target.versao().to_string(),
394        conteudo,
395    })
396}
397
398/// Split `"github:pleme-io"` → `("github", "pleme-io")`. Unrecognized hosts
399/// return `("github", default_host_as_is)`.
400fn split_default_host(default_host: &str) -> (&str, &str) {
401    default_host
402        .split_once(':')
403        .unwrap_or(("github", default_host))
404}
405
406/// Per-`Dep` `:fonte` presence-projection with the resolver's
407/// `default_host`-derived fallback baked in — the substrate-primitive
408/// typed dispatch every caixa-resolver closure-walker per-`Dep`
409/// `:fonte`-expansion consumer keys off. The helper reads through the
410/// typed [`caixa_core::Dep::fonte`] `Option<&DepSource>`-return accessor
411/// rather than the raw `.fonte.clone()` field-access, so any future
412/// extension of the accessor's semantics (a per-scope source-override
413/// table on `:fonte` the M4 CR materializer resolves at admission time,
414/// a per-tenant `:fonte` rewrite overlay the roadmap acknowledges, a
415/// lacre-projected concrete-source pin resolver) reaches this fallback
416/// surface through exactly one caixa-core edit rather than a coordinated
417/// rewrite of both open-coded expansions.
418///
419/// Sibling of caixa-feira/src/cmd/lock.rs's `resolve_stub`
420/// (33e5d9a) per-`Dep` `:fonte` accessor-route on the peer stub-resolver
421/// site — same "the emit path must route through the substrate-primitive
422/// typed dispatch" discipline extended onto the caixa-resolver
423/// closure-walker's per-target `:fonte`-expansion surface. Byte-equal to
424/// the prior inline cascade today: on the author-set arm the accessor
425/// returns `Some(&<verbatim>)` and `.cloned()` allocates exactly one
426/// `DepSource` byte-equal to the pre-lift `.fonte.clone()` read; on the
427/// author-omitted arm the accessor returns `None` and the fallback
428/// materializes `DepSource::Git { repo: "<host>:<org>/<nome>", tag/rev/
429/// branch: None }` byte-equal to the pre-lift `unwrap_or_else(...)`
430/// tail. The `default_host` split cascades through
431/// [`split_default_host`] so `"github:pleme-io"` → `github:pleme-io/…`
432/// and `"acme-org"` → `github:acme-org/…` (the unrecognized-host arm
433/// defaults `host` to `"github"`).
434///
435/// The fallback deliberately does NOT compose through
436/// [`caixa_core::DepSource::default_github`] — that helper hardcodes
437/// `"github"` as the host segment (`"github:{org}/{nome}"`), whereas
438/// this helper honors an author-configurable `default_host` (e.g. a
439/// `"codeberg:acme"` config would produce `codeberg:acme/<nome>`,
440/// which `default_github` cannot express).
441pub(crate) fn expand_fonte(dep: &Dep, default_host: &str) -> DepSource {
442    dep.fonte().cloned().unwrap_or_else(|| {
443        let (host, org) = split_default_host(default_host);
444        DepSource::Git {
445            repo: format!("{host}:{org}/{}", dep.nome()),
446            tag: None,
447            rev: None,
448            branch: None,
449        }
450    })
451}
452
453#[allow(dead_code)]
454fn _unused_path(_p: &Path) {}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459    use caixa_core::CaixaKind;
460    use tempfile::tempdir;
461
462    #[test]
463    fn split_default_host_parses_github() {
464        assert_eq!(
465            split_default_host("github:pleme-io"),
466            ("github", "pleme-io")
467        );
468    }
469
470    /// Pin that the per-`Dep` `:fonte` presence-projection under
471    /// [`expand_fonte`] routes through the typed [`Dep::fonte`]
472    /// `Option<&DepSource>`-return accessor rather than the raw
473    /// `.fonte.clone()` field-access, and that the accessor's `None` arm
474    /// materializes the `<host>:<org>/<nome>`-shaped `default_host`-
475    /// derived fallback with `tag`/`rev`/`branch` all `None`. Sweeps
476    /// three arms of the per-`Dep` `:fonte` presence bit × per-config
477    /// `default_host` shape lattice the closure-walker's
478    /// [`fetch_dep`] call-site keys off:
479    ///
480    /// 1. Author-omitted arm × the canonical `"github:pleme-io"`
481    ///    default-host shape — accessor projects `None`, helper falls
482    ///    back to `DepSource::Git { repo: "github:pleme-io/<nome>",
483    ///    tag/rev/branch: None }`, byte-equal to the pre-lift
484    ///    inline `format!("{host}:{org}/{}", dep.nome())` on the
485    ///    canonical default-host arm.
486    /// 2. Author-omitted arm × an unrecognized single-segment
487    ///    `default_host` (`"acme-org"`) — [`split_default_host`]
488    ///    defaults `host` to `"github"` and carries the whole string as
489    ///    `org`, so the fallback is `DepSource::Git { repo:
490    ///    "github:acme-org/<nome>", … }`. Pins the unrecognized-host
491    ///    arm's fall-through under the helper.
492    /// 3. Author-set arm × any `default_host` — accessor projects
493    ///    `Some(&<verbatim>)`, helper carries the source through
494    ///    `.cloned()` byte-verbatim regardless of `default_host` (the
495    ///    fallback branch never fires when the author declared
496    ///    `:fonte`).
497    ///
498    /// Byte-equal today (`.fonte()` returns `self.fonte.as_ref()`;
499    /// `.cloned()` on `Option<&DepSource>` allocates exactly one
500    /// `DepSource` byte-equal to the prior raw `.fonte.clone()` read);
501    /// catches any future emit-side regression that re-introduces the
502    /// raw `.fonte.clone()` cascade at the [`fetch_dep`] call site (a
503    /// future accessor extension — a per-scope alias table, a per-tenant
504    /// `:fonte` rewrite overlay, a lacre-projected concrete-source pin
505    /// resolver — would then reach only the open-coded cascade and
506    /// silently diverge from the helper's typed dispatch, tripping the
507    /// author-set-arm pin below).
508    ///
509    /// Peer of the sibling caixa-feira/src/cmd/lock.rs's
510    /// `resolve_stub_fonte_routes_through_dep_fonte_accessor` (33e5d9a)
511    /// per-`Dep` `:fonte` accessor-route pin on the stub-resolver
512    /// surface — same "the emit path must route through the substrate-
513    /// primitive typed dispatch" discipline extended onto the
514    /// caixa-resolver closure-walker's per-target `:fonte`-expansion
515    /// helper.
516    #[test]
517    #[allow(clippy::too_many_lines)]
518    fn expand_fonte_routes_through_dep_fonte_accessor() {
519        // Arm 1: author-omitted `:fonte` × canonical `"github:pleme-io"`
520        // default-host. Accessor must project `None` and the helper's
521        // fallback must materialize the canonical
522        // `github:pleme-io/<nome>` shorthand.
523        let bare = Dep::simple("caixa-teia", "^0.1");
524        assert!(
525            bare.fonte().is_none(),
526            "author-omitted :fonte must project None through the accessor \
527             — pins the substrate contract the helper's unwrap_or_else \
528             cascade discriminates on",
529        );
530        let expanded = expand_fonte(&bare, "github:pleme-io");
531        assert_eq!(
532            expanded,
533            DepSource::Git {
534                repo: "github:pleme-io/caixa-teia".to_string(),
535                tag: None,
536                rev: None,
537                branch: None,
538            },
539            "author-omitted :fonte on the canonical github:pleme-io \
540             default-host must fall back to the github:<org>/<nome>-shaped \
541             Git shorthand byte-verbatim",
542        );
543
544        // Arm 2: author-omitted `:fonte` × unrecognized single-segment
545        // `default_host` (`"acme-org"`). `split_default_host`'s
546        // unrecognized-host arm defaults `host` to `"github"` and carries
547        // the whole string as `org`, so the helper's fallback must land
548        // `github:acme-org/<nome>` — pins that the helper reads the
549        // `default_host` through the shared [`split_default_host`]
550        // parser rather than re-inlining a `"github:"`-hardcoded
551        // shorthand (which would produce the same output for this input
552        // by coincidence but would diverge on a `"codeberg:acme"`-shaped
553        // config the arm-3 assertion below stresses).
554        let expanded_unrecognized_host = expand_fonte(&bare, "acme-org");
555        assert_eq!(
556            expanded_unrecognized_host,
557            DepSource::Git {
558                repo: "github:acme-org/caixa-teia".to_string(),
559                tag: None,
560                rev: None,
561                branch: None,
562            },
563            "author-omitted :fonte on an unrecognized single-segment \
564             default_host must default the host segment to `github` and \
565             carry the whole config string as the org",
566        );
567
568        // Arm 3: author-set `:fonte` × any `default_host`. Accessor must
569        // project `Some(&<verbatim>)`, and the helper must carry the
570        // source through `.cloned()` byte-verbatim — the fallback branch
571        // never fires when the author declared `:fonte` at authoring
572        // time. Any regression that re-inlined the raw `.fonte.clone()`
573        // cascade would still pass this arm today (both routes are
574        // byte-equal on the `Some` arm) but a future accessor extension
575        // (a per-scope alias table, a per-tenant rewrite overlay)
576        // would silently diverge here — the pin catches that
577        // divergence at compile time as soon as the accessor's return
578        // shape widens beyond `self.fonte.as_ref()`.
579        let with_path = Dep {
580            nome: "child".into(),
581            versao: "0.1.0".into(),
582            fonte: Some(DepSource::Path {
583                caminho: "/tmp/child".into(),
584            }),
585            opcional: false,
586            caracteristicas: vec![],
587        };
588        assert_eq!(
589            with_path.fonte(),
590            Some(&DepSource::Path {
591                caminho: "/tmp/child".into(),
592            }),
593            "author-set :fonte must project Some(&<verbatim>) through \
594             the accessor — pins the substrate contract the helper's \
595             .cloned() carry discriminates on",
596        );
597        let expanded_author_set = expand_fonte(&with_path, "github:pleme-io");
598        assert_eq!(
599            expanded_author_set,
600            DepSource::Path {
601                caminho: "/tmp/child".into(),
602            },
603            "author-set :fonte must carry through the helper .cloned() \
604             byte-verbatim regardless of default_host — the fallback \
605             branch must not fire when the accessor projects Some(&…)",
606        );
607        // Cross-check: swapping `default_host` on the author-set arm
608        // must not perturb the output — the fallback string is dead
609        // code on the `Some` arm.
610        let expanded_author_set_alt_host = expand_fonte(&with_path, "codeberg:acme");
611        assert_eq!(
612            expanded_author_set, expanded_author_set_alt_host,
613            "author-set :fonte must be default_host-agnostic — the \
614             fallback shorthand must not leak into the output when the \
615             accessor projects Some(&…)",
616        );
617    }
618
619    /// Pin that every projection of the outer-`Caixa` `:deps` /
620    /// `:deps-dev` dependency-slot family in [`resolve_lacre`] and the
621    /// per-target [`fetch_path`] transitive walk — the outer
622    /// queue-seeding walks over `root.deps()` and `root.deps_dev()`
623    /// (the two entry points every downstream git-clone target flows
624    /// through), plus the per-fetched-target `target.deps().to_vec()`
625    /// child-dep collection inside [`fetch_path`] the transitive walk
626    /// keys off — routes through the typed [`caixa_core::Caixa::deps`]
627    /// / [`caixa_core::Caixa::deps_dev`] `&[Dep]`-return slice
628    /// accessors rather than raw `&root.deps` / `&root.deps_dev` /
629    /// `target.deps.clone()` field reads. Byte-equal today (each
630    /// accessor returns `self.<field>.as_slice()`); catches any future
631    /// emit-site regression that reintroduces a raw field read, and
632    /// pins the closure-walker's four-site accessor-routing against a
633    /// hermetic tempdir-hosted `defcaixa`-parsed three-caixa closure
634    /// (root → child → grandchild via `:fonte (:tipo path :caminho …)`
635    /// on each edge, plus a dev-only sibling under `:deps-dev` that
636    /// the closure walker admits only when `cfg.include_dev` is true).
637    /// Peer of the sibling caixa-crd's `dep_into_ref_routes_through_dep_accessors`
638    /// (d65d1bf) per-entry `:deps` sub-slot family pin on the paired
639    /// per-`Dep` `Option<&DepSource>` composite-reference axis — same
640    /// "the emit path must route through the substrate-primitive
641    /// typed dispatch" discipline extended onto the outer top-level
642    /// [`Caixa`] `&[Dep]` slice axes at the closure-resolver's
643    /// queue-seeding surface.
644    #[test]
645    #[allow(clippy::too_many_lines)]
646    fn resolve_lacre_routes_dep_slot_family_through_caixa_accessors() {
647        let td = tempdir().expect("tempdir");
648        // Grandchild caixa on disk — no deps.
649        let grandchild_path = td.path().join("grandchild");
650        std::fs::create_dir_all(&grandchild_path).unwrap();
651        std::fs::write(
652            grandchild_path.join("caixa.lisp"),
653            r#"(defcaixa
654                  :nome "grandchild"
655                  :versao "0.1.0"
656                  :kind Biblioteca
657                  :bibliotecas ("lib/grandchild.lisp"))"#,
658        )
659        .unwrap();
660
661        // Child caixa on disk — depends on grandchild via Path.
662        let child_path = td.path().join("child");
663        std::fs::create_dir_all(&child_path).unwrap();
664        let child_lisp = format!(
665            r#"(defcaixa
666                  :nome "child"
667                  :versao "0.1.0"
668                  :kind Biblioteca
669                  :bibliotecas ("lib/child.lisp")
670                  :deps ((:nome "grandchild" :versao "0.1.0"
671                          :fonte (:tipo path :caminho "{}"))))"#,
672            grandchild_path.display()
673        );
674        std::fs::write(child_path.join("caixa.lisp"), child_lisp).unwrap();
675
676        // Dev-only sibling — no deps of its own.
677        let devchild_path = td.path().join("devchild");
678        std::fs::create_dir_all(&devchild_path).unwrap();
679        std::fs::write(
680            devchild_path.join("caixa.lisp"),
681            r#"(defcaixa
682                  :nome "devchild"
683                  :versao "0.1.0"
684                  :kind Biblioteca
685                  :bibliotecas ("lib/devchild.lisp"))"#,
686        )
687        .unwrap();
688
689        let root = Caixa {
690            nome: "root".into(),
691            versao: "0.1.0".into(),
692            kind: CaixaKind::Biblioteca,
693            edicao: None,
694            descricao: None,
695            repositorio: None,
696            licenca: None,
697            autores: vec![],
698            etiquetas: vec![],
699            deps: vec![Dep {
700                nome: "child".into(),
701                versao: "0.1.0".into(),
702                fonte: Some(DepSource::Path {
703                    caminho: child_path.to_string_lossy().into_owned(),
704                }),
705                opcional: false,
706                caracteristicas: vec![],
707            }],
708            deps_dev: vec![Dep {
709                nome: "devchild".into(),
710                versao: "0.1.0".into(),
711                fonte: Some(DepSource::Path {
712                    caminho: devchild_path.to_string_lossy().into_owned(),
713                }),
714                opcional: false,
715                caracteristicas: vec![],
716            }],
717            exe: vec![],
718            bibliotecas: vec![],
719            servicos: vec![],
720            limits: None,
721            behavior: None,
722            upgrade_from: vec![],
723            estrategia: None,
724            max_restarts: None,
725            restart_window: None,
726            children: vec![],
727            membros: vec![],
728            contratos: vec![],
729            politicas: None,
730            placement: None,
731            entrada: None,
732            ci: None,
733        };
734
735        let cache_root = td.path().join("cache");
736        std::fs::create_dir_all(&cache_root).unwrap();
737        let cache = CacheDir::at(&cache_root);
738
739        // include_dev=false: the closure-walker admits only `:deps`
740        // entries + their transitives. `:deps-dev` sibling must NOT
741        // appear, and the walker must have iterated `root.deps()`
742        // (surfacing `"child"`) and then `child.deps().to_vec()` inside
743        // `fetch_path` (surfacing `"grandchild"`).
744        let cfg = ResolverConfig::default();
745        let lacre = resolve_lacre(&root, &cfg, &cache).expect("resolve runtime-only closure");
746        let names: Vec<&str> = lacre.entradas.iter().map(|e| e.nome.as_str()).collect();
747        assert!(
748            names.contains(&"child"),
749            "resolve_lacre must surface `child` via root.deps() — got {names:?}"
750        );
751        assert!(
752            names.contains(&"grandchild"),
753            "resolve_lacre must surface `grandchild` via the transitive \
754             fetch_path target.deps().to_vec() walk — got {names:?}"
755        );
756        assert!(
757            !names.contains(&"devchild"),
758            "resolve_lacre must NOT surface `devchild` when \
759             include_dev=false (deps_dev accessor must be walked only \
760             under the include_dev cfg arm) — got {names:?}"
761        );
762
763        // include_dev=true: the closure-walker also admits `:deps-dev`
764        // entries. The `devchild` sibling must appear via
765        // `root.deps_dev()`.
766        let cfg_with_dev = ResolverConfig {
767            include_dev: true,
768            ..Default::default()
769        };
770        let lacre_with_dev =
771            resolve_lacre(&root, &cfg_with_dev, &cache).expect("resolve with dev closure");
772        let names_with_dev: Vec<&str> = lacre_with_dev
773            .entradas
774            .iter()
775            .map(|e| e.nome.as_str())
776            .collect();
777        assert!(
778            names_with_dev.contains(&"child"),
779            "include_dev=true closure must still surface `child` via \
780             root.deps() — got {names_with_dev:?}"
781        );
782        assert!(
783            names_with_dev.contains(&"grandchild"),
784            "include_dev=true closure must still surface `grandchild` \
785             via the transitive walk — got {names_with_dev:?}"
786        );
787        assert!(
788            names_with_dev.contains(&"devchild"),
789            "include_dev=true closure must surface `devchild` via \
790             root.deps_dev() — got {names_with_dev:?}"
791        );
792    }
793
794    /// Pin that both fetch-arm branches of the closure walker's per-
795    /// fetched-target [`FetchedDep`] emit surface — the [`fetch_path`]
796    /// arm and the [`fetch_git`] arm — carry each target's `:versao`
797    /// scalar into the resulting [`crate::LacreEntry`] via the typed
798    /// [`caixa_core::Caixa::versao`] `&str`-return accessor rather than a
799    /// raw `target.versao.clone()` field access. Byte-equal today
800    /// (`Caixa::versao` is `&self.versao`); catches any future emit-side
801    /// regression that reintroduces a raw field read and pins the two
802    /// [`FetchedDep::concrete_versao`] construction sites against a
803    /// hermetic tempdir-hosted three-caixa closure whose `:versao` bytes
804    /// distinguish each layer (root `"0.9.0"` → child `"0.5.2"` →
805    /// grandchild `"0.1.3"`), so a stub-that-hardcodes-a-fixed-string
806    /// regression trips on any layer.
807    ///
808    /// Peer of the sibling [`resolve_lacre_routes_dep_slot_family_through_caixa_accessors`]
809    /// per-`Caixa` `:deps` / `:deps-dev` slot-family pin on the sibling
810    /// per-`Caixa` `&[Dep]` slice-return accessor axis — same "the
811    /// emit path must route through the substrate-primitive typed
812    /// dispatch" discipline extended onto the outer top-level [`Caixa`]
813    /// `:versao` `&str`-return universal-axis at the closure-resolver's
814    /// per-fetched-target [`FetchedDep`] emit surface. Sibling to the
815    /// peer per-`Caixa` universal-axis converges every peer per-kind
816    /// renderer (caixa-helm eb912de, caixa-flux 2fc5f81, caixa-mesh
817    /// 980c059, caixa-tatara e73b19f, caixa-crd 41ab9a3) already routes
818    /// its `:versao` `String`-carry through.
819    #[test]
820    fn resolve_lacre_fetched_target_versao_routes_through_caixa_versao_accessor() {
821        let td = tempdir().expect("tempdir");
822        // Grandchild caixa on disk — `:versao "0.1.3"`.
823        let grandchild_path = td.path().join("grandchild");
824        std::fs::create_dir_all(&grandchild_path).unwrap();
825        std::fs::write(
826            grandchild_path.join("caixa.lisp"),
827            r#"(defcaixa
828                  :nome "grandchild"
829                  :versao "0.1.3"
830                  :kind Biblioteca
831                  :bibliotecas ("lib/grandchild.lisp"))"#,
832        )
833        .unwrap();
834
835        // Child caixa on disk — `:versao "0.5.2"`, depends on grandchild
836        // via Path so the closure walker traverses through
837        // [`fetch_path`] on both edges.
838        let child_path = td.path().join("child");
839        std::fs::create_dir_all(&child_path).unwrap();
840        let child_lisp = format!(
841            r#"(defcaixa
842                  :nome "child"
843                  :versao "0.5.2"
844                  :kind Biblioteca
845                  :bibliotecas ("lib/child.lisp")
846                  :deps ((:nome "grandchild" :versao "0.1.3"
847                          :fonte (:tipo path :caminho "{}"))))"#,
848            grandchild_path.display()
849        );
850        std::fs::write(child_path.join("caixa.lisp"), child_lisp).unwrap();
851
852        let root = Caixa {
853            nome: "root".into(),
854            versao: "0.9.0".into(),
855            kind: CaixaKind::Biblioteca,
856            edicao: None,
857            descricao: None,
858            repositorio: None,
859            licenca: None,
860            autores: vec![],
861            etiquetas: vec![],
862            deps: vec![Dep {
863                nome: "child".into(),
864                versao: "0.5.2".into(),
865                fonte: Some(DepSource::Path {
866                    caminho: child_path.to_string_lossy().into_owned(),
867                }),
868                opcional: false,
869                caracteristicas: vec![],
870            }],
871            deps_dev: vec![],
872            exe: vec![],
873            bibliotecas: vec![],
874            servicos: vec![],
875            limits: None,
876            behavior: None,
877            upgrade_from: vec![],
878            estrategia: None,
879            max_restarts: None,
880            restart_window: None,
881            children: vec![],
882            membros: vec![],
883            contratos: vec![],
884            politicas: None,
885            placement: None,
886            entrada: None,
887            ci: None,
888        };
889
890        let cache_root = td.path().join("cache");
891        std::fs::create_dir_all(&cache_root).unwrap();
892        let cache = CacheDir::at(&cache_root);
893        let cfg = ResolverConfig::default();
894        let lacre = resolve_lacre(&root, &cfg, &cache).expect("resolve closure");
895
896        // Every fetched target's `:versao` scalar must land in the
897        // resulting `LacreEntry.versao` byte-verbatim to what
898        // `Caixa::versao()` returns for the source caixa on disk. A
899        // regression that hardcoded a fixed string on either fetch-arm
900        // side of the closure walker trips on the layer whose byte-shape
901        // it drifted off.
902        let pairs: Vec<(&str, &str)> = lacre
903            .entradas
904            .iter()
905            .map(|e| (e.nome.as_str(), e.versao.as_str()))
906            .collect();
907        assert!(
908            pairs.contains(&("child", "0.5.2")),
909            "child entry must carry `:versao \"0.5.2\"` verbatim through \
910             fetch_path -> Caixa::versao() -> FetchedDep::concrete_versao \
911             -> LacreEntry.versao — got {pairs:?}"
912        );
913        assert!(
914            pairs.contains(&("grandchild", "0.1.3")),
915            "grandchild entry must carry `:versao \"0.1.3\"` verbatim \
916             through the transitive walk's Caixa::versao() accessor route \
917             — got {pairs:?}"
918        );
919    }
920
921    /// Pin that the closure-walker's queue-seeding parent-`:nome` label
922    /// axis — the `String` carried on the queue's `from` arm at both the
923    /// outer `for dep in root.deps()` runtime seed and the
924    /// `cfg.include_dev`-gated `for dep in root.deps_dev()` dev-only
925    /// seed inside [`resolve_lacre`] — routes through the typed
926    /// [`caixa_core::Caixa::nome`] `&str`-return accessor rather than a
927    /// raw `root.nome.clone()` field read. The queue's per-transitive-
928    /// target sibling push already routes the paired parent-`:nome`
929    /// label through the peer [`caixa_core::Dep::nome`] accessor
930    /// (`dep.nome().to_string()` at the transitive-walk head), so this
931    /// pin closes the last unlifted parent-`:nome` field-read axis in
932    /// the closure walker's queue-carried `from` surface.
933    ///
934    /// Byte-equal today (`Caixa::nome` is `&self.nome`; `.to_string()`
935    /// on the borrowed `&str` allocates exactly one `String` byte-equal
936    /// to the prior raw `.nome.clone()` read); pin catches any future
937    /// silent detour that reintroduces a raw field read at either queue-
938    /// seeding site. The hermetic tempdir fixture uses a byte-
939    /// distinctive `:nome` on the root (`"root-abc"` — pairing DNS-1123
940    /// legality with three ASCII bytes distinct from every child's
941    /// `:nome` so a stub-that-hardcodes-a-fixed-string regression on
942    /// either seeding site can only pass by accident), and asserts
943    /// [`caixa_core::Caixa::nome`] returns byte-verbatim to what
944    /// [`resolve_lacre`]'s queue-seed reads. The end-to-end assertion
945    /// on the resulting [`crate::LacreEntry`] set pins that both
946    /// `:deps` and `:deps-dev` walks reach their transitive targets
947    /// after the accessor-route substitution, so a regression that
948    /// broke the queue-seeding shape by dropping the second push arm
949    /// (e.g. mis-collapsing the two guarded arms onto one) surfaces
950    /// as a missing child in the resolved closure.
951    ///
952    /// Peer of the sibling [`resolve_lacre_routes_dep_slot_family_through_caixa_accessors`]
953    /// per-`:deps` / `:deps-dev` slot-family pin above and of the
954    /// [`resolve_lacre_fetched_target_versao_routes_through_caixa_versao_accessor`]
955    /// per-`:versao` axis pin (0556249) on the same
956    /// closure-walker emit surface — same "the emit path must route
957    /// through the substrate-primitive typed dispatch" discipline the
958    /// per-`Caixa` `.nome` universal-axis converges every peer per-kind
959    /// renderer already carry, extended onto the outer top-level
960    /// [`Caixa`] `:nome` `&str`-return universal-axis at the closure-
961    /// resolver's queue-seeded per-transitive-target `from` label.
962    #[test]
963    fn resolve_lacre_queue_seed_parent_nome_routes_through_caixa_nome_accessor() {
964        let td = tempdir().expect("tempdir");
965        // Runtime-child caixa on disk — no deps of its own, distinct
966        // `:nome` bytes so it can be identified in the resolved closure.
967        let child_path = td.path().join("child");
968        std::fs::create_dir_all(&child_path).unwrap();
969        std::fs::write(
970            child_path.join("caixa.lisp"),
971            r#"(defcaixa
972                  :nome "child"
973                  :versao "0.1.0"
974                  :kind Biblioteca
975                  :bibliotecas ("lib/child.lisp"))"#,
976        )
977        .unwrap();
978
979        // Dev-only sibling — distinct `:nome`, guards the second
980        // seeding-arm branch.
981        let devchild_path = td.path().join("devchild");
982        std::fs::create_dir_all(&devchild_path).unwrap();
983        std::fs::write(
984            devchild_path.join("caixa.lisp"),
985            r#"(defcaixa
986                  :nome "devchild"
987                  :versao "0.1.0"
988                  :kind Biblioteca
989                  :bibliotecas ("lib/devchild.lisp"))"#,
990        )
991        .unwrap();
992
993        let root = Caixa {
994            nome: "root-abc".into(),
995            versao: "0.9.0".into(),
996            kind: CaixaKind::Biblioteca,
997            edicao: None,
998            descricao: None,
999            repositorio: None,
1000            licenca: None,
1001            autores: vec![],
1002            etiquetas: vec![],
1003            deps: vec![Dep {
1004                nome: "child".into(),
1005                versao: "0.1.0".into(),
1006                fonte: Some(DepSource::Path {
1007                    caminho: child_path.to_string_lossy().into_owned(),
1008                }),
1009                opcional: false,
1010                caracteristicas: vec![],
1011            }],
1012            deps_dev: vec![Dep {
1013                nome: "devchild".into(),
1014                versao: "0.1.0".into(),
1015                fonte: Some(DepSource::Path {
1016                    caminho: devchild_path.to_string_lossy().into_owned(),
1017                }),
1018                opcional: false,
1019                caracteristicas: vec![],
1020            }],
1021            exe: vec![],
1022            bibliotecas: vec![],
1023            servicos: vec![],
1024            limits: None,
1025            behavior: None,
1026            upgrade_from: vec![],
1027            estrategia: None,
1028            max_restarts: None,
1029            restart_window: None,
1030            children: vec![],
1031            membros: vec![],
1032            contratos: vec![],
1033            politicas: None,
1034            placement: None,
1035            entrada: None,
1036            ci: None,
1037        };
1038
1039        // Substrate-primitive byte-parity pin: `Caixa::nome()` returns
1040        // the raw `:nome` byte-string verbatim. Both queue-seeded push
1041        // arms compose `root.nome().to_string()` off this accessor, so
1042        // a future accessor extension (a canonicalization pass, an
1043        // aliasing overlay) reaches both call sites through one edit.
1044        assert_eq!(
1045            root.nome(),
1046            "root-abc",
1047            "Caixa::nome() must return the :nome byte-string verbatim — \
1048             pins the substrate contract both queue-seeded push arms \
1049             compose the parent-`:nome` label off of"
1050        );
1051        assert_eq!(
1052            root.nome().to_string(),
1053            "root-abc",
1054            "root.nome().to_string() must equal the raw :nome byte-string \
1055             verbatim — pins the accessor-routed String-carry the queue's \
1056             `from` arm receives at both seeding sites"
1057        );
1058
1059        // End-to-end pin on the closure walker's two-arm seeding surface:
1060        // both `:deps` and `:deps-dev` walks must reach their transitive
1061        // targets after the accessor-route substitution. A regression
1062        // that dropped either arm's queue-seed push surfaces here as a
1063        // missing child in the resolved lacre.
1064        let cache_root = td.path().join("cache");
1065        std::fs::create_dir_all(&cache_root).unwrap();
1066        let cache = CacheDir::at(&cache_root);
1067        let cfg_with_dev = ResolverConfig {
1068            include_dev: true,
1069            ..Default::default()
1070        };
1071        let lacre =
1072            resolve_lacre(&root, &cfg_with_dev, &cache).expect("resolve closure with dev deps");
1073        let names: Vec<&str> = lacre.entradas.iter().map(|e| e.nome.as_str()).collect();
1074        assert!(
1075            names.contains(&"child"),
1076            "resolve_lacre with the accessor-routed queue-seed must \
1077             surface `child` via the runtime `:deps` arm — got {names:?}"
1078        );
1079        assert!(
1080            names.contains(&"devchild"),
1081            "resolve_lacre with the accessor-routed queue-seed must \
1082             surface `devchild` via the `cfg.include_dev`-gated \
1083             `:deps-dev` arm — got {names:?}"
1084        );
1085    }
1086
1087    /// Pin that [`ResolveError::missing_path`] is byte-equal to the
1088    /// pre-lift open-coded four-line `{ nome: nome.to_string(), path }`
1089    /// two-slot struct-literal on a representative
1090    /// `("caixa-teia", PathBuf("/nonexistent/child"))` fixture. Catches
1091    /// any future ctor-side silent field-swap, `.to_string()` /
1092    /// `.into()` cascade drop, or variant rename that would leave the
1093    /// wire-up compiling but surface a different two-slot payload than
1094    /// the pre-lift struct-literal.
1095    #[test]
1096    fn missing_path_ctor_matches_struct_literal_wrap() {
1097        let ctor = ResolveError::missing_path("caixa-teia", PathBuf::from("/nonexistent/child"));
1098        let literal = ResolveError::MissingPath {
1099            nome: "caixa-teia".to_string(),
1100            path: PathBuf::from("/nonexistent/child"),
1101        };
1102        assert_eq!(
1103            format!("{ctor:?}"),
1104            format!("{literal:?}"),
1105            "missing_path ctor must debug-render byte-equal to the \
1106             open-coded two-slot struct-literal on the same fixture — \
1107             any silent field-swap or cascade drop surfaces here"
1108        );
1109        assert_eq!(
1110            format!("{ctor}"),
1111            format!("{literal}"),
1112            "missing_path ctor must display-render byte-equal to the \
1113             open-coded struct-literal on the same fixture — pins the \
1114             thiserror #[error] template's routing through both slots"
1115        );
1116    }
1117
1118    /// Sweep [`ResolveError::missing_path`] across a boundary matrix of
1119    /// `nome` shapes × `path` shapes so any wrapper-side silent
1120    /// lowercase, trim, truncate, two-axis field-swap, or `.into()`
1121    /// cascade divergence on the two-field construction surfaces at
1122    /// assert time rather than at a downstream diagnostic consumer that
1123    /// reads the fields back and gets a different value than the one it
1124    /// stored.
1125    ///
1126    /// - `nome` axis: canonical DNS-1123 identifier (`"caixa-teia"`),
1127    ///   single-char (`"a"`), and an inner-hyphen + digit shape
1128    ///   (`"hello-rio-42"`) — the three canonical `Dep::nome`-return
1129    ///   shapes the `caixa-core::Dep::nome` accessor surfaces at the
1130    ///   wire-up.
1131    /// - `path` axis: absolute UNIX-style (`"/absent/child"`), relative
1132    ///   single-segment (`"child"`), and a nested relative with parent
1133    ///   traversal (`"../sibling/child"`) — the three canonical
1134    ///   `PathBuf::from(caminho)` shapes the `DepSource::Path
1135    ///   { caminho }` arm surfaces at the wire-up.
1136    ///
1137    /// Both `&str`-literal and `&String` (via Deref coercion) carriers
1138    /// are exercised for `nome` because the wire-up hands `dep.nome()`
1139    /// (a `&str` accessor); both `PathBuf`-owned and `&Path` (via
1140    /// `Into<PathBuf>`) carriers are exercised for `path` because the
1141    /// wire-up hands a `PathBuf` built from `PathBuf::from(caminho)`.
1142    #[test]
1143    fn missing_path_ctor_routes_nome_and_path_through_verbatim() {
1144        let nomes: &[&str] = &["caixa-teia", "a", "hello-rio-42"];
1145        let paths: &[PathBuf] = &[
1146            PathBuf::from("/absent/child"),
1147            PathBuf::from("child"),
1148            PathBuf::from("../sibling/child"),
1149        ];
1150        for nome in nomes {
1151            for path in paths {
1152                // &str carrier for nome × PathBuf-owned carrier for path.
1153                let ctor = ResolveError::missing_path(nome, path.clone());
1154                match &ctor {
1155                    ResolveError::MissingPath {
1156                        nome: got_nome,
1157                        path: got_path,
1158                    } => {
1159                        assert_eq!(
1160                            got_nome, nome,
1161                            "missing_path ctor must carry nome byte-verbatim \
1162                             — no silent lowercase/trim/truncate on the \
1163                             &str-literal carrier at {nome}"
1164                        );
1165                        assert_eq!(
1166                            got_path, path,
1167                            "missing_path ctor must carry path byte-verbatim \
1168                             — no silent normalization/canonicalization on \
1169                             the PathBuf-owned carrier at {path:?}"
1170                        );
1171                    }
1172                    other => panic!(
1173                        "missing_path ctor must construct the MissingPath \
1174                         variant — got {other:?} for ({nome:?}, {path:?})"
1175                    ),
1176                }
1177                // &String (Deref coercion) carrier for nome × &Path
1178                // (Into<PathBuf>) carrier for path — pins that the ctor
1179                // signature accepts both without a pre-conversion at the
1180                // call site.
1181                let owned_nome = String::from(*nome);
1182                let ctor2 = ResolveError::missing_path(&owned_nome, path.as_path());
1183                match &ctor2 {
1184                    ResolveError::MissingPath {
1185                        nome: got_nome,
1186                        path: got_path,
1187                    } => {
1188                        assert_eq!(
1189                            got_nome, nome,
1190                            "missing_path ctor must carry nome byte-verbatim \
1191                             on the &String Deref-coercion carrier at {nome}"
1192                        );
1193                        assert_eq!(
1194                            got_path, path,
1195                            "missing_path ctor must carry path byte-verbatim \
1196                             on the &Path Into<PathBuf> carrier at {path:?}"
1197                        );
1198                    }
1199                    other => panic!(
1200                        "missing_path ctor must construct the MissingPath \
1201                         variant on the &String/&Path carriers — got \
1202                         {other:?} for ({nome:?}, {path:?})"
1203                    ),
1204                }
1205            }
1206        }
1207    }
1208
1209    /// Pin the end-to-end route from [`fetch_path`]'s on-disk-absence
1210    /// arm through [`ResolveError::missing_path`]: authoring a `:deps`
1211    /// entry whose `:fonte (:tipo path :caminho …)` points at a path
1212    /// that does not exist on disk must surface a
1213    /// `ResolveError::MissingPath { nome, path }` byte-equal to the
1214    /// substrate-primitive ctor on the same fixture, so a future silent
1215    /// de-lift of the wire-up back to the open-coded struct-literal
1216    /// trips at caixa-resolver test time rather than at a downstream
1217    /// diagnostic consumer far from the wire-up commit.
1218    #[test]
1219    fn fetch_path_absent_dir_routes_through_missing_path_ctor() {
1220        let td = tempdir().expect("tempdir");
1221        // Root caixa points at a `:caminho` under the tempdir that
1222        // was never created — the closure walker's `fetch_path` arm
1223        // must refuse before it opens the (nonexistent)
1224        // `caixa.lisp`.
1225        let absent = td.path().join("absent-child");
1226        assert!(
1227            !absent.exists(),
1228            "test precondition: the child path must be absent on disk \
1229             so fetch_path's `!path.exists()` refusal arm fires"
1230        );
1231        let root = Caixa {
1232            nome: "root".into(),
1233            versao: "0.1.0".into(),
1234            kind: CaixaKind::Biblioteca,
1235            edicao: None,
1236            descricao: None,
1237            repositorio: None,
1238            licenca: None,
1239            autores: vec![],
1240            etiquetas: vec![],
1241            deps: vec![Dep {
1242                nome: "absent-child".into(),
1243                versao: "0.1.0".into(),
1244                fonte: Some(DepSource::Path {
1245                    caminho: absent.to_string_lossy().into_owned(),
1246                }),
1247                opcional: false,
1248                caracteristicas: vec![],
1249            }],
1250            deps_dev: vec![],
1251            exe: vec![],
1252            bibliotecas: vec![],
1253            servicos: vec![],
1254            limits: None,
1255            behavior: None,
1256            upgrade_from: vec![],
1257            estrategia: None,
1258            max_restarts: None,
1259            restart_window: None,
1260            children: vec![],
1261            membros: vec![],
1262            contratos: vec![],
1263            politicas: None,
1264            placement: None,
1265            entrada: None,
1266            ci: None,
1267        };
1268        let cache_root = td.path().join("cache");
1269        std::fs::create_dir_all(&cache_root).unwrap();
1270        let cache = CacheDir::at(&cache_root);
1271        let cfg = ResolverConfig::default();
1272        let err = resolve_lacre(&root, &cfg, &cache)
1273            .expect_err("resolve_lacre must refuse when a :caminho points at an absent path");
1274        let expected = ResolveError::missing_path("absent-child", absent.clone());
1275        assert_eq!(
1276            format!("{err:?}"),
1277            format!("{expected:?}"),
1278            "fetch_path absent-dir refusal must debug-render byte-equal \
1279             to the substrate-primitive missing_path ctor on the same \
1280             fixture — a silent de-lift of the wire-up back to the \
1281             open-coded struct-literal would still pass this arm today \
1282             (both routes are byte-equal at emit time) but a future \
1283             ctor-side extension (a per-cluster :caminho overlay \
1284             rewrite, a per-scope alias table) would silently diverge \
1285             here as soon as the ctor's construction path widens"
1286        );
1287        assert_eq!(
1288            format!("{err}"),
1289            format!("{expected}"),
1290            "fetch_path absent-dir refusal must display-render \
1291             byte-equal to the substrate-primitive missing_path ctor \
1292             — pins the #[error(...)] template routing on both slots"
1293        );
1294        match err {
1295            ResolveError::MissingPath {
1296                nome: got_nome,
1297                path: got_path,
1298            } => {
1299                assert_eq!(
1300                    got_nome, "absent-child",
1301                    "fetch_path refusal must surface the offending Dep::nome \
1302                     verbatim in the MissingPath::nome slot"
1303                );
1304                assert_eq!(
1305                    got_path, absent,
1306                    "fetch_path refusal must surface the offending \
1307                     PathBuf::from(caminho) verbatim in the \
1308                     MissingPath::path slot"
1309                );
1310            }
1311            other => panic!(
1312                "fetch_path absent-dir refusal must construct the \
1313                 MissingPath variant on the ResolveError envelope — got \
1314                 {other:?}"
1315            ),
1316        }
1317    }
1318
1319    /// Pin that [`ResolveError::missing_pin`] is byte-equal to the
1320    /// pre-lift open-coded three-line `{ nome: nome.to_string() }`
1321    /// one-slot struct-literal on a representative `"caixa-teia"`
1322    /// fixture. Catches any future ctor-side silent `.to_string()`
1323    /// cascade drop, variant rename, or slot-widening that would
1324    /// leave the wire-up compiling but surface a different one-slot
1325    /// payload than the pre-lift struct-literal.
1326    #[test]
1327    fn missing_pin_ctor_matches_struct_literal_wrap() {
1328        let ctor = ResolveError::missing_pin("caixa-teia");
1329        let literal = ResolveError::MissingPin {
1330            nome: "caixa-teia".to_string(),
1331        };
1332        assert_eq!(
1333            format!("{ctor:?}"),
1334            format!("{literal:?}"),
1335            "missing_pin ctor must debug-render byte-equal to the \
1336             open-coded one-slot struct-literal on the same fixture — \
1337             any silent cascade drop or slot-widening surfaces here"
1338        );
1339        assert_eq!(
1340            format!("{ctor}"),
1341            format!("{literal}"),
1342            "missing_pin ctor must display-render byte-equal to the \
1343             open-coded struct-literal on the same fixture — pins the \
1344             thiserror #[error] template's routing through the sole slot"
1345        );
1346    }
1347
1348    /// Sweep [`ResolveError::missing_pin`] across the boundary matrix
1349    /// of `nome` shapes so any wrapper-side silent lowercase, trim,
1350    /// truncate, or `.to_string()`-cascade divergence on the one-field
1351    /// construction surfaces at assert time rather than at a downstream
1352    /// diagnostic consumer that reads the field back and gets a
1353    /// different value than the one it stored.
1354    ///
1355    /// - `nome` axis: canonical DNS-1123 identifier (`"caixa-teia"`),
1356    ///   single-char (`"a"`), and an inner-hyphen + digit shape
1357    ///   (`"hello-rio-42"`) — the three canonical [`Dep::nome`]-return
1358    ///   shapes the [`caixa_core::Dep::nome`] accessor surfaces at the
1359    ///   [`fetch_git`] wire-up. Byte-parallel to the sibling
1360    ///   [`missing_path_ctor_routes_nome_and_path_through_verbatim`]
1361    ///   sweep on the peer [`ResolveError::missing_path`] ctor.
1362    ///
1363    /// Both `&str`-literal and `&String` (via Deref coercion) carriers
1364    /// are exercised because the wire-up hands `dep.nome()` (a `&str`
1365    /// accessor).
1366    #[test]
1367    fn missing_pin_ctor_routes_nome_through_verbatim() {
1368        let nomes: &[&str] = &["caixa-teia", "a", "hello-rio-42"];
1369        for nome in nomes {
1370            // &str carrier.
1371            let ctor = ResolveError::missing_pin(nome);
1372            match &ctor {
1373                ResolveError::MissingPin { nome: got_nome } => {
1374                    assert_eq!(
1375                        got_nome, nome,
1376                        "missing_pin ctor must carry nome byte-verbatim \
1377                         — no silent lowercase/trim/truncate on the \
1378                         &str-literal carrier at {nome}"
1379                    );
1380                }
1381                other => panic!(
1382                    "missing_pin ctor must construct the MissingPin \
1383                     variant — got {other:?} for {nome:?}"
1384                ),
1385            }
1386            // &String (Deref coercion) carrier — pins that the ctor
1387            // signature accepts both without a pre-conversion at the
1388            // call site.
1389            let owned_nome = String::from(*nome);
1390            let ctor2 = ResolveError::missing_pin(&owned_nome);
1391            match &ctor2 {
1392                ResolveError::MissingPin { nome: got_nome } => {
1393                    assert_eq!(
1394                        got_nome, nome,
1395                        "missing_pin ctor must carry nome byte-verbatim \
1396                         on the &String Deref-coercion carrier at {nome}"
1397                    );
1398                }
1399                other => panic!(
1400                    "missing_pin ctor must construct the MissingPin \
1401                     variant on the &String carrier — got {other:?} \
1402                     for {nome:?}"
1403                ),
1404            }
1405        }
1406    }
1407
1408    /// Pin the end-to-end route from [`fetch_git`]'s sole-set-pin
1409    /// absence arm through [`ResolveError::missing_pin`]: authoring a
1410    /// `:deps` entry whose expanded `:fonte (:tipo git …)` shape lands
1411    /// with every `tag`/`rev`/`branch` pin `None` (the shape
1412    /// [`expand_fonte`] materializes for an author-omitted-`:fonte`
1413    /// bare [`Dep::simple`] dep under the canonical
1414    /// `"github:pleme-io"` default-host) must surface a
1415    /// `ResolveError::MissingPin { nome }` byte-equal to the
1416    /// substrate-primitive ctor on the same fixture, so a future
1417    /// silent de-lift of the wire-up back to the open-coded
1418    /// struct-literal trips at caixa-resolver test time rather than
1419    /// at a downstream diagnostic consumer far from the wire-up
1420    /// commit. The refusal fires inside [`fetch_git`]'s
1421    /// `sole_pin.ok_or_else(...)?` arm before any `git::clone_or_fetch`
1422    /// call, so the test is fully hermetic — no network, no `git`
1423    /// binary on the runner, no on-disk source directory beyond the
1424    /// empty cache root.
1425    #[test]
1426    fn fetch_git_unpinned_dep_routes_through_missing_pin_ctor() {
1427        let td = tempdir().expect("tempdir");
1428        // Root caixa with one bare, registry-shaped dep whose `:fonte`
1429        // the author omitted — [`expand_fonte`] materializes the
1430        // `github:pleme-io/absent-child` shorthand with every pin
1431        // (`tag`, `rev`, `branch`) `None`, so
1432        // `DepSource::sole_pin()` projects `None` at the fetch site
1433        // and `fetch_git`'s `.ok_or_else` closure-form emission arm
1434        // fires before any git operation.
1435        let root = Caixa {
1436            nome: "root".into(),
1437            versao: "0.1.0".into(),
1438            kind: CaixaKind::Biblioteca,
1439            edicao: None,
1440            descricao: None,
1441            repositorio: None,
1442            licenca: None,
1443            autores: vec![],
1444            etiquetas: vec![],
1445            deps: vec![Dep::simple("absent-child", "^0.1")],
1446            deps_dev: vec![],
1447            exe: vec![],
1448            bibliotecas: vec![],
1449            servicos: vec![],
1450            limits: None,
1451            behavior: None,
1452            upgrade_from: vec![],
1453            estrategia: None,
1454            max_restarts: None,
1455            restart_window: None,
1456            children: vec![],
1457            membros: vec![],
1458            contratos: vec![],
1459            politicas: None,
1460            placement: None,
1461            entrada: None,
1462            ci: None,
1463        };
1464
1465        // Test precondition: the bare `Dep::simple(...)` shape must
1466        // reach `fetch_git` with every pin `None` after `expand_fonte`
1467        // materializes the default-host shorthand. Pinning this
1468        // upstream so a future `Dep::simple` extension (e.g. auto-
1469        // pinning to a lockfile-derived rev) surfaces here rather
1470        // than at the ctor-route assert below.
1471        let expanded = expand_fonte(&root.deps[0], "github:pleme-io");
1472        assert!(
1473            matches!(
1474                &expanded,
1475                DepSource::Git {
1476                    tag: None,
1477                    rev: None,
1478                    branch: None,
1479                    ..
1480                }
1481            ),
1482            "test precondition: expand_fonte on a bare Dep::simple \
1483             under the canonical default-host must land with every \
1484             pin `None` — got {expanded:?}"
1485        );
1486        assert_eq!(
1487            expanded.sole_pin(),
1488            None,
1489            "test precondition: DepSource::sole_pin on the unpinned \
1490             git-shape must project None so fetch_git's ok_or_else \
1491             fires before any git operation"
1492        );
1493
1494        let cache_root = td.path().join("cache");
1495        std::fs::create_dir_all(&cache_root).unwrap();
1496        let cache = CacheDir::at(&cache_root);
1497        let cfg = ResolverConfig::default();
1498        let err = resolve_lacre(&root, &cfg, &cache).expect_err(
1499            "resolve_lacre must refuse when a bare :deps entry expands \
1500             to a git source with no :tag/:rev/:branch pin",
1501        );
1502        let expected = ResolveError::missing_pin("absent-child");
1503        assert_eq!(
1504            format!("{err:?}"),
1505            format!("{expected:?}"),
1506            "fetch_git unpinned-dep refusal must debug-render \
1507             byte-equal to the substrate-primitive missing_pin ctor \
1508             on the same fixture — a silent de-lift of the wire-up \
1509             back to the open-coded struct-literal would still pass \
1510             this arm today (both routes are byte-equal at emit time) \
1511             but a future ctor-side extension (a per-scope pin \
1512             overlay, a per-cluster :fonte rewrite) would silently \
1513             diverge here as soon as the ctor's construction path \
1514             widens"
1515        );
1516        assert_eq!(
1517            format!("{err}"),
1518            format!("{expected}"),
1519            "fetch_git unpinned-dep refusal must display-render \
1520             byte-equal to the substrate-primitive missing_pin ctor \
1521             — pins the #[error(...)] template routing on the sole slot"
1522        );
1523        match err {
1524            ResolveError::MissingPin { nome: got_nome } => {
1525                assert_eq!(
1526                    got_nome, "absent-child",
1527                    "fetch_git refusal must surface the offending \
1528                     Dep::nome verbatim in the MissingPin::nome slot"
1529                );
1530            }
1531            other => panic!(
1532                "fetch_git unpinned-dep refusal must construct the \
1533                 MissingPin variant on the ResolveError envelope — \
1534                 got {other:?}"
1535            ),
1536        }
1537    }
1538}