Skip to main content

caixa_core/
layout.rs

1//! Layout invariants — the Rust-enforced package structure.
2//!
3//! This is the caixa analog of Cargo's implicit `src/lib.rs` vs `src/main.rs`
4//! rule: the Rust type system dictates the package shape, and the invariant
5//! checker runs before any build step. [`StandardLayout`] encodes the
6//! canonical layout:
7//!
8//! - `caixa.lisp`           — always required
9//! - `lib/<nome>.lisp`      — required when `:kind Biblioteca` and
10//!                            `:bibliotecas` is empty
11//! - each `:bibliotecas`    — must resolve on disk
12//! - each `:exe`            — must resolve on disk, under `exe/`
13//! - each `:servicos`       — must resolve on disk, under `servicos/`
14//!
15//! Filesystem I/O is injected through [`StandardLayout::with_path_exists`]
16//! so tests can run without touching disk.
17
18use std::path::{Path, PathBuf};
19use std::sync::Arc;
20
21use thiserror::Error;
22
23use crate::{Caixa, CaixaKind};
24
25/// Contract — a caixa layout checker.
26pub trait LayoutInvariants {
27    /// Verify every declared path resolves + kind-specific invariants hold.
28    fn verify(&self, caixa: &Caixa, root: &Path) -> Result<(), LayoutError>;
29}
30
31type ExistsFn = Arc<dyn Fn(&Path) -> bool + Send + Sync>;
32
33/// The default layout contract.
34#[derive(Default, Clone)]
35pub struct StandardLayout {
36    path_exists: Option<ExistsFn>,
37}
38
39impl StandardLayout {
40    #[must_use]
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// Override how file existence is tested. Useful for in-memory tests.
46    #[must_use]
47    pub fn with_path_exists<F>(mut self, f: F) -> Self
48    where
49        F: Fn(&Path) -> bool + Send + Sync + 'static,
50    {
51        self.path_exists = Some(Arc::new(f));
52        self
53    }
54
55    fn exists(&self, p: &Path) -> bool {
56        self.path_exists
57            .as_ref()
58            .map_or_else(|| p.exists(), |f| f(p))
59    }
60
61    /// Probe an authored path's resolved on-disk location under `root`
62    /// and, on absence, return the paired [`LayoutError::MissingEntry`]
63    /// naming the missing entry at that resolved location with the
64    /// caller-provided canonical `kind` label (from the
65    /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] const family).
66    /// Returns the resolved [`PathBuf`] on hit so callers that need to
67    /// run a follow-up per-path gate (the `:exe` /
68    /// `:servicos` sandbox-directory-containment check the peer
69    /// wire-up sites carry) reach it without re-computing
70    /// `root.join(path)`.
71    ///
72    /// Folds the five self-similar
73    /// `let full = root.join(p); if !self.exists(&full) { return
74    /// Err(LayoutError::missing_entry(<kind>, full)); }` existence-probe
75    /// blocks at [`Self::verify`] (`:bibliotecas` iteration, `:exe`
76    /// iteration, `:servicos` iteration, `:behavior` on-disk callback-
77    /// path iteration, `:upgrade-from` per-instruction script-path
78    /// iteration) onto one substrate primitive on [`StandardLayout`].
79    /// Every future consumer that wants to probe a declared path
80    /// against an out-of-band filesystem oracle (a per-slot admission
81    /// webhook, a `feira validate --paths` per-caixa admission verb,
82    /// the deferred `caixa.pleme.io/v1alpha1/Caixa` CR materializer's
83    /// admission-webhook floor, a per-cluster overlay resolver
84    /// re-probing a declared path against a cluster-local filesystem
85    /// snapshot) reaches the two-step `root.join` + `exists` + wrap
86    /// dispatch through one call rather than re-inlining the three-
87    /// line block in lockstep with the five wire-up sites.
88    fn probe_declared_entry<P: AsRef<Path>>(
89        &self,
90        path: P,
91        root: &Path,
92        kind: &'static str,
93    ) -> Result<PathBuf, LayoutError> {
94        let full = root.join(path.as_ref());
95        if !self.exists(&full) {
96            return Err(LayoutError::missing_entry(kind, full));
97        }
98        Ok(full)
99    }
100
101    /// Probe an authored path's resolved on-disk location under `root`
102    /// and, on presence, verify the resolved [`PathBuf`] lives inside
103    /// the paired `sandbox_dir` sub-tree; on absence return the paired
104    /// [`LayoutError::MissingEntry`] (via [`Self::probe_declared_entry`])
105    /// and on sandbox escape return `outside_ctor(full)` naming the
106    /// offending resolved path.
107    ///
108    /// Folds the two self-similar `let full = self.probe_declared_entry(
109    /// p, root, <kind>)?; if !full.starts_with(&<slot>_dir) { return
110    /// Err(LayoutError::<Slot>OutsideDir(full)); }` blocks at
111    /// [`Self::verify`] (`:exe` iteration, `:servicos` iteration) onto
112    /// one substrate primitive on [`StandardLayout`]. The two wire-ups
113    /// used the identical two-arm cascade around the peer
114    /// [`Self::probe_declared_entry`] hit-arm hand-off, differing only
115    /// in the three names bound at each site — the `kind` label, the
116    /// resolved `sandbox_dir`, and the paired outside-dir
117    /// tuple-variant constructor (`LayoutError::ExeOutsideDir` /
118    /// `LayoutError::ServicoOutsideDir`) — exactly the shape the PRIME
119    /// DIRECTIVE names as a bug.
120    ///
121    /// Diagnostic order preserved: [`LayoutError::MissingEntry`] fires
122    /// before `outside_ctor` on the same iteration (the pre-lift order
123    /// this primitive's sibling [`Self::probe_declared_entry`]
124    /// docstring documents). Every future consumer that wants to probe
125    /// a declared path against an out-of-band filesystem oracle *and*
126    /// gate the resolved path on a sandbox sub-tree — a per-slot
127    /// admission webhook probing a declared `:exe` / `:servicos` entry
128    /// against a mounted cluster-local filesystem snapshot, a
129    /// per-cluster overlay resolver rejecting a sandbox-escape patch,
130    /// the deferred `caixa.pleme.io/v1alpha1/Caixa` CR materializer's
131    /// admission-webhook floor — reaches the two-arm probe + sandbox
132    /// dispatch through one call rather than re-inlining the three-
133    /// line block in lockstep with the two `verify` wire-up sites.
134    fn probe_sandboxed_declared_entry<P: AsRef<Path>>(
135        &self,
136        path: P,
137        root: &Path,
138        kind: &'static str,
139        sandbox_dir: &Path,
140        outside_ctor: fn(PathBuf) -> LayoutError,
141    ) -> Result<PathBuf, LayoutError> {
142        let full = self.probe_declared_entry(path, root, kind)?;
143        if !full.starts_with(sandbox_dir) {
144            return Err(outside_ctor(full));
145        }
146        Ok(full)
147    }
148
149    /// Probe every path yielded by `paths` against the injected oracle
150    /// under `root`; on the first miss return the paired
151    /// [`LayoutError::MissingEntry`] naming the offending entry with the
152    /// caller-provided canonical `kind` label (via the sibling
153    /// [`Self::probe_declared_entry`] primitive), and on empty / all-hit
154    /// arms return `Ok(())`.
155    ///
156    /// Folds the three self-similar per-slot existence-probe *loop*
157    /// blocks at [`Self::verify`] onto one substrate primitive on
158    /// [`StandardLayout`]:
159    ///
160    /// - `:bibliotecas` iteration — `for p in caixa.bibliotecas() { self.
161    ///   probe_declared_entry(p, root, LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA)?; }`,
162    /// - `:behavior` on-disk callback-path iteration — `for p in
163    ///   b.declared_paths() { self.probe_declared_entry(p, root,
164    ///   LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK)?; }`,
165    /// - `:upgrade-from` per-instruction script-path iteration — the
166    ///   `for entry in caixa.upgrade_from() { for instr in entry.
167    ///   instructions() { if let Some(p) = instr.declared_path() { self.
168    ///   probe_declared_entry(p, root, LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT)?;
169    ///   } } }` nested cascade, flattened at the wire-up site through
170    ///   `.iter().flat_map(_::instructions).filter_map(_::declared_path)`.
171    ///
172    /// Three consumers, three identical `for … { self.probe_declared_entry
173    /// (…, kind)?; }` loop shapes, one substrate primitive on
174    /// [`StandardLayout`] closing the duplication the PRIME DIRECTIVE
175    /// names as a bug — sibling to the peer per-arm-probe
176    /// [`Self::probe_declared_entry`] (fda1e35) and the two-arm-sandboxed
177    /// [`Self::probe_sandboxed_declared_entry`] (4940d55) primitives on
178    /// the same layout-pipeline existence-probe axis. Diagnostic order
179    /// preserved: iteration proceeds in the caller-supplied iterator
180    /// order and short-circuits on the first miss, byte-equal to the
181    /// pre-lift `for … { … ? }` loop's first-error return semantics.
182    /// Every future consumer that wants to sweep a per-slot declared-
183    /// path list against an out-of-band filesystem oracle (a per-slot
184    /// admission webhook probing an entire `:bibliotecas` / `:behavior`
185    /// / `:upgrade-from` slot as a unit against a mounted cluster-local
186    /// filesystem snapshot, a `feira validate --exists` per-caixa
187    /// admission verb, the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
188    /// materializer's admission-webhook floor, a per-cluster overlay
189    /// resolver re-probing a slot-of-paths after a patch) reaches the
190    /// per-slot batch through one call rather than re-inlining the
191    /// three-line loop body in lockstep with the three `verify` wire-up
192    /// sites.
193    fn probe_declared_entries<I, P>(
194        &self,
195        paths: I,
196        root: &Path,
197        kind: &'static str,
198    ) -> Result<(), LayoutError>
199    where
200        I: IntoIterator<Item = P>,
201        P: AsRef<Path>,
202    {
203        for p in paths {
204            self.probe_declared_entry(p, root, kind)?;
205        }
206        Ok(())
207    }
208
209    /// Probe every path yielded by `paths` against the injected oracle
210    /// under `root` and, on presence, verify the resolved path lives
211    /// inside `sandbox_dir` (via the sibling
212    /// [`Self::probe_sandboxed_declared_entry`] primitive); on the first
213    /// miss return the paired [`LayoutError::MissingEntry`] naming the
214    /// offending entry with the caller-provided canonical `kind` label,
215    /// on the first sandbox-escape return `outside_ctor(full)` naming
216    /// the offending resolved path, and on empty / all-hit arms return
217    /// `Ok(())`.
218    ///
219    /// Folds the two self-similar per-slot sandboxed-existence-probe
220    /// *loop* blocks at [`Self::verify`] onto one substrate primitive on
221    /// [`StandardLayout`]:
222    ///
223    /// - `:exe` iteration — `let exe_dir = root.join(LAYOUT_DIR_EXE); for
224    ///   p in caixa.exe() { self.probe_sandboxed_declared_entry(p, root,
225    ///   LAYOUT_MISSING_ENTRY_KIND_EXE, &exe_dir, LayoutError::ExeOutsideDir)?; }`,
226    /// - `:servicos` iteration — `let servicos_dir = root.join(
227    ///   LAYOUT_DIR_SERVICOS); for p in caixa.servicos() {
228    ///   self.probe_sandboxed_declared_entry(p, root,
229    ///   LAYOUT_MISSING_ENTRY_KIND_SERVICO, &servicos_dir,
230    ///   LayoutError::ServicoOutsideDir)?; }`.
231    ///
232    /// Two consumers, two identical `for … { self.probe_sandboxed_declared_entry
233    /// (…, kind, &sandbox_dir, outside_ctor)?; }` loop shapes, one
234    /// substrate primitive on [`StandardLayout`] closing the duplication
235    /// the PRIME DIRECTIVE names as a bug — sibling to the peer per-slot
236    /// batch [`Self::probe_declared_entries`] (d1ccb0b) primitive on the
237    /// non-sandboxed axis and the per-arm [`Self::probe_declared_entry`]
238    /// (fda1e35) / [`Self::probe_sandboxed_declared_entry`] (4940d55)
239    /// primitives on the per-path axis. The four together close the
240    /// layout-pipeline existence-probe algebra: every wire-up on the
241    /// (per-arm | per-slot batch) × (bare | sandboxed) product now
242    /// routes through one substrate primitive rather than N open-coded
243    /// blocks.
244    ///
245    /// Diagnostic order preserved: iteration proceeds in the caller-
246    /// supplied iterator order and short-circuits on the first miss or
247    /// sandbox-escape, byte-equal to the pre-lift `for … { … ? }` loop's
248    /// first-error return semantics. Within each iteration
249    /// [`LayoutError::MissingEntry`] fires before `outside_ctor` (the
250    /// order the peer [`Self::probe_sandboxed_declared_entry`] primitive
251    /// docstring documents). Every future consumer that wants to sweep a
252    /// per-slot declared-path list against an out-of-band filesystem
253    /// oracle *and* gate each resolved path on a sandbox sub-tree (a
254    /// per-slot admission webhook probing an entire `:exe` / `:servicos`
255    /// slot as a unit against a mounted cluster-local filesystem
256    /// snapshot, a `feira validate --exists --sandbox` per-caixa
257    /// admission verb, the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
258    /// materializer's admission-webhook floor, a per-cluster overlay
259    /// resolver re-probing a slot-of-sandboxed-paths after a patch)
260    /// reaches the per-slot batch through one call rather than
261    /// re-inlining the three-line loop body in lockstep with the two
262    /// `verify` wire-up sites.
263    fn probe_sandboxed_declared_entries<I, P>(
264        &self,
265        paths: I,
266        root: &Path,
267        kind: &'static str,
268        sandbox_dir: &Path,
269        outside_ctor: fn(PathBuf) -> LayoutError,
270    ) -> Result<(), LayoutError>
271    where
272        I: IntoIterator<Item = P>,
273        P: AsRef<Path>,
274    {
275        for p in paths {
276            self.probe_sandboxed_declared_entry(p, root, kind, sandbox_dir, outside_ctor)?;
277        }
278        Ok(())
279    }
280}
281
282impl std::fmt::Debug for StandardLayout {
283    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284        f.debug_struct("StandardLayout")
285            .field("custom_exists", &self.path_exists.is_some())
286            .finish()
287    }
288}
289
290impl LayoutInvariants for StandardLayout {
291    fn verify(&self, caixa: &Caixa, root: &Path) -> Result<(), LayoutError> {
292        let manifest = root.join("caixa.lisp");
293        if !self.exists(&manifest) {
294            return Err(LayoutError::MissingManifest(manifest));
295        }
296
297        // Caixa-identity value-shape gates on the two universal axes
298        // (`:nome`, `:versao`) every substrate-side artifact's
299        // `metadata.name` / version derivation flows through. The
300        // [`Caixa::validate_nome`] / [`Caixa::validate_versao`] doc-
301        // comments name the canonical authoring footguns verbatim —
302        // `:nome` "MyApp" / "my_app" / "team.app" / "-app" / "café"
303        // (DNS-1123 violations the K8s apiserver refuses at admission
304        // time on every derived `metadata.name`: `lareira-<nome>`,
305        // programs.yaml entry, CiliumNetworkPolicy / HTTPRoute names,
306        // `LABEL_APLICACAO` value); `:versao` "0.1" / "v0.1.0" / "latest"
307        // / "^0.1" / "0.1.0.0" (SemVer-2 violations Helm / OCI tag /
308        // `feira publish` git tag / lacre `concrete_versao` /
309        // `:upgrade-from :from` peer matching each refuse downstream).
310        // Until this wire-up landed both validators existed as `pub fn`
311        // on [`Caixa`] (with full per-arm test coverage in
312        // `manifest::tests`) but no production code path called them —
313        // `feira build` (the canonical author-time gate) silently
314        // accepted a malformed `:nome` / `:versao` and the failure
315        // surfaced at `helm install` / `kubectl apply` / `feira publish`
316        // time on the *first* downstream consumer to strict-parse the
317        // value, far from the source `caixa.lisp` and without any field
318        // naming the offending Caixa identity axis. The gate runs
319        // *after* [`LayoutError::MissingManifest`] (no caixa to check
320        // when the manifest is missing) and *before* every kind-coherence
321        // gate (each of which carries `caixa.nome` verbatim in its
322        // diagnostic — running them first on a structurally-invalid
323        // identity would surface a "this kind has slot X" diagnostic
324        // against an unrecoverable name). Cross-axis precedence is
325        // `:nome` → `:versao` — the canonical declaration order on
326        // [`Caixa`] and the same author-grep ordering the
327        // [`ManifestError`] family uses. Same per-axis `*Violation
328        // { caixa, issue }` envelope every peer per-axis wrap exposes
329        // ([`LayoutError::CodePathViolation`] b868442,
330        // [`LayoutError::LimitsViolation`] / [`LayoutError::BehaviorViolation`]
331        // / [`LayoutError::UpgradeViolation`] / [`LayoutError::SupervisorViolation`]
332        // / [`LayoutError::AplicacaoViolation`]).
333        caixa.run_layout_gate(Caixa::validate_nome, LayoutError::nome_violation)?;
334        // `:nome`-side joint-length budget on the canonical
335        // `lareira-<nome>` chart-name shape — the second arm on the
336        // shared `:nome` axis after the bare-DNS-1123 gate above. Runs
337        // through the same [`LayoutError::NomeViolation`] envelope so
338        // every per-axis diagnostic on `:nome` carries one wrap shape,
339        // peer with the [`Caixa::validate_nome`] → `NomeInvalid`
340        // routing already at this site. The chart-name budget is the
341        // second-axis ceiling [`Caixa::validate_nome`] cannot see — a
342        // 56-byte DNS-1123-valid `:nome` passes the bare-`:nome` shape
343        // but produces a 64-byte `lareira-<nome>` chart name the
344        // apiserver / `helm lint` rejects at admission, far from the
345        // source `caixa.lisp` and naming none of the joint-length
346        // overflow's three carriers (DNS-1123 cap, prefix, `:nome`
347        // length). Closing it at this wire-up turns the
348        // [`lareira_chart_name`] doc-comment's explicit M4-admission
349        // deferral (caixa-core/src/render.rs:3198) into a build-time
350        // structural property of every emitted artifact.
351        caixa.run_layout_gate(
352            Caixa::validate_nome_chart_name_budget,
353            LayoutError::nome_violation,
354        )?;
355        caixa.run_layout_gate(Caixa::validate_versao, LayoutError::versao_violation)?;
356
357        // `:deps` / `:deps-dev` per-entry shape gate. The third Caixa-
358        // level orphan validator on the universal authoring surface (peer
359        // of [`Caixa::validate_nome`] / [`Caixa::validate_versao`] wired
360        // immediately above): [`Caixa::validate_deps`] walks every
361        // [`Dep::validate`] arm — empty / non-DNS-1123 `:nome`, empty /
362        // unparseable `:versao` requirement, malformed `:fonte` repo /
363        // pin / `:caminho`, malformed `:caracteristicas` Cargo-feature
364        // name (de68c0c) — and then closes the per-list set-not-multiset
365        // duplicate-`:nome` invariant on each of `:deps` and `:deps-dev`
366        // (359fba5). Until this wire-up landed `validate_deps` existed as
367        // `pub fn` on [`Caixa`] with full per-arm unit coverage in
368        // `manifest::tests` + `dep::tests` (validate_deps_rejects_*,
369        // 53 dep-axis tests) but no production code path called it —
370        // `feira build` (the canonical author-time gate;
371        // `caixa-feira/src/cmd/build.rs:29` routes through
372        // `StandardLayout::verify`) silently accepted a malformed `:deps`
373        // entry and the failure surfaced at the *first* downstream
374        // consumer to strict-parse it: at lacre-resolve time as a
375        // `semver::Error` not naming the offending dep (`:versao` per-
376        // entry); at `git clone` time as a fetch failure quoting the
377        // shell-escape `repo` (`:fonte :repo`); at the resolver's
378        // `HashMap<:nome>` collapse as a silent "second-wins" overwrite
379        // (within-list `:nome` duplicate); at `cargo metadata` time as a
380        // feature-name rejection on the *target* caixa rather than the
381        // dep entry referencing it (`:caracteristicas`); at `helm
382        // install` / `kubectl apply` time as an apiserver `metadata.name`
383        // rejection on the rendered `lareira-<nome>` chart's per-dep
384        // derivation (DNS-1123-violating `:deps :nome`) — each far from
385        // the source `caixa.lisp`, none naming the offending `:deps` /
386        // `:deps-dev` axis. Runs *after* the Caixa-identity gates (the
387        // diagnostic carries `caixa.nome().to_string()` verbatim, which the
388        // peer [`Caixa::validate_nome`] gate above has just guaranteed is
389        // a valid DNS-1123 label) and *before* every kind-coherence gate
390        // (the dep surface is universal — every kind has `:deps` /
391        // `:deps-dev` — so its shape diagnostic is more fundamental than
392        // the kind-coherence partitions on `:bibliotecas` / `:exe` /
393        // `:servicos` / `:membros` / `:children` / M2 slots that follow).
394        // Same per-axis `*Violation { caixa, issue }` envelope every peer
395        // per-axis wrap exposes ([`LayoutError::NomeViolation`] /
396        // [`LayoutError::VersaoViolation`] (1f74a5f),
397        // [`LayoutError::CodePathViolation`] (b868442),
398        // [`LayoutError::LimitsViolation`] / [`LayoutError::BehaviorViolation`]
399        // / [`LayoutError::UpgradeViolation`] / [`LayoutError::SupervisorViolation`]
400        // / [`LayoutError::AplicacaoViolation`]). Threads [`DepError`]
401        // Display through verbatim — every per-arm reason already names
402        // the offending dep's `:nome` (e.g. `":deps entry "caixa-teia"
403        // :versao "^bad" is not a valid semver requirement: …"`), so the
404        // wrap envelope's `issue` carries a self-locating "which dep,
405        // which axis, why" without re-shaping the per-arm parser-side
406        // reason. With this wire-up the canonical author-time gate
407        // refuses every ill-formed `:deps` / `:deps-dev` value-shape by
408        // construction — closing the second-to-last orphan-validator gap
409        // on the typed Caixa surface (`validate_restart_window` is the
410        // remaining orphan, Supervisor-axis specific and wired into the
411        // Supervisor branch below alongside `view.validate()`).
412        // Compound per-Caixa entry gate on the dep-graph axis: the
413        // layout pipeline's two-dispatch `:deps` / `:deps-dev` cascade
414        // — the per-entry + within-list duplicate-`:nome` gate (the
415        // [`crate::Dep::validate`] + [`crate::render::insert_first_seen`]
416        // cascade `Caixa::validate_deps` opened on, 359fba5) and the
417        // cross-slot self-edge gate
418        // ([`crate::dep::validate_no_self_dep`], ad4abf1) — folded
419        // onto the [`crate::Caixa::validate_deps`] substrate primitive.
420        // The two arms run in the same canonical order at the primitive
421        // (per-entry + cross-entry duplicate → cross-slot self-edge) so
422        // the fold is byte-for-byte equivalent to the pre-fold
423        // two-block cascade this call site formerly carried, pinned by
424        // the paired
425        // `validate_deps_folds_{per_entry,self_edge}_arm_matches_gate`
426        // equivalence pins and the
427        // `validate_deps_per_entry_arm_fires_before_self_edge_arm`
428        // ordering pin in the [`crate::Caixa::validate_deps`] pin
429        // family (`manifest.rs`).
430        //
431        // Same lift discipline the peer per-slot compound gates
432        // ([`crate::AplicacaoSpec::validate_contratos`] and its
433        // `:membros` / `:entrada` / `:placement` / `:politicas` peers,
434        // [`crate::MeshPolicy::validate`],
435        // [`crate::SupervisorSpec::validate_children`],
436        // [`crate::Caixa::validate_upgrade_from`] d6801df) each carry —
437        // one named substrate-primitive gate per typed slot folds every
438        // structural + cross-slot axis on that slot onto one call, so
439        // every future consumer that wants to re-check the dep-graph
440        // after a per-entry patch (the deferred
441        // `caixa.pleme.io/v1alpha1/Caixa` CR materializer's admission
442        // webhook, a future `feira validate --deps` per-caixa admission
443        // verb, a per-`:deps` overlay resolver) reaches the two-arm
444        // compound gate through one dispatch rather than re-inlining
445        // the two-dispatch cascade in lockstep with this wire-up.
446        caixa.run_layout_gate(Caixa::validate_deps, LayoutError::deps_violation)?;
447
448        // `:etiquetas` per-entry empty + cross-entry duplicate gate. The
449        // fourth universal-axis Caixa-level value-shape gate (peer of
450        // [`Caixa::validate_nome`] / [`Caixa::validate_versao`] /
451        // [`Caixa::validate_deps`] wired immediately above and
452        // [`Caixa::validate_code_paths`] wired below the kind-coherence
453        // gates) on the typed Caixa surface. `:etiquetas` is the
454        // registry-search-tag axis every kind carries (universal
455        // `Vec<String>` slot on [`Caixa`]) and lands verbatim as the
456        // Helm chart `Chart.yaml` `keywords:` array on every Servico
457        // (`caixa-helm/src/lib.rs:236` folds it through a
458        // [`std::collections::BTreeSet`]). Until this wire-up landed
459        // `:etiquetas` had no shape gate at any layer — an empty entry
460        // (`(:etiquetas (""))` — the canonical paste-from-blank-doc
461        // footgun) silently rendered as `keywords: [""]` in `Chart.yaml`,
462        // and duplicate entries (`(:etiquetas ("demo" "demo"))` — the
463        // copy-paste-the-wrong-tag footgun) were silently dedup'd by
464        // the renderer's `BTreeSet` collect — a "second wins / one
465        // silently disappears" shape divergent from every peer typed-
466        // graph set gate (`:membros :caixa`, `:placement :clusters`,
467        // `:entrada :paths`, `:contratos`, `:deps :nome`,
468        // `:upgrade-from :from`, the per-instruction-class singularity
469        // gates on `:upgrade-from :instructions`). Runs *after* the
470        // peer universal `:nome` / `:versao` / `:deps` gates (declaration
471        // order on [`Caixa`] is `:nome` → `:versao` → `:edicao` →
472        // `:descricao` → `:repositorio` → `:licenca` → `:autores` →
473        // `:etiquetas` → `:deps` → `:deps-dev`, but the gate order
474        // follows the same identity-axis-first cascade the peer gates
475        // establish: `:nome` → `:versao` are the load-bearing identity
476        // axes that flow into every diagnostic's caixa prefix, and
477        // `:deps` is the universal dep surface that dominates every
478        // kind-coherence gate; `:etiquetas` runs after this trio so the
479        // diagnostic carries an already-validated `:nome` and the
480        // peer universal axes' narrower diagnostics surface first when
481        // multiple axes are malformed) and *before* the kind-coherence
482        // gates ([`Self::MeshSlotsOnNonAplicacao`] /
483        // [`Self::SupervisorSlotsOnNonSupervisor`] /
484        // [`Self::ServicoSlotsOnNonServico`] / [`Self::ForeignCodeSlot`])
485        // — `:etiquetas` is universal so its shape diagnostic is more
486        // fundamental than the kind-coherence partitions on kind-
487        // exclusive slot sets.
488        //
489        // Same per-axis `*Violation { caixa, issue }` envelope every peer
490        // per-axis wrap exposes ([`Self::NomeViolation`] /
491        // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
492        // aa77d0f, [`Self::CodePathViolation`] b868442,
493        // [`Self::RestartWindowViolation`] 10e321a). Threads
494        // [`ManifestError::EtiquetaEmpty`] / [`ManifestError::EtiquetaDuplicate`]
495        // Display through verbatim — each per-arm reason already names
496        // the offending tag (for the duplicate arm) or the structural
497        // "empty entry" defect (for the empty arm), so the wrap
498        // envelope's `issue` carries a self-locating "which axis, which
499        // entry, why" without re-shaping the per-arm reason.
500        caixa.run_layout_gate(Caixa::validate_etiquetas, LayoutError::etiquetas_violation)?;
501
502        // `:autores` per-entry empty + cross-entry duplicate gate. The
503        // fifth universal-axis Caixa-level value-shape gate (peer of
504        // [`Caixa::validate_nome`] / [`Caixa::validate_versao`] /
505        // [`Caixa::validate_deps`] / [`Caixa::validate_etiquetas`] wired
506        // immediately above and [`Caixa::validate_code_paths`] wired
507        // below the kind-coherence gates) on the typed Caixa surface.
508        // `:autores` is the maintainer-axis every kind carries
509        // (universal `Vec<String>` slot on [`Caixa`]) and lands verbatim
510        // as the Helm chart `Chart.yaml` `maintainers:` array on every
511        // Servico (`caixa-helm/src/lib.rs:251` maps each entry to a
512        // `Maintainer { name, email: None }` without dedup). Until this
513        // wire-up landed `:autores` had no shape gate at any layer — an
514        // empty entry (`(:autores (""))` — the canonical paste-from-
515        // blank-doc footgun) silently rendered as
516        // `maintainers: [{name: "", email: null}]` in `Chart.yaml`, and
517        // duplicate entries (`(:autores ("pleme-io" "pleme-io"))` —
518        // the copy-paste-the-wrong-author footgun) stacked verbatim in
519        // the chart. Unlike the peer `:etiquetas` axis (where the
520        // renderer's `BTreeSet` collect silently dedups the `keywords:`
521        // array at chart render — a "second wins / one silently
522        // disappears" shape), `maintainers:` has *no* renderer-side
523        // dedup, so duplicate `:autores` entries render as two identical
524        // maintainer records by construction — a strictly worse footgun
525        // than the peer `:etiquetas` shape. Runs *after* the peer
526        // universal `:nome` / `:versao` / `:deps` / `:etiquetas` gates
527        // (the gate order follows the canonical identity-axis-first
528        // cascade the peer gates establish; `:autores` and `:etiquetas`
529        // are the two Vec-shaped universal metadata axes — they sit
530        // adjacent in the cascade after the load-bearing identity +
531        // dep trio) and *before* the kind-coherence gates
532        // ([`Self::MeshSlotsOnNonAplicacao`] /
533        // [`Self::SupervisorSlotsOnNonSupervisor`] /
534        // [`Self::ServicoSlotsOnNonServico`] / [`Self::ForeignCodeSlot`])
535        // — `:autores` is universal so its shape diagnostic is more
536        // fundamental than the kind-coherence partitions on kind-
537        // exclusive slot sets.
538        //
539        // Same per-axis `*Violation { caixa, issue }` envelope every peer
540        // per-axis wrap exposes ([`Self::NomeViolation`] /
541        // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
542        // aa77d0f, [`Self::EtiquetasViolation`] 360a499,
543        // [`Self::CodePathViolation`] b868442,
544        // [`Self::RestartWindowViolation`] 10e321a). Threads
545        // [`ManifestError::AutorEmpty`] / [`ManifestError::AutorDuplicate`]
546        // Display through verbatim — each per-arm reason already names
547        // the offending author (for the duplicate arm) or the structural
548        // "empty entry" defect (for the empty arm), so the wrap
549        // envelope's `issue` carries a self-locating "which axis, which
550        // entry, why" without re-shaping the per-arm reason.
551        caixa.run_layout_gate(Caixa::validate_autores, LayoutError::autores_violation)?;
552
553        // `:repositorio` git-repo-URL shape gate. The sixth
554        // universal-axis Caixa-level value-shape gate (peer of
555        // [`Caixa::validate_nome`] / [`Caixa::validate_versao`] /
556        // [`Caixa::validate_deps`] / [`Caixa::validate_etiquetas`] /
557        // [`Caixa::validate_autores`] wired immediately above and
558        // [`Caixa::validate_code_paths`] wired below the kind-coherence
559        // gates) on the typed Caixa surface. `:repositorio` is the
560        // universal git-shaped homepage axis every kind carries
561        // (universal `Option<String>` slot on [`Caixa`]) and routes
562        // through two load-bearing substrate consumers:
563        // [`caixa-helm`] folds it verbatim into the rendered
564        // `lareira-<nome>` Helm chart's `Chart.yaml` `home:` field
565        // (`build_chart_yaml` at `caixa-helm/src/lib.rs:268`) and into
566        // the chart `README.md` `repo = …` interpolation
567        // (`caixa-helm/src/lib.rs:359`); [`caixa-flux`] folds it
568        // verbatim into the standalone `ClusterBundleOpts::for_caixa`
569        // `git_url:` field (`caixa-flux/src/lib.rs:293`), which
570        // becomes the FluxCD `GitRepository.spec.url` the cluster's
571        // source-controller polls — the load-bearing deploy-time axis.
572        // Both consumers use `Option::unwrap_or_else(|| <fallback>)`
573        // to substitute a placeholder when the slot is absent (`None`
574        // → the fallback fires); a `Some("")` *skips the fallback*
575        // and silently passes the empty string through to
576        // `Chart.yaml home: ""` / `GitRepository url: ""`. Until this
577        // wire-up landed `:repositorio` had no shape gate at any
578        // layer — empty (`(:repositorio "")` — the canonical
579        // paste-from-blank-doc footgun) and malformed (whitespace,
580        // control char / CRLF, leading `-` CLI-arg-injection,
581        // missing `:` separator) values silently landed in the
582        // rendered artifacts and broke at `helm template` / FluxCD
583        // reconcile time far from the source `caixa.lisp`.
584        //
585        // Runs *after* the peer universal `:nome` / `:versao` /
586        // `:deps` / `:etiquetas` / `:autores` gates (the gate order
587        // follows the canonical identity-axis-first cascade the peer
588        // gates establish; `:repositorio` is the universal git-URL
589        // axis — it sits adjacent to `:autores` in the cascade after
590        // the load-bearing identity + dep trio + the two Vec-shaped
591        // universal metadata axes) and *before* the kind-coherence
592        // gates ([`Self::MeshSlotsOnNonAplicacao`] /
593        // [`Self::SupervisorSlotsOnNonSupervisor`] /
594        // [`Self::ServicoSlotsOnNonServico`] / [`Self::ForeignCodeSlot`])
595        // — `:repositorio` is universal so its shape diagnostic is
596        // more fundamental than the kind-coherence partitions on
597        // kind-exclusive slot sets.
598        //
599        // Same per-axis `*Violation { caixa, issue }` envelope every
600        // peer per-axis wrap exposes ([`Self::NomeViolation`] /
601        // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
602        // aa77d0f, [`Self::EtiquetasViolation`] 360a499,
603        // [`Self::AutoresViolation`] 86c769b, [`Self::CodePathViolation`]
604        // b868442, [`Self::RestartWindowViolation`] 10e321a). Threads
605        // [`ManifestError::RepositorioEmpty`] /
606        // [`ManifestError::RepositorioInvalid`] Display through
607        // verbatim — each per-arm reason already names the offending
608        // `:repositorio` value (for the invalid arm) or the
609        // structural "empty entry" defect (for the empty arm), so
610        // the wrap envelope's `issue` carries a self-locating "which
611        // axis, which value, why" without re-shaping the per-arm
612        // reason. With this gate the two `git URL`-shaped surfaces on
613        // the typed Caixa (`:repositorio` here, `:deps :fonte :repo`
614        // peer routed through the same shared
615        // [`crate::render::is_git_repo_url`] predicate via
616        // [`crate::DepSource::validate`]) are now structurally
617        // equivalent — every value past validate is
618        // guaranteed-acceptable by the shared predicate's constraint
619        // union, by construction.
620        caixa.run_layout_gate(
621            Caixa::validate_repositorio,
622            LayoutError::repositorio_violation,
623        )?;
624
625        // `:descricao` non-empty shape gate. The seventh universal-
626        // axis Caixa-level value-shape gate (peer of
627        // [`Caixa::validate_nome`] / [`Caixa::validate_versao`] /
628        // [`Caixa::validate_deps`] / [`Caixa::validate_etiquetas`] /
629        // [`Caixa::validate_autores`] / [`Caixa::validate_repositorio`]
630        // wired immediately above and [`Caixa::validate_code_paths`]
631        // wired below the kind-coherence gates) on the typed Caixa
632        // surface. `:descricao` is the universal free-form-prose
633        // summary axis every kind carries (universal `Option<String>`
634        // slot on [`Caixa`]) and routes through two load-bearing
635        // [`caixa-helm`] consumers: `build_chart_yaml` folds it
636        // verbatim into the rendered `lareira-<nome>` Helm chart's
637        // `Chart.yaml` `description:` field
638        // (`caixa-helm/src/lib.rs:232-235`), and `build_readme` folds
639        // it verbatim into the chart `README.md` header
640        // (`caixa-helm/src/lib.rs:333-336`). Both consumers use
641        // `Option::unwrap_or_else(|| <fallback>)` to substitute a
642        // `caixa.nome`-derived placeholder when the slot is absent
643        // (`None` → the fallback fires); a `Some("")` *skips the
644        // fallback* and silently passes the empty string through to
645        // `Chart.yaml description: ""` / a blank `README.md` header
646        // — exact same footgun shape as the peer `:repositorio`
647        // surface above. Until this wire-up landed `:descricao` had
648        // no shape gate at any layer — the empty
649        // (`(:descricao "")` — the canonical paste-from-blank-doc
650        // footgun) silently landed in the rendered artifacts and
651        // broke at `helm lint` time (`WARNING [chart.metadata.description]:
652        // description is required` on `apiVersion: v2` charts) far
653        // from the source `caixa.lisp`.
654        //
655        // Runs *after* the peer universal `:nome` / `:versao` /
656        // `:deps` / `:etiquetas` / `:autores` / `:repositorio` gates
657        // (the gate order follows the canonical identity-axis-first
658        // cascade the peer gates establish; `:descricao` is the
659        // universal free-form-prose axis — it sits adjacent to
660        // `:repositorio` in the cascade after the load-bearing
661        // identity + dep trio + the two Vec-shaped universal
662        // metadata axes + the universal git-URL axis) and *before*
663        // the kind-coherence gates ([`Self::MeshSlotsOnNonAplicacao`]
664        // / [`Self::SupervisorSlotsOnNonSupervisor`] /
665        // [`Self::ServicoSlotsOnNonServico`] /
666        // [`Self::ForeignCodeSlot`]) — `:descricao` is universal so
667        // its shape diagnostic is more fundamental than the kind-
668        // coherence partitions on kind-exclusive slot sets.
669        //
670        // Same per-axis `*Violation { caixa, issue }` envelope every
671        // peer per-axis wrap exposes ([`Self::NomeViolation`] /
672        // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
673        // aa77d0f, [`Self::EtiquetasViolation`] 360a499,
674        // [`Self::AutoresViolation`] 86c769b,
675        // [`Self::RepositorioViolation`] 577b0a9,
676        // [`Self::CodePathViolation`] b868442,
677        // [`Self::RestartWindowViolation`] 10e321a). Threads
678        // [`ManifestError::DescricaoEmpty`] Display through verbatim
679        // — the per-arm reason already names the offending
680        // `:descricao` slot + cites the renderer-side footgun, so
681        // the wrap envelope's `issue` carries a self-locating
682        // "which axis, why" without re-shaping the per-arm reason.
683        caixa.run_layout_gate(Caixa::validate_descricao, LayoutError::descricao_violation)?;
684
685        // `:licenca` non-empty shape gate. The eighth universal-axis
686        // Caixa-level value-shape gate (peer of [`Caixa::validate_nome`]
687        // / [`Caixa::validate_versao`] / [`Caixa::validate_deps`] /
688        // [`Caixa::validate_etiquetas`] / [`Caixa::validate_autores`] /
689        // [`Caixa::validate_repositorio`] / [`Caixa::validate_descricao`]
690        // wired immediately above and [`Caixa::validate_code_paths`]
691        // wired below the kind-coherence gates) on the typed Caixa
692        // surface. `:licenca` is the universal SPDX-shaped license-
693        // expression axis every kind carries (universal `Option<String>`
694        // slot on [`Caixa`]) and routes through one load-bearing
695        // [`caixa-helm`] consumer: `build_readme` folds it verbatim into
696        // the rendered `lareira-<nome>` Helm chart's `README.md` `##
697        // License` section (`caixa-helm/src/lib.rs:361`) via
698        // `caixa.licenca.clone().unwrap_or_else(|| "MIT".into())`. The
699        // consumer's fallback only fires when the slot is absent (`None`
700        // → the `MIT` fallback fires); a `Some("")` *skips the
701        // fallback* and silently passes the empty string through to a
702        // chart `README.md` whose `License` section renders as a bare
703        // trailing period — exact same footgun shape as the peer
704        // `:repositorio` (577b0a9) and `:descricao` (4e6db38) surfaces
705        // above. Until this wire-up landed `:licenca` had no shape
706        // gate at any layer — the empty (`(:licenca "")` — the
707        // canonical paste-from-blank-doc footgun) silently landed in
708        // the rendered chart `README.md` far from the source
709        // `caixa.lisp`.
710        //
711        // Runs *after* the peer universal `:nome` / `:versao` /
712        // `:deps` / `:etiquetas` / `:autores` / `:repositorio` /
713        // `:descricao` gates (the gate order follows the canonical
714        // identity-axis-first cascade the peer gates establish;
715        // `:licenca` sits adjacent to `:descricao` in the cascade
716        // after the load-bearing identity + dep trio + the two
717        // Vec-shaped universal metadata axes + the universal
718        // git-URL + free-form-prose axes) and *before* the kind-
719        // coherence gates ([`Self::MeshSlotsOnNonAplicacao`] /
720        // [`Self::SupervisorSlotsOnNonSupervisor`] /
721        // [`Self::ServicoSlotsOnNonServico`] /
722        // [`Self::ForeignCodeSlot`]) — `:licenca` is universal so
723        // its shape diagnostic is more fundamental than the kind-
724        // coherence partitions on kind-exclusive slot sets.
725        //
726        // Same per-axis `*Violation { caixa, issue }` envelope every
727        // peer per-axis wrap exposes ([`Self::NomeViolation`] /
728        // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
729        // aa77d0f, [`Self::EtiquetasViolation`] 360a499,
730        // [`Self::AutoresViolation`] 86c769b,
731        // [`Self::RepositorioViolation`] 577b0a9,
732        // [`Self::DescricaoViolation`] 4e6db38,
733        // [`Self::CodePathViolation`] b868442,
734        // [`Self::RestartWindowViolation`] 10e321a). Threads
735        // [`ManifestError::LicencaEmpty`] Display through verbatim
736        // — the per-arm reason already names the offending
737        // `:licenca` slot + cites the renderer-side footgun, so
738        // the wrap envelope's `issue` carries a self-locating
739        // "which axis, why" without re-shaping the per-arm reason.
740        caixa.run_layout_gate(Caixa::validate_licenca, LayoutError::licenca_violation)?;
741
742        // `:edicao` non-empty shape gate. The ninth (and last
743        // un-gated) universal-axis Caixa-level value-shape gate
744        // (peer of [`Caixa::validate_nome`] / [`Caixa::validate_versao`]
745        // / [`Caixa::validate_deps`] / [`Caixa::validate_etiquetas`] /
746        // [`Caixa::validate_autores`] / [`Caixa::validate_repositorio`]
747        // / [`Caixa::validate_descricao`] / [`Caixa::validate_licenca`]
748        // wired immediately above and [`Caixa::validate_code_paths`]
749        // wired below the kind-coherence gates) on the typed Caixa
750        // surface. `:edicao` is the universal language-edition axis
751        // every kind carries (universal `Option<String>` slot on
752        // [`Caixa`]) that selects the tatara-lisp macro surface +
753        // compatibility flags the substrate applies when building
754        // the caixa. The canonical [`Caixa::template`] scaffold every
755        // `feira init` emits carries `:edicao "2026"` verbatim
756        // (`caixa-core/src/manifest.rs:1193`) and every renderer-side
757        // fixture carries `edicao: Some("2026".into())` by
758        // construction (`caixa-helm/src/lib.rs:375`,
759        // `caixa-flux/src/lib.rs:445`, `caixa-mesh/src/lib.rs:629`,
760        // `caixa-core/src/render.rs:2510`). Until this wire-up landed
761        // `:edicao` had no shape gate at any layer — the empty
762        // (`(:edicao "")` — the canonical paste-from-blank-doc
763        // footgun) silently landed as a bare `(:edicao "")` line
764        // in the rendered caixa.lisp and a future renderer-side
765        // consumer that folds the value through
766        // `Option::unwrap_or_else` would skip its fallback (which
767        // only fires on `None`) and pass the empty edition through
768        // to the substrate's build-time edition selector far from
769        // the source `caixa.lisp` — exact same
770        // `Some("")`-skips-`unwrap_or_else` footgun shape as the
771        // peer `:repositorio` (577b0a9), `:descricao` (4e6db38),
772        // and `:licenca` (3d1e535) surfaces above.
773        //
774        // Runs *after* the peer universal `:nome` / `:versao` /
775        // `:deps` / `:etiquetas` / `:autores` / `:repositorio` /
776        // `:descricao` / `:licenca` gates (the gate order follows
777        // the canonical identity-axis-first cascade the peer gates
778        // establish; `:edicao` sits at the tail of the cascade
779        // after the load-bearing identity + dep trio + the two
780        // Vec-shaped universal metadata axes + the three universal
781        // `Option<String>` chart-metadata axes) and *before* the
782        // kind-coherence gates ([`Self::MeshSlotsOnNonAplicacao`] /
783        // [`Self::SupervisorSlotsOnNonSupervisor`] /
784        // [`Self::ServicoSlotsOnNonServico`] /
785        // [`Self::ForeignCodeSlot`]) — `:edicao` is universal so
786        // its shape diagnostic is more fundamental than the kind-
787        // coherence partitions on kind-exclusive slot sets.
788        //
789        // Same per-axis `*Violation { caixa, issue }` envelope every
790        // peer per-axis wrap exposes ([`Self::NomeViolation`] /
791        // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
792        // aa77d0f, [`Self::EtiquetasViolation`] 360a499,
793        // [`Self::AutoresViolation`] 86c769b,
794        // [`Self::RepositorioViolation`] 577b0a9,
795        // [`Self::DescricaoViolation`] 4e6db38,
796        // [`Self::LicencaViolation`] 3d1e535,
797        // [`Self::CodePathViolation`] b868442,
798        // [`Self::RestartWindowViolation`] 10e321a). Threads
799        // [`ManifestError::EdicaoEmpty`] Display through verbatim
800        // — the per-arm reason already names the offending
801        // `:edicao` slot + cites the renderer-side footgun, so
802        // the wrap envelope's `issue` carries a self-locating
803        // "which axis, why" without re-shaping the per-arm reason.
804        // With this gate every universal-axis `Option<String>`
805        // surface on the typed Caixa (`:repositorio` 577b0a9,
806        // `:descricao` 4e6db38, `:licenca` 3d1e535, `:edicao` here)
807        // now carries the same structural empty-arm gate by
808        // construction.
809        caixa.run_layout_gate(Caixa::validate_edicao, LayoutError::edicao_violation)?;
810
811        // Kind ↔ code-surface coherence on the three no-code kinds
812        // (Supervisor / Aplicacao / Acao) — folded onto one substrate
813        // primitive at [`crate::Caixa::validate_no_code_kind_coherence`].
814        // Pre-lift each of the three arms lived as a self-similar
815        // `if caixa.kind().is_<no-code-kind>() && has_code { return
816        // Err(LayoutError::<kind>_owns_code(caixa)); }` block at this
817        // call site — three consumers, three identical shapes, one
818        // substrate primitive on [`Caixa`] closing the duplication the
819        // PRIME DIRECTIVE names as a bug. Mirror of the sibling
820        // [`crate::Caixa::validate_kind_slot_coherence`] fold f0d286e
821        // on the author-time typed-slot coherence axis: this wire-up
822        // closes the same three-arm cascade on the reciprocal
823        // code-surface axis, so the layout pipeline now routes both
824        // "no-code kind declares typed slots" and "no-code kind
825        // declares code" author-time footgun families through one
826        // substrate primitive per axis rather than six open-coded
827        // blocks. Each of the three inner ctors
828        // ([`crate::LayoutError::supervisor_owns_code`] /
829        // [`crate::LayoutError::aplicacao_owns_code`] /
830        // [`crate::LayoutError::acao_owns_code`]) was already lifted
831        // onto the substrate by the peer [`layout_nome_only_ctors!`]
832        // macro, so the primitive routes through the same
833        // `Self::<variant>(caixa.nome().to_string())` tuple-literal
834        // wrap per arm as the pre-lift open-coded blocks. Runs
835        // BEFORE the path-existence loops below so a no-code kind
836        // that declares code surfaces the self-locating OwnsCode
837        // diagnostic naming the offending kind rather than a
838        // downstream `MissingEntry` / `ExeOutsideDir` /
839        // `ServicoOutsideDir` against the resolved path far from the
840        // source `caixa.lisp`.
841        caixa.validate_no_code_kind_coherence()?;
842
843        // Kind ↔ typed-slot coherence on the M3 mesh / supervisor-tree /
844        // M2 Servico-runtime slot families — folded onto one substrate
845        // primitive at [`crate::Caixa::validate_kind_slot_coherence`].
846        // Pre-lift each of the three arms lived as a self-similar
847        // five-line `if !caixa.kind().is_<owner>() { let slots =
848        // caixa.declared_<family>_slots(); if !slots.is_empty() { return
849        // Err(LayoutError::<family>_on_non_<owner>(caixa, slots)); } }`
850        // block at this call site — three consumers, three identical
851        // shapes, one substrate primitive on [`Caixa`] closing the
852        // duplication the PRIME DIRECTIVE names as a bug. The primitive
853        // preserves the pre-fold canonical diagnostic order — mesh →
854        // supervisor → servico — pinned by the load-bearing
855        // `validate_kind_slot_coherence_mesh_arm_fires_before_supervisor_arm`
856        // / `_supervisor_arm_fires_before_servico_arm` ordering pins at
857        // caixa-core/src/manifest.rs, so this wire-up is byte-for-byte
858        // equivalent to the pre-fold three-block cascade on every fixture
859        // exercising any of the three arms. Peer with the per-slot
860        // compound entry gates the substrate already carries
861        // ([`crate::Caixa::validate_deps`] b5dd55e,
862        // [`crate::Caixa::validate_limits`] baa4688,
863        // [`crate::Caixa::validate_behavior`] 0d2877a,
864        // [`crate::Caixa::validate_upgrade_from`] d6801df,
865        // [`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
866        // [`crate::Caixa::validate_supervisor_shape`] 4c70105,
867        // [`crate::Caixa::validate_acao_shape`] 5d6df54) — the
868        // author-time gate axis on the per-slot algebra now shares one
869        // substrate primitive per compound gate, and this lift closes
870        // the symmetric axis on the cross-family kind ↔ slot coherence
871        // algebra so the layout pipeline routes the three self-similar
872        // gates through one substrate primitive rather than three
873        // open-coded blocks. The sibling
874        // [`LayoutError::ForeignCodeSlot`] (code-surface family) and
875        // [`LayoutError::CiOnNonAcao`] (`:ci` axis) gates stay
876        // open-coded downstream: the former bakes the kind-check into
877        // its declared_*_slots helper by design (so it carries no outer
878        // `if !kind().is_<owner>()` guard), the latter carries a
879        // distinct `{ caixa, kind }` wrap shape (no `slots` field —
880        // `:ci` is a single `Option`, not a `Vec`-of-named-slots) whose
881        // reshape onto the uniform `{ caixa, kind, slots }` envelope a
882        // future symmetry lift can join here.
883        caixa.validate_kind_slot_coherence()?;
884
885        // Kind ↔ `:ci` coherence (mirror of the three arms above on
886        // the M3 mesh / supervisor-tree / M2 Servico-runtime slot
887        // families, CANTEIRO §7.1-C, folded onto one substrate
888        // primitive at [`crate::Caixa::validate_ci_kind_coherence`]):
889        // `:ci` carries a typed CI run — a
890        // [`canteiro_types::CiRun`] — that only the caixa-actions
891        // renderer decomposes + validates, and only for a `:kind
892        // Acao`. On any *other* kind a declared `:ci` is the
893        // manifest field's documented "ignored otherwise": it
894        // silently passes verify and then vanishes (never
895        // decomposed, never rendered), far from the source
896        // `caixa.lisp`. Pre-lift the arm lived as a self-similar
897        // `if caixa.ci().is_some() && !caixa.kind().is_acao() { …
898        // return Err(LayoutError::CiOnNonAcao { … }); }` block at
899        // this call site — one consumer today but every future
900        // consumer that wanted to gate this coherence axis as a
901        // unit was structurally forced to re-inline the two-
902        // condition guard in lockstep with this wire-up (the
903        // duplication the PRIME DIRECTIVE names as a bug).
904        // Post-fold the arm reads through one call, and the
905        // [`crate::LayoutError::CiOnNonAcao`] envelope (with its
906        // distinct `{ caixa, kind }` wrap shape — no `slots`
907        // field, because `:ci` is a single `Option` not a `Vec`-
908        // of-named-slots) surfaces byte-for-byte equivalent to the
909        // pre-fold open-coded block, pinned by the paired
910        // `validate_ci_kind_coherence_folds_arm_matches_gate`
911        // per-arm equivalence pin and the
912        // `validate_ci_kind_coherence_accepts_acao_on_every_ci_shape`
913        // /
914        // `validate_ci_kind_coherence_accepts_absent_ci_on_every_kind`
915        // identity-element pins in the
916        // [`crate::Caixa::validate_ci_kind_coherence`] pin family
917        // (`manifest.rs`). Peer to the sibling three-arm
918        // [`crate::Caixa::validate_kind_slot_coherence`] fold
919        // f0d286e: that primitive carries the M3 / supervisor-tree
920        // / M2 axes under a uniform `{ caixa, kind, slots }`
921        // envelope, this primitive carries the `:ci` axis under
922        // its distinct `{ caixa, kind }` envelope, and every
923        // author-time kind ↔ slot coherence diagnostic at the
924        // layout altitude now routes through one substrate
925        // primitive per envelope shape rather than an open-coded
926        // block.
927        caixa.validate_ci_kind_coherence()?;
928
929        // Kind ↔ slot coherence on the fourth and final axis — the
930        // code-surface slot set (the trio M2/Supervisor/Aplicacao gates
931        // above close on the M2 runtime, supervisor-tree, and M3 mesh
932        // axes; this gate closes the symmetric "kind owns this code
933        // shape" relation on `:exe` + `:servicos`). `:exe` is the nix-
934        // built executable surface owned only by Binario; `:servicos`
935        // is the wasm-component daemon surface owned only by Servico.
936        // The caixa-helm / caixa-flux / caixa-flake renderers gate on
937        // `require_kind(_, <owning-kind>)` and only emit the slot for
938        // its owning kind — so on any *other* code-running kind a
939        // declared `:exe` / `:servicos` is the manifest field's
940        // documented "ignored otherwise" (see the field docs on
941        // `Caixa::exe` + `Caixa::servicos`): the path is validated by
942        // the per-kind path-existence loops below, but the value is
943        // never rendered into a build target or programs.yaml entry —
944        // it silently passes `feira build` and then vanishes, far from
945        // the source caixa.lisp.
946        //
947        // Reject it here — beside the M2/Supervisor/Aplicacao slot
948        // gates, after the `SupervisorOwnsCode` / `AplicacaoOwnsCode`
949        // OwnCode gates which dominate on those two no-code kinds (a
950        // Supervisor / Aplicacao with any of `:bibliotecas` / `:exe` /
951        // `:servicos` surfaces the OwnCode diagnostic first), and
952        // before the path-existence loops which would otherwise spend
953        // a less-helpful `MissingEntry` diagnostic on the foreign
954        // slot's path. `declared_foreign_code_slots` is the single
955        // typed source of the foreign-code-slot set + its canonical
956        // diagnostic order (`:exe` → `:servicos`).
957        //
958        // Mirrors the 9d37f98 / 510c00a / 760a430 kind ↔ slot
959        // coherence trio's "declared-but-inert" footgun closure on the
960        // M2 / supervisor-tree / M3 axes, now extended onto the code-
961        // surface axis — every code-running kind's exclusive code
962        // surface is structurally fenced from every other code-running
963        // kind. `:bibliotecas` is deliberately excluded from the foreign
964        // set on Binario / Servico (a `lib/` helper bundled into the
965        // nix flake's build or the wasm-component's source tree is a
966        // legitimate cross-kind authoring shape); on Biblioteca it is
967        // the native slot, and on Supervisor / Aplicacao the OwnCode
968        // gates above already close it.
969        //
970        // Folded onto [`crate::Caixa::validate_foreign_code_kind_coherence`]
971        // — the fourth and last kind ↔ slot coherence primitive on the
972        // typed [`Caixa`] surface, peer of
973        // [`crate::Caixa::validate_kind_slot_coherence`] (f0d286e,
974        // the cross-family M3/supervisor/M2 fold on the sibling
975        // `{ caixa, kind, slots }` envelope),
976        // [`crate::Caixa::validate_no_code_kind_coherence`] (3bbf6a2,
977        // the reciprocal no-code-kind fold on `SupervisorOwnsCode` /
978        // `AplicacaoOwnsCode` / `AcaoOwnsCode`), and
979        // [`crate::Caixa::validate_ci_kind_coherence`] (9b55beb, the
980        // `:ci` axis fold on the `{ caixa, kind }` envelope). With
981        // this lift every kind-coherence axis at the layout altitude
982        // routes through one substrate primitive per axis rather than
983        // an open-coded block, closing the last open-coded gap the
984        // sibling [`crate::Caixa::validate_kind_slot_coherence`]
985        // doc-comment's "ForeignCodeSlot … stays open-coded downstream"
986        // note flagged.
987        caixa.validate_foreign_code_kind_coherence()?;
988
989        // Per-entry path-shape gate on the three Caixa-level code-surface
990        // path lists (`:bibliotecas`, `:exe`, `:servicos`): each entry must
991        // be non-empty, relative, and free of `..` components — the same
992        // [`crate::render::is_sandboxed_relative_path`] discipline the
993        // peer `:behavior :on-*` (b0c8389) and
994        // `:upgrade-from :state-change :script` (26da2c7) axes already
995        // route through. Runs *after* the kind-coherence gates above (so
996        // a `:exe` on a Servico surfaces ForeignCodeSlot rather than a
997        // per-entry shape diagnostic, and a Supervisor/Aplicacao with any
998        // code surface surfaces OwnCode first) and *before* the existence
999        // loops below (so an empty / absolute / parent-escaping entry
1000        // surfaces its self-locating per-slot diagnostic rather than a
1001        // downstream `MissingEntry` / `ExeOutsideDir` /
1002        // `ServicoOutsideDir` against the resolved sandbox-escape path).
1003        caixa.run_layout_gate(Caixa::validate_code_paths, LayoutError::code_path_violation)?;
1004
1005        if caixa.kind().requires_lib() && caixa.bibliotecas().is_empty() {
1006            let expected = root
1007                .join(crate::render::LAYOUT_DIR_LIB)
1008                .join(format!("{}.lisp", caixa.nome()));
1009            if !self.exists(&expected) {
1010                return Err(LayoutError::MissingLib {
1011                    caixa: caixa.nome().to_string(),
1012                    expected,
1013                });
1014            }
1015        }
1016
1017        // Required-slot gate on the three [`CaixaKind`] arms whose
1018        // sole payload is a canonical typed slot — folded onto one
1019        // substrate primitive at
1020        // [`crate::Caixa::validate_required_kind_slot`]. Pre-lift each
1021        // of the three arms lived as a self-similar
1022        // `if caixa.kind().requires_<slot>() && caixa.<slot>().is_<empty>() {
1023        // return Err(LayoutError::<kind>_without_<slot>(caixa)); }` block
1024        // at this call site — three consumers, three identical shapes,
1025        // one substrate primitive on [`Caixa`] closing the duplication
1026        // the PRIME DIRECTIVE names as a bug. Diagnostic order at the
1027        // primitive matches the pre-fold canonical sequence — `:exe` →
1028        // `:servicos` → `:ci` — the same three-arm sweep the peer
1029        // [`CaixaKind`] discriminator carries at its `requires_*`
1030        // accessors; the three arms are mutually exclusive by
1031        // construction (`:kind` is a single-valued discriminator so at
1032        // most one arm can fire per caixa) so no cross-arm ordering
1033        // pin is meaningful.
1034        //
1035        // The paired `Biblioteca`-arm required-slot check
1036        // ([`LayoutError::MissingLib`], immediately above) stays
1037        // open-coded at this altitude by design: it needs the
1038        // [`LayoutInvariants::exists`] filesystem oracle to check the
1039        // default `lib/<nome>.lisp` fallback path, which the pure
1040        // per-`Caixa` typed-shape surface the fold rides on has no
1041        // reference to. Same posture the peer
1042        // [`crate::Caixa::validate_no_code_kind_coherence`] fold
1043        // (3bbf6a2) takes on the on-disk existence loops.
1044        //
1045        // Peer with the per-slot and per-kind compound entry gates
1046        // every substrate primitive on the M2/M3 typed-slot family
1047        // already carries ([`crate::Caixa::validate_deps`] b5dd55e,
1048        // [`crate::Caixa::validate_limits`] baa4688,
1049        // [`crate::Caixa::validate_behavior`] 0d2877a,
1050        // [`crate::Caixa::validate_upgrade_from`] d6801df,
1051        // [`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
1052        // [`crate::Caixa::validate_supervisor_shape`] 4c70105,
1053        // [`crate::Caixa::validate_acao_shape`] 5d6df54,
1054        // [`crate::Caixa::validate_kind_slot_coherence`] f0d286e,
1055        // [`crate::Caixa::validate_no_code_kind_coherence`] 3bbf6a2,
1056        // [`crate::Caixa::validate_ci_kind_coherence`] 9b55beb): the
1057        // layout pipeline routes the three self-similar required-slot
1058        // gates through one substrate primitive rather than three
1059        // open-coded blocks, and every future required-slot arm (a
1060        // per-Aplicacao required-`:membros` gate, a per-Supervisor
1061        // required-`:children` gate — both already carried at
1062        // [`CaixaKind::requires_membros`] / [`CaixaKind::requires_children`]
1063        // without a paired layout-side wire-up) folds onto this
1064        // compound gate as one arm addition rather than a fourth
1065        // open-coded block at the wire-up site.
1066        caixa.validate_required_kind_slot()?;
1067
1068        // On-disk existence probe on the three Caixa-level code-surface
1069        // path lists (`:bibliotecas`, `:exe`, `:servicos`): every declared
1070        // entry must resolve on disk. The `:bibliotecas` sweep folds onto
1071        // the [`Self::probe_declared_entries`] per-slot batch primitive
1072        // so the iteration + per-iteration `probe_declared_entry`
1073        // dispatch reads through one call rather than a three-line
1074        // `for … { self.probe_declared_entry(…)?; }` loop kept in
1075        // lockstep with the peer `:behavior` / `:upgrade-from` batch
1076        // wire-ups below. The `:exe` / `:servicos` peers fold onto the
1077        // sibling [`Self::probe_sandboxed_declared_entries`] per-slot
1078        // batch primitive on the sandboxed axis so the iteration +
1079        // per-iteration `probe_sandboxed_declared_entry` dispatch reads
1080        // through one call rather than a three-line `for … { self.
1081        // probe_sandboxed_declared_entry(…)?; }` loop kept in lockstep
1082        // with each other and with the peer `:bibliotecas` bare-batch
1083        // wire-up above. All three preserve the pre-lift diagnostic
1084        // order: `MissingEntry` fires before `ExeOutsideDir` /
1085        // `ServicoOutsideDir` on the same iteration, and iteration
1086        // proceeds in the caller-supplied iterator order (short-
1087        // circuiting on the first miss).
1088        self.probe_declared_entries(
1089            caixa.bibliotecas(),
1090            root,
1091            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
1092        )?;
1093
1094        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
1095        self.probe_sandboxed_declared_entries(
1096            caixa.exe(),
1097            root,
1098            crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
1099            &exe_dir,
1100            LayoutError::ExeOutsideDir,
1101        )?;
1102
1103        let servicos_dir = root.join(crate::render::LAYOUT_DIR_SERVICOS);
1104        self.probe_sandboxed_declared_entries(
1105            caixa.servicos(),
1106            root,
1107            crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
1108            &servicos_dir,
1109            LayoutError::ServicoOutsideDir,
1110        )?;
1111
1112        // ── M2 typed-substrate invariants ────────────────────────────────
1113
1114        // Compound per-Caixa entry gate on the M2 `:limits` slot: the
1115        // layout pipeline's `if let Some(l) = caixa.limits() { l.validate() }`
1116        // `Option::None → Ok(()) | Some(_) → dispatch` unwrap-and-
1117        // dispatch pattern — the four-axis cascade on the present-slot
1118        // arm ([`crate::LimitsSpec::validate`]'s `:memory` wasm32
1119        // zero-floor / below-page / above-cap / non-page-multiple;
1120        // `:fuel` zero-floor / cap; `:wall-clock` zero-floor / cap;
1121        // `:cpu` zero-floor / cap) folded onto the
1122        // [`crate::Caixa::validate_limits`] substrate primitive. The
1123        // absent-slot arm (`limits: None`, the canonical "no bound
1124        // declared — engine-default applies" author shape) is the
1125        // fold's identity element and passes trivially through the
1126        // primitive, byte-equal to the pre-lift `if let Some(l) = …`
1127        // guard this call site formerly carried. Pinned by the paired
1128        // `validate_limits_folds_arm_matches_gate` equivalence pin and
1129        // the `validate_limits_accepts_none` / `_accepts_clean_fixture`
1130        // positive-control pins in the [`crate::Caixa::validate_limits`]
1131        // pin family (`manifest.rs`).
1132        //
1133        // Same lift discipline the peer per-Caixa compound gates
1134        // ([`crate::Caixa::validate_upgrade_from`] d6801df,
1135        // [`crate::Caixa::validate_deps`] b5dd55e) each carry — one
1136        // named substrate-primitive gate per typed slot folds every
1137        // structural axis on that slot (plus the `Option::None`
1138        // identity element for the `Option`-shaped slots) onto one
1139        // call, so every future consumer that wants to re-check
1140        // `:limits` after a per-`{:memory, :fuel, :wall-clock, :cpu}`
1141        // patch (the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
1142        // materializer's admission webhook, a future `feira validate
1143        // --limits` per-caixa admission verb, a per-`:limits` overlay
1144        // resolver) reaches the four-axis cascade through one dispatch
1145        // rather than re-inlining the `if let Some(l) = …` unwrap-and-
1146        // dispatch pattern in lockstep with this wire-up.
1147        caixa.run_layout_gate(Caixa::validate_limits, LayoutError::limits_violation)?;
1148
1149        // Compound per-Caixa entry gate on the M2 `:behavior` slot's
1150        // pure value-shape surface: the layout pipeline's
1151        // `if let Some(b) = caixa.behavior() { b.validate() }`
1152        // `Option::None → Ok(()) | Some(_) → dispatch` unwrap-and-
1153        // dispatch pattern — the six-slot value-shape cascade on the
1154        // present-slot arm ([`crate::BehaviorSpec::validate`]'s per-
1155        // `:on-init` / `:on-call` / `:on-cast` / `:on-info` /
1156        // `:on-state-change` / `:on-terminate` non-empty / relative /
1157        // no-`..`-parent-escape / terminating-`.lisp`-extension
1158        // arm-set routed through the shared
1159        // [`crate::render::require_sandboxed_lisp_path`] helper) —
1160        // folded onto the [`crate::Caixa::validate_behavior`] substrate
1161        // primitive. The absent-slot arm (`behavior: None`, the
1162        // canonical "no callback declared — the runtime falls back to
1163        // the wasm-engine's default per arm" author shape) is the
1164        // fold's identity element and passes trivially through the
1165        // primitive, byte-equal to the pre-lift `if let Some(b) = …`
1166        // guard this call site formerly carried. Pinned by the paired
1167        // `validate_behavior_folds_arm_matches_gate` equivalence pin
1168        // and the `validate_behavior_accepts_none` /
1169        // `_accepts_clean_fixture` positive-control pins in the
1170        // [`crate::Caixa::validate_behavior`] pin family
1171        // (`manifest.rs`).
1172        //
1173        // The value-shape gate runs BEFORE the on-disk callback-path
1174        // existence walk below so a malformed `:behavior` slot
1175        // surfaces its self-locating per-slot diagnostic (naming the
1176        // offending `:on-*` slot) rather than the less-helpful
1177        // "missing behavior-callback" the existence probe would raise
1178        // against the resolved sandbox-escape path.
1179        //
1180        // Same lift discipline the peer per-Caixa compound gates
1181        // ([`crate::Caixa::validate_limits`] baa4688,
1182        // [`crate::Caixa::validate_upgrade_from`] d6801df,
1183        // [`crate::Caixa::validate_deps`] b5dd55e) each carry — one
1184        // named substrate-primitive gate per typed slot folds every
1185        // structural axis on that slot (plus the `Option::None`
1186        // identity element for the `Option`-shaped slots) onto one
1187        // call, so every future consumer that wants to re-check
1188        // `:behavior` after a per-`{:on-init, …, :on-terminate}`
1189        // patch (the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
1190        // materializer's admission webhook, a future `feira validate
1191        // --behavior` per-caixa admission verb, a per-`:behavior`
1192        // overlay resolver) reaches the six-slot cascade through one
1193        // dispatch rather than re-inlining the `if let Some(b) = …`
1194        // unwrap-and-dispatch pattern in lockstep with this wire-up.
1195        // The paired on-disk existence walk stays open-coded at this
1196        // altitude because it needs the [`LayoutInvariants::exists`]
1197        // filesystem oracle the pure typed-shape surface has no
1198        // reference to — mirror of the peer M2 `:upgrade-from` per-
1199        // instruction script-path existence probe that stayed at this
1200        // altitude after the [`crate::Caixa::validate_upgrade_from`]
1201        // lift for the same reason.
1202        caixa.run_layout_gate(Caixa::validate_behavior, LayoutError::behavior_violation)?;
1203        if let Some(b) = caixa.behavior() {
1204            self.probe_declared_entries(
1205                b.declared_paths(),
1206                root,
1207                crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
1208            )?;
1209        }
1210
1211        // Compound per-Caixa entry gate on `:upgrade-from`: the layout
1212        // pipeline's three-dispatch M2 `:upgrade-from` cascade — the
1213        // per-entry shape + cross-entry duplicate-`:from` gate
1214        // ([`crate::upgrade::validate_upgrade_from`]), the cross-slot
1215        // `:from < :versao` SemVer-2 precedence gate
1216        // ([`crate::upgrade::validate_upgrade_from_against_versao`]), and
1217        // the cross-slot `:state-change` ↔ `:on-state-change` composition
1218        // gate ([`crate::upgrade::validate_upgrade_from_against_behavior`])
1219        // — folded onto the [`crate::Caixa::validate_upgrade_from`]
1220        // substrate primitive. The three dispatches run in the same
1221        // canonical order at the primitive (per-entry → versao → behavior)
1222        // so the fold is byte-for-byte equivalent to the pre-fold
1223        // three-block cascade this call site formerly carried, pinned by
1224        // the paired
1225        // `validate_upgrade_from_folds_{per_entry,versao,behavior}_arm_matches_gate`
1226        // equivalence pins and the
1227        // `validate_upgrade_from_{per_entry_arm_fires_before_versao_arm,
1228        // versao_arm_fires_before_behavior_arm}` ordering pins in the
1229        // [`crate::Caixa::validate_upgrade_from`] pin family
1230        // (`manifest.rs`).
1231        //
1232        // Runs BEFORE the existing per-instruction script-path existence
1233        // pass below so a malformed typed slot surfaces its own
1234        // self-locating diagnostic rather than the less-helpful "missing
1235        // upgrade-script" (which doesn't fire for non-script axes at all).
1236        // Same lift discipline the peer per-slot compound gates
1237        // ([`crate::AplicacaoSpec::validate_contratos`] and its
1238        // `:membros` / `:entrada` / `:placement` / `:politicas` peers,
1239        // [`crate::MeshPolicy::validate`],
1240        // [`crate::SupervisorSpec::validate_children`]) each carry — one
1241        // named substrate-primitive gate per typed slot folds every
1242        // structural axis on that slot onto one call, so every future
1243        // consumer that wants to re-check `:upgrade-from` after a
1244        // per-entry patch (the deferred `caixa.pleme.io/v1alpha1/Caixa`
1245        // CR materializer's admission webhook, a future `feira validate
1246        // --upgrade` per-caixa admission verb, a per-`:upgrade-from`
1247        // overlay resolver) reaches the three-arm compound gate through
1248        // one dispatch rather than re-inlining the three-dispatch
1249        // cascade in lockstep with this wire-up.
1250        //
1251        // The per-instruction on-disk existence-probe walk below stays
1252        // open-coded at the layout wire-up site — that arm needs the
1253        // filesystem oracle on the [`LayoutInvariants`] trait, not on
1254        // the pure per-Caixa typed-shape surface the compound gate
1255        // folds. Same posture [`crate::Caixa::validate_code_paths`] takes
1256        // on the sibling code-path axes: the typed-shape gate fires on
1257        // the per-Caixa surface, the on-disk existence check fires on
1258        // the [`StandardLayout`] surface.
1259        caixa.run_layout_gate(Caixa::validate_upgrade_from, LayoutError::upgrade_violation)?;
1260        self.probe_declared_entries(
1261            caixa
1262                .upgrade_from()
1263                .iter()
1264                .flat_map(crate::UpgradeFromEntry::instructions)
1265                .filter_map(crate::upgrade::UpgradeInstruction::declared_path),
1266            root,
1267            crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
1268        )?;
1269
1270        // Supervisor invariants (typed shape — children, restart strategy).
1271        // The "supervisor doesn't own code" check is at the top of verify()
1272        // so it fires before the existence-check loops.
1273        if caixa.kind().is_supervisor() {
1274            // Raw `:restart-window` parse gate on the flat
1275            // `Caixa::restart_window: Option<String>` axis — the last
1276            // orphan-validator on the typed Caixa surface flagged by the
1277            // [`Self::validate_deps`] wire-up's closing comment (the
1278            // "Supervisor-axis specific" remainder) and the
1279            // [`Caixa::supervisor_view`] doc-comment's "the future
1280            // layout-side wire-up" pin. Until this gate landed
1281            // [`Caixa::validate_restart_window`] existed as `pub fn` on
1282            // [`Caixa`] with full per-arm unit coverage in `manifest::tests`
1283            // (`validate_restart_window_rejects_*` — fractional seconds,
1284            // decimal-shaped integer, half-unit minute, leading sign,
1285            // unknown unit, garbage, empty-after-trim; eight rejection
1286            // arms total), but no production code path called it —
1287            // `feira build` (the canonical author-time gate; routes
1288            // through [`StandardLayout::verify`]) silently accepted a
1289            // malformed `:restart-window` and [`Caixa::supervisor_view`]
1290            // soft-swallowed the parse failure as `restart_window: None`
1291            // (i.e. the canonical "omit the slot to express no reset"
1292            // sentinel), turning every malformed window into a never-reset
1293            // supervisor far from the source `caixa.lisp`, with no field
1294            // naming the offending `:restart-window`. The Erlang/OTP
1295            // `MaxIntensity / Period` invariant the typed [`SupervisorSpec`]
1296            // gate (`view.validate()` immediately below) enforces on the
1297            // `Option<Duration>` value never reached the gate at all on
1298            // these inputs: the parse error was already laundered to
1299            // `None`, and `None` is the canonical "never reset" shape
1300            // that always validates cleanly. Lifting the parse gate to
1301            // the layout-pipeline wire-up closes the laundering — every
1302            // value past this gate either parses through the shared
1303            // `crate::supervisor::duration_codec::parse` (and therefore
1304            // round-trips canonically) or fires the new
1305            // [`Self::RestartWindowViolation`] envelope at the source.
1306            //
1307            // Runs *inside* the `kind == Supervisor` branch (rather than
1308            // alongside the peer flat-Caixa gates `validate_nome` /
1309            // `validate_versao` / `validate_deps` / `validate_code_paths`
1310            // above the kind dispatch) because `:restart-window` is in
1311            // the Supervisor slot set per [`Caixa::declared_supervisor_slots`]
1312            // — every non-Supervisor caixa with `:restart-window` set
1313            // already errors upstream via the
1314            // [`Self::SupervisorSlotsOnNonSupervisor`] kind-coherence gate
1315            // (line 243-252), so reaching this gate on a non-Supervisor
1316            // kind would be a no-op (the field is `None` by construction).
1317            // Runs *before* `view.validate()` so the parse-side diagnostic
1318            // surfaces first on the raw-string axis — a `:restart-window
1319            // "1.5s"` lands on the more self-locating
1320            // `RestartWindowViolation` (which names the offending raw
1321            // string verbatim) rather than the laundered-to-`None`
1322            // soft-pass that the typed view would silently let through.
1323            //
1324            // Same per-axis `*Violation { caixa, issue }` envelope every
1325            // peer flat-Caixa wrap exposes ([`Self::NomeViolation`] /
1326            // [`Self::VersaoViolation`] 1f74a5f,
1327            // [`Self::DepsViolation`] aa77d0f, [`Self::CodePathViolation`]
1328            // b868442). Threads [`ManifestError::RestartWindowMalformed`]
1329            // Display through verbatim — the per-arm reason already names
1330            // the offending raw value (e.g. `":restart-window \"1.5s\" is
1331            // not a canonical duration: …"`), so the wrap's `issue`
1332            // carries a self-locating "which axis, which value, why"
1333            // without re-shaping the parser-side reason.
1334            caixa.run_layout_gate(
1335                Caixa::validate_restart_window,
1336                LayoutError::restart_window_violation,
1337            )?;
1338            // Compound per-Caixa entry gate on the Supervisor-kind
1339            // supervision-tree slot family: the layout pipeline's paired
1340            // `let view = caixa.supervisor_view().expect(...);
1341            // view.validate() … validate_no_self_supervision(...) …`
1342            // cascade — the typed-shape cascade
1343            // ([`crate::SupervisorSpec::validate`]'s per-slot gates on
1344            // `:estrategia` ↔ `:children` invariants, `:max-restarts` /
1345            // `:restart-window` bounds, per-child DNS-1123 `:caixa`
1346            // names, semver-valid `:versao` constraints, the
1347            // set-not-multiset duplicate-child gate) and the cross-slot
1348            // self-edge gate
1349            // ([`crate::supervisor::validate_no_self_supervision`], the
1350            // `:children :caixa` ≠ `:nome` invariant the typed view
1351            // cannot enforce on its own because it carries the children
1352            // but not the parent `:nome`) — folded onto the
1353            // [`crate::Caixa::validate_supervisor_shape`] substrate
1354            // primitive. The two arms run in the same canonical order at
1355            // the primitive (typed-shape cascade → cross-slot self-edge)
1356            // so the fold is byte-for-byte equivalent to the pre-fold
1357            // two-block cascade this call site formerly carried, pinned
1358            // by the paired
1359            // `validate_supervisor_shape_folds_{view,self_supervision}_arm_matches_gate`
1360            // equivalence pins and the
1361            // `validate_supervisor_shape_view_arm_fires_before_self_supervision_arm`
1362            // ordering pin in the
1363            // [`crate::Caixa::validate_supervisor_shape`] pin family
1364            // (`manifest.rs`).
1365            //
1366            // Same lift discipline the peer per-Caixa compound gates
1367            // ([`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
1368            // [`crate::Caixa::validate_upgrade_from`] d6801df,
1369            // [`crate::Caixa::validate_deps`] b5dd55e,
1370            // [`crate::Caixa::validate_limits`] baa4688,
1371            // [`crate::Caixa::validate_behavior`] 0d2877a) each carry —
1372            // one named substrate-primitive gate folds every structural +
1373            // cross-slot axis on that slot family onto one call, so every
1374            // future consumer that wants to re-check the Supervisor shape
1375            // after a per-slot patch (the wasm-operator's hierarchical
1376            // reconciliation scheduler, the M4
1377            // `mesh.pleme.io/v1alpha1/Supervisor` CR materializer's
1378            // admission webhook, a future `feira validate --supervisor`
1379            // per-caixa admission verb, a per-Supervisor overlay
1380            // resolver) reaches the two-arm compound gate through one
1381            // dispatch rather than re-inlining the two-dispatch cascade
1382            // in lockstep with this wire-up. Peer with the
1383            // [`crate::render::require_supervisor_view`] compound entry
1384            // gate every per-Supervisor renderer would route through
1385            // (which already folds the same `spec.validate()` +
1386            // `validate_no_self_supervision` two-arm cascade behind its
1387            // `require_kind` + `validate_restart_window` prelude): the
1388            // two consumers of the Supervisor-shape cascade now share
1389            // one substrate primitive on each side of the
1390            // author-time-vs-renderer split, rather than two open-coded
1391            // cascades kept in lockstep.
1392            //
1393            // `:restart-window` stays wired above through its own
1394            // per-axis `LayoutError::RestartWindowViolation` envelope
1395            // (the raw-string parse gate on the flat
1396            // `Caixa::restart_window: Option<String>` axis, distinct
1397            // from the typed view's `Duration`-shape gate) — the fold
1398            // covers the two arms that share the
1399            // `LayoutError::SupervisorViolation` envelope; the
1400            // parse-gate arm keeps its self-locating envelope so a
1401            // malformed `:restart-window` surfaces the raw-value
1402            // diagnostic rather than the laundered-to-`None` soft-pass.
1403            caixa.run_layout_gate(
1404                Caixa::validate_supervisor_shape,
1405                LayoutError::supervisor_violation,
1406            )?;
1407        }
1408
1409        // Aplicacao invariants — typed graph composition. Like
1410        // Supervisor, an Aplicacao runs no code itself.
1411        //
1412        // Compound per-Caixa entry gate on the Aplicacao-kind mesh-slot
1413        // family: the layout pipeline's paired `let view =
1414        // caixa.aplicacao_view().expect(...); view.validate() …
1415        // validate_no_self_membership(...) …` cascade — the typed-shape
1416        // cascade ([`crate::AplicacaoSpec::validate`]'s per-slot gates
1417        // on `:membros`, `:contratos`, `:entrada`, `:placement`,
1418        // `:politicas`, in that declared order) and the cross-slot
1419        // self-edge gate ([`crate::aplicacao::validate_no_self_membership`],
1420        // the `:membros :caixa` ≠ `:nome` invariant the typed view
1421        // cannot enforce on its own because it carries the membros but
1422        // not the parent `:nome`) — folded onto the
1423        // [`crate::Caixa::validate_aplicacao_shape`] substrate primitive.
1424        // The two arms run in the same canonical order at the primitive
1425        // (typed-shape cascade → cross-slot self-edge) so the fold is
1426        // byte-for-byte equivalent to the pre-fold two-block cascade
1427        // this call site formerly carried, pinned by the paired
1428        // `validate_aplicacao_shape_folds_{view,self_membership}_arm_matches_gate`
1429        // equivalence pins and the
1430        // `validate_aplicacao_shape_view_arm_fires_before_self_membership_arm`
1431        // ordering pin in the [`crate::Caixa::validate_aplicacao_shape`]
1432        // pin family (`manifest.rs`).
1433        //
1434        // Same lift discipline the peer per-slot compound gates
1435        // ([`crate::Caixa::validate_upgrade_from`] d6801df,
1436        // [`crate::Caixa::validate_deps`] b5dd55e,
1437        // [`crate::Caixa::validate_limits`] baa4688,
1438        // [`crate::Caixa::validate_behavior`] 0d2877a) each carry — one
1439        // named substrate-primitive gate folds every structural +
1440        // cross-slot axis on that slot family onto one call, so every
1441        // future consumer that wants to re-check the Aplicacao shape
1442        // after a per-slot patch (the deferred
1443        // `caixa.pleme.io/v1alpha1/Caixa` CR materializer's admission
1444        // webhook, a future `feira validate --aplicacao` per-caixa
1445        // admission verb, a per-Aplicacao overlay resolver) reaches the
1446        // two-arm compound gate through one dispatch rather than
1447        // re-inlining the two-dispatch cascade in lockstep with this
1448        // wire-up. Peer with the [`crate::render::require_aplicacao_view`]
1449        // compound entry gate every per-Aplicacao renderer routes
1450        // through (3aefefb): the two consumers of the Aplicacao-shape
1451        // cascade now share one substrate primitive on each side of the
1452        // author-time-vs-renderer split, rather than two open-coded
1453        // cascades kept in lockstep.
1454        //
1455        // The outer `if caixa.kind().is_aplicacao()` guard stays because
1456        // [`crate::Caixa::validate_aplicacao_shape`] is the fold's
1457        // identity element on non-Aplicacao kinds (returns `Ok(())`
1458        // without touching the mesh slots — same posture as
1459        // [`crate::Caixa::validate_limits`] / [`Self`]::
1460        // [`crate::Caixa::validate_behavior`] on their `Option`-shaped
1461        // slots); the guard is a redundant but zero-cost fast-path that
1462        // preserves the peer supervisor branch's `if
1463        // caixa.kind().is_supervisor()` parallel structure at this
1464        // altitude.
1465        if caixa.kind().is_aplicacao() {
1466            caixa.run_layout_gate(
1467                Caixa::validate_aplicacao_shape,
1468                LayoutError::aplicacao_violation,
1469            )?;
1470        }
1471
1472        // Acao invariants — typed CI-run decompose. Like Supervisor and
1473        // Aplicacao, an Acao runs no code itself; unlike them, its sole
1474        // payload is a [`canteiro_types::CiRun`] whose declared-node
1475        // shape can carry three structural violations
1476        // ([`canteiro_types::DecomposeError`]: `DuplicateNode`,
1477        // `UnknownDep`, `Cycle`) the layout pipeline formerly deferred
1478        // to [`caixa_actions::validate`] — layout only checked `:ci`
1479        // *presence* via [`Self::MissingCi`] above, so a `:kind Acao`
1480        // carrying a structurally illegal `:ci` (a duplicate node
1481        // name, a dependency on an undeclared node, a dependency
1482        // cycle) passed `feira build` cleanly and surfaced the
1483        // diagnostic only when [`caixa_actions::validate`] later
1484        // refused it — far from the source `caixa.lisp` on the
1485        // author-time gate side.
1486        //
1487        // Compound per-Caixa entry gate on the Acao-kind `:ci` slot
1488        // family: the [`crate::render::decompose_ci`] typed decompose
1489        // gate — the sibling axis owned by the substrate-canonical
1490        // [`crate::render::require_acao_view`] compound helper every
1491        // per-`Acao` renderer routes through — folded onto the
1492        // [`crate::Caixa::validate_acao_shape`] substrate primitive.
1493        // The single-arm fold is byte-for-byte equivalent to the
1494        // pre-fold `decompose_ci(caixa, ci).map(|_| ())?` call this
1495        // wire-up sees, pinned by the paired
1496        // `validate_acao_shape_folds_decompose_arm_matches_gate`
1497        // equivalence pin and the
1498        // `validate_acao_shape_accepts_non_acao_kind` /
1499        // `validate_acao_shape_accepts_absent_ci_slot` identity-
1500        // element pins in the
1501        // [`crate::Caixa::validate_acao_shape`] pin family
1502        // (`manifest.rs`).
1503        //
1504        // Same lift discipline the peer per-kind compound gates
1505        // ([`crate::Caixa::validate_supervisor_shape`] 4c70105,
1506        // [`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
1507        // [`crate::Caixa::validate_upgrade_from`] d6801df,
1508        // [`crate::Caixa::validate_deps`] b5dd55e,
1509        // [`crate::Caixa::validate_limits`] baa4688,
1510        // [`crate::Caixa::validate_behavior`] 0d2877a) each carry —
1511        // one named substrate-primitive gate folds every structural
1512        // axis on that kind onto one call, so every future consumer
1513        // that wants to re-check the Acao shape after a per-node
1514        // patch (a per-`Acao` CR materializer's admission webhook, a
1515        // future `feira validate --acao` per-caixa admission verb, a
1516        // per-`Acao` overlay resolver) reaches the compound gate
1517        // through one dispatch rather than re-inlining the decompose
1518        // cascade in lockstep with this wire-up. Peer with the
1519        // [`crate::render::require_acao_view`] compound entry gate
1520        // every per-`Acao` renderer routes through: the two consumers
1521        // of the Acao-shape cascade now share one substrate primitive
1522        // on each side of the author-time-vs-renderer split, rather
1523        // than two open-coded cascades kept in lockstep. Closes the
1524        // last per-kind asymmetry — with this wire-up the four typed
1525        // named-caixa kinds (`Servico` / `Aplicacao` / `Supervisor` /
1526        // `Acao`) each route through one compound per-Caixa shape
1527        // gate at the layout altitude.
1528        //
1529        // Runs *after* the [`Self::MissingCi`] presence gate above
1530        // (so a `:kind Acao` caixa with `ci = None` surfaces the
1531        // presence diagnostic first — the fold's identity-element arm
1532        // passes cleanly on absent `:ci`) and *after* the sibling
1533        // Supervisor / Aplicacao shape gates so the per-kind
1534        // diagnostic ordering at the layout altitude reads as the
1535        // canonical `Supervisor → Aplicacao → Acao` sweep.
1536        //
1537        // The outer `if caixa.kind().is_acao()` guard stays because
1538        // [`crate::Caixa::validate_acao_shape`] is the fold's
1539        // identity element on non-Acao kinds (returns `Ok(())`
1540        // without touching the `:ci` slot — same posture as
1541        // [`crate::Caixa::validate_supervisor_shape`] /
1542        // [`crate::Caixa::validate_aplicacao_shape`] on the sibling
1543        // typed-view-carrying arms); the guard is a redundant but
1544        // zero-cost fast-path that preserves the peer supervisor /
1545        // aplicacao branches' `if caixa.kind().is_<kind>()` parallel
1546        // structure at this altitude.
1547        if caixa.kind().is_acao() {
1548            caixa.run_layout_gate(Caixa::validate_acao_shape, LayoutError::acao_violation)?;
1549        }
1550
1551        Ok(())
1552    }
1553}
1554
1555#[derive(Debug, Error, PartialEq, Eq)]
1556pub enum LayoutError {
1557    #[error("manifest missing: {}", .0.display())]
1558    MissingManifest(PathBuf),
1559    #[error("caixa '{caixa}' is a Biblioteca but has no lib entry — expected {}", expected.display())]
1560    MissingLib { caixa: String, expected: PathBuf },
1561    #[error("caixa '{0}' is a Binario but has no :exe entries")]
1562    BinarioWithoutExe(String),
1563    #[error("caixa '{0}' is a Servico but has no :servicos entries")]
1564    ServicoWithoutServicos(String),
1565    #[error("declared {kind} entry missing: {}", path.display())]
1566    MissingEntry { kind: &'static str, path: PathBuf },
1567    #[error("exe entry outside exe/ directory: {}", .0.display())]
1568    ExeOutsideDir(PathBuf),
1569    #[error("servico entry outside servicos/ directory: {}", .0.display())]
1570    ServicoOutsideDir(PathBuf),
1571    #[error("caixa '{caixa}' has invalid :nome: {issue}")]
1572    NomeViolation { caixa: String, issue: String },
1573    #[error("caixa '{caixa}' has invalid :versao: {issue}")]
1574    VersaoViolation { caixa: String, issue: String },
1575    #[error("caixa '{caixa}' has invalid :deps / :deps-dev entry: {issue}")]
1576    DepsViolation { caixa: String, issue: String },
1577    #[error("caixa '{caixa}' has invalid :etiquetas entry: {issue}")]
1578    EtiquetasViolation { caixa: String, issue: String },
1579    #[error("caixa '{caixa}' has invalid :autores entry: {issue}")]
1580    AutoresViolation { caixa: String, issue: String },
1581    #[error("caixa '{caixa}' has invalid :repositorio: {issue}")]
1582    RepositorioViolation { caixa: String, issue: String },
1583    #[error("caixa '{caixa}' has invalid :descricao: {issue}")]
1584    DescricaoViolation { caixa: String, issue: String },
1585    #[error("caixa '{caixa}' has invalid :licenca: {issue}")]
1586    LicencaViolation { caixa: String, issue: String },
1587    #[error("caixa '{caixa}' has invalid :edicao: {issue}")]
1588    EdicaoViolation { caixa: String, issue: String },
1589    #[error("caixa '{caixa}' has invalid code-path entry: {issue}")]
1590    CodePathViolation { caixa: String, issue: String },
1591    #[error("caixa '{caixa}' has invalid :limits: {issue}")]
1592    LimitsViolation { caixa: String, issue: String },
1593    #[error("caixa '{caixa}' has invalid :behavior callback: {issue}")]
1594    BehaviorViolation { caixa: String, issue: String },
1595    #[error("caixa '{caixa}' has invalid :upgrade-from entry: {issue}")]
1596    UpgradeViolation { caixa: String, issue: String },
1597    #[error("supervisor caixa '{caixa}' violates typed shape: {issue}")]
1598    SupervisorViolation { caixa: String, issue: String },
1599    #[error("supervisor caixa '{caixa}' has invalid :restart-window: {issue}")]
1600    RestartWindowViolation { caixa: String, issue: String },
1601    #[error(
1602        "supervisor caixa '{0}' must not declare :bibliotecas, :exe, or :servicos — supervisors don't run code, they orchestrate other caixas"
1603    )]
1604    SupervisorOwnsCode(String),
1605    #[error("aplicacao caixa '{caixa}' violates typed shape: {issue}")]
1606    AplicacaoViolation { caixa: String, issue: String },
1607    #[error("acao caixa '{caixa}' violates typed shape: {issue}")]
1608    AcaoViolation { caixa: String, issue: String },
1609    #[error(
1610        "aplicacao caixa '{0}' must not declare :bibliotecas, :exe, or :servicos — aplicacaos compose Servicos, they don't run code themselves"
1611    )]
1612    AplicacaoOwnsCode(String),
1613    #[error(
1614        "acao caixa '{0}' must not declare :bibliotecas, :exe, or :servicos — acaos carry a typed CI run (:ci), they don't run code themselves"
1615    )]
1616    AcaoOwnsCode(String),
1617    #[error(
1618        "caixa '{caixa}' is :kind {kind:?} but declares Aplicacao-only mesh slot(s): {slots} — \
1619         :membros / :contratos / :politicas / :placement / :entrada compose a :kind Aplicacao's \
1620         typed graph (MESH-COMPOSITION §III.1) and are silently ignored on every other kind \
1621         (never validated, never rendered); move them to a :kind Aplicacao caixa or remove them"
1622    )]
1623    MeshSlotsOnNonAplicacao {
1624        caixa: String,
1625        kind: CaixaKind,
1626        slots: String,
1627    },
1628    #[error(
1629        "caixa '{caixa}' is :kind {kind:?} but declares Supervisor-only slot(s): {slots} — \
1630         :estrategia / :max-restarts / :restart-window / :children compose a :kind Supervisor's \
1631         typed OTP supervisor (INSPIRATIONS §II.2) and are silently ignored on every other kind \
1632         (never validated, never reconciled); move them to a :kind Supervisor caixa or remove them"
1633    )]
1634    SupervisorSlotsOnNonSupervisor {
1635        caixa: String,
1636        kind: CaixaKind,
1637        slots: String,
1638    },
1639    #[error(
1640        "caixa '{caixa}' is :kind {kind:?} but declares Servico-only slot(s): {slots} — \
1641         :limits / :behavior / :upgrade-from configure the runtime of a long-running :kind Servico \
1642         wasm component (INSPIRATIONS §III.1 / §II.3 / §II.4) and are silently ignored on every \
1643         other kind (never rendered into a chart or programs.yaml entry); move them to a :kind \
1644         Servico caixa or remove them"
1645    )]
1646    ServicoSlotsOnNonServico {
1647        caixa: String,
1648        kind: CaixaKind,
1649        slots: String,
1650    },
1651    #[error(
1652        "caixa '{caixa}' is :kind {kind:?} but declares foreign code-surface slot(s): {slots} — \
1653         :exe is the nix-built executable surface owned only by :kind Binario, :servicos is the \
1654         wasm-component + ComputeUnit daemon surface owned only by :kind Servico; \
1655         caixa-helm / caixa-flux / caixa-flake gate emission on `require_kind(_, <owning-kind>)`, \
1656         so a declared :exe / :servicos on the wrong code-running kind is silently ignored — the \
1657         path is validated by the layout's path-existence loops but never rendered into a build \
1658         target or programs.yaml entry. Move the slot to its owning kind, change :kind to match \
1659         (Binario for :exe, Servico for :servicos), or drop the slot entirely"
1660    )]
1661    ForeignCodeSlot {
1662        caixa: String,
1663        kind: CaixaKind,
1664        slots: String,
1665    },
1666    #[error("caixa '{0}' is an Acao but has no :ci slot")]
1667    MissingCi(String),
1668    #[error(
1669        "caixa '{caixa}' is :kind {kind:?} but declares the Acao-only :ci slot — \
1670         :ci carries a typed CI run (canteiro_types::CiRun, CANTEIRO §7.1-C) that only the \
1671         caixa-actions renderer validates for :kind Acao, and is silently ignored on every \
1672         other kind (never decomposed, never rendered); move it to a :kind Acao caixa or \
1673         remove it"
1674    )]
1675    CiOnNonAcao { caixa: String, kind: CaixaKind },
1676}
1677
1678// Fold the layout-pipeline per-Caixa violation wrap onto one substrate
1679// primitive per typed slot. Every `LayoutError::*Violation { caixa, issue }`
1680// variant follows the same uniform shape — `caixa = layout-Caixa's :nome`,
1681// `issue = the gate's per-arm Display` — and every wire-up in
1682// [`StandardLayout::verify`] used to open-code the identical five-line
1683// `.map_err(|err| LayoutError::XxxViolation { caixa: caixa.nome().to_string(),
1684// issue: err.to_string() })` block, once per typed slot. Sixteen distinct
1685// wrap-variants × eighteen wire-up sites is exactly the duplication the
1686// PRIME DIRECTIVE names as a bug: every future consumer that wants to add a
1687// new per-slot gate (the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
1688// materializer's admission webhook, a future per-slot `feira validate`
1689// verb, a per-slot overlay resolver) had to re-inline the five-line block
1690// in lockstep with the pre-existing wire-ups.
1691//
1692// The macro below generates one static constructor per variant of shape
1693// `fn <slot>_violation(caixa: &Caixa, err: impl Display) -> LayoutError`,
1694// so every wire-up site collapses onto one dispatch:
1695// `caixa.validate_<slot>().map_err(|err| LayoutError::<slot>_violation(caixa,
1696// err))?;`. The uniform two-slot construction (`caixa: caixa.nome()
1697// .to_string()`, `issue: err.to_string()`) is spelled once — inside the
1698// macro — rather than at every wire-up site. Every constructor is
1699// `#[must_use]` so a caller who mistakenly discards the wrapped error
1700// (rather than routing it through `?`) trips a compile warning at the
1701// wire-up site.
1702//
1703// Peer with the per-slot compound entry gates every substrate primitive
1704// on the M2/M3 typed-slot family already carries
1705// ([`crate::Caixa::validate_deps`] b5dd55e, [`crate::Caixa::validate_limits`]
1706// baa4688, [`crate::Caixa::validate_behavior`] 0d2877a,
1707// [`crate::Caixa::validate_upgrade_from`] d6801df,
1708// [`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
1709// [`crate::AplicacaoSpec::validate_contratos`],
1710// [`crate::MeshPolicy::validate`],
1711// [`crate::SupervisorSpec::validate_children`]): the author-time gates
1712// fold onto one substrate primitive per typed slot; here the layout-side
1713// error-wrap folds onto one substrate primitive per typed variant, so the
1714// two sides of the layout pipeline's per-slot cascade (the gate, the
1715// wrap) each route through one call rather than N open-coded block
1716// repetitions.
1717macro_rules! layout_violation_ctors {
1718    ($($ctor:ident => $variant:ident),* $(,)?) => {
1719        impl LayoutError {
1720            $(
1721                #[doc = concat!(
1722                    "Construct a [`LayoutError::",
1723                    stringify!($variant),
1724                    "`] wrapping `err` under `caixa.nome()`. Folds the ",
1725                    "uniform `{ caixa: caixa.nome().to_string(), issue: ",
1726                    "err.to_string() }` two-slot construction onto one ",
1727                    "substrate primitive so every ",
1728                    "[`StandardLayout::verify`] wire-up on this variant ",
1729                    "reads through one dispatch rather than the pre-lift ",
1730                    "five-line open-coded block."
1731                )]
1732                #[must_use]
1733                pub fn $ctor<E: std::fmt::Display>(caixa: &crate::Caixa, err: E) -> Self {
1734                    Self::$variant {
1735                        caixa: caixa.nome().to_string(),
1736                        issue: err.to_string(),
1737                    }
1738                }
1739            )*
1740        }
1741    };
1742}
1743
1744layout_violation_ctors! {
1745    nome_violation => NomeViolation,
1746    versao_violation => VersaoViolation,
1747    deps_violation => DepsViolation,
1748    etiquetas_violation => EtiquetasViolation,
1749    autores_violation => AutoresViolation,
1750    repositorio_violation => RepositorioViolation,
1751    descricao_violation => DescricaoViolation,
1752    licenca_violation => LicencaViolation,
1753    edicao_violation => EdicaoViolation,
1754    code_path_violation => CodePathViolation,
1755    limits_violation => LimitsViolation,
1756    behavior_violation => BehaviorViolation,
1757    upgrade_violation => UpgradeViolation,
1758    restart_window_violation => RestartWindowViolation,
1759    supervisor_violation => SupervisorViolation,
1760    aplicacao_violation => AplicacaoViolation,
1761    acao_violation => AcaoViolation,
1762}
1763
1764// Fold the four `LayoutError::*SlotsOn*` / `LayoutError::ForeignCodeSlot`
1765// kind-coherence wrap sites onto one substrate primitive per typed variant —
1766// the sibling of [`layout_violation_ctors!`] above on the second uniform
1767// error-envelope shape `LayoutError` carries: `{ caixa: caixa.nome(),
1768// kind: caixa.kind(), slots: <declared_*_slots()>.join(" ") }`. Every
1769// wire-up in [`StandardLayout::verify`] on this shape (four sites: the
1770// M3-mesh gate on non-Aplicacao, the supervisor-tree gate on
1771// non-Supervisor, the M2-runtime gate on non-Servico, the code-surface
1772// `ForeignCodeSlot` gate) used to open-code the identical four-field
1773// `.{ caixa: caixa.nome().to_string(), kind: caixa.kind(), slots:
1774// <declared_*_slots>.join(" ") }` block — the exact "same block re-inlined
1775// at every consumer" shape the PRIME DIRECTIVE names as a bug on the same
1776// altitude the peer [`layout_violation_ctors!`] macro just closed on the
1777// `{ caixa, issue }` sibling shape.
1778//
1779// The macro below generates one static constructor per variant of shape
1780// `fn <slot>_on_non_<owner>(caixa: &Caixa, slots: Vec<&'static str>) ->
1781// LayoutError`, so every wire-up site collapses onto one dispatch:
1782// `return Err(LayoutError::<slot>_on_non_<owner>(caixa, <declared_slots>));`.
1783// The uniform four-field construction is spelled once — inside the macro —
1784// rather than at every wire-up site. Every constructor is `#[must_use]` so
1785// a caller who mistakenly discards the constructed error (rather than
1786// routing it through `return Err(…)`) trips a compile warning at the
1787// wire-up site.
1788//
1789// Peer with the `_violation` constructor family above on the same
1790// `LayoutError` — the two together now fold every uniform-shape
1791// `LayoutError` variant carried by [`StandardLayout::verify`] onto one
1792// substrate primitive per typed variant, so the layout-side error-wrap
1793// surface reads through one dispatch per variant rather than N open-coded
1794// blocks. Every future consumer that wants to construct one of these
1795// variants outside the layout pipeline (a per-slot admission webhook, a
1796// `feira validate --kind X` verb, an overlay resolver rejecting a
1797// kind-foreign patch) reaches its variant through one call, matching the
1798// `_violation` family's substrate-primitive discipline.
1799macro_rules! layout_slot_kind_ctors {
1800    ($($ctor:ident => $variant:ident),* $(,)?) => {
1801        impl LayoutError {
1802            $(
1803                #[doc = concat!(
1804                    "Construct a [`LayoutError::",
1805                    stringify!($variant),
1806                    "`] naming the offending slot list under `caixa.nome()` ",
1807                    "at `caixa.kind()`. Folds the uniform `{ caixa: caixa.",
1808                    "nome().to_string(), kind: caixa.kind(), slots: slots.",
1809                    "join(\" \") }` four-field construction onto one substrate ",
1810                    "primitive so every [`StandardLayout::verify`] wire-up on ",
1811                    "this variant reads through one dispatch rather than the ",
1812                    "pre-lift open-coded block."
1813                )]
1814                #[must_use]
1815                pub fn $ctor(caixa: &crate::Caixa, slots: Vec<&'static str>) -> Self {
1816                    Self::$variant {
1817                        caixa: caixa.nome().to_string(),
1818                        kind: caixa.kind(),
1819                        slots: slots.join(" "),
1820                    }
1821                }
1822            )*
1823        }
1824    };
1825}
1826
1827layout_slot_kind_ctors! {
1828    mesh_slots_on_non_aplicacao => MeshSlotsOnNonAplicacao,
1829    supervisor_slots_on_non_supervisor => SupervisorSlotsOnNonSupervisor,
1830    servico_slots_on_non_servico => ServicoSlotsOnNonServico,
1831    foreign_code_slot => ForeignCodeSlot,
1832}
1833
1834// Fold the five [`LayoutError::MissingEntry`] wire-up sites at
1835// [`StandardLayout::verify`] onto one substrate primitive on `LayoutError` —
1836// the third and last uniform-shape envelope on `LayoutError` after the
1837// `{ caixa, issue }` family the [`layout_violation_ctors!`] macro closed
1838// (131ca0d) and the `{ caixa, kind, slots }` family the peer
1839// [`layout_slot_kind_ctors!`] macro closed (0419438). Each of the five
1840// wire-up sites on `MissingEntry` (`:bibliotecas` iteration line 823,
1841// `:exe` iteration line 834, `:servicos` iteration line 848, `:behavior`
1842// on-disk callback-path iteration line 957, `:upgrade-from` per-
1843// instruction script-path iteration line 1021) opened the same four-line
1844// `LayoutError::MissingEntry { kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_<slot>,
1845// path: full }` struct-literal block — the exact "same block re-inlined
1846// at every consumer" shape the PRIME DIRECTIVE names as a bug, on the
1847// same altitude the peer `_violation` / `_slots_on_non_*` families each
1848// closed on the sibling `LayoutError` envelopes.
1849//
1850// One `#[must_use]` inherent constructor on `LayoutError` collapses the
1851// five sites onto one dispatch:
1852// `return Err(LayoutError::missing_entry(<kind-label>, full));`, byte-
1853// equal to the pre-lift struct-literal block. A macro is not warranted
1854// on the one-variant envelope shape `{ kind: &'static str, path: PathBuf }`
1855// (unlike the sibling 16-variant `_violation` / 4-variant
1856// `_slots_on_non_*` shapes), but the same substrate-primitive discipline
1857// applies: every future consumer that wants to construct a `MissingEntry`
1858// outside the layout pipeline (a per-slot admission webhook probing a
1859// declared path against an out-of-band filesystem oracle, a `feira
1860// validate --lib` / `--exe` / `--servico` / `--behavior` / `--upgrade`
1861// per-caixa admission verb, the deferred `caixa.pleme.io/v1alpha1/Caixa`
1862// CR materializer's admission-webhook floor, a per-cluster overlay
1863// resolver rejecting a missing entry against a cluster-local filesystem
1864// snapshot) reaches the variant through one call rather than re-inlining
1865// the four-line struct-literal block in lockstep with the five
1866// layout-pipeline wire-up sites.
1867impl LayoutError {
1868    /// Construct a [`LayoutError::MissingEntry`] naming the missing
1869    /// declared entry at `path` under the canonical `kind` label from
1870    /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`] /
1871    /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] /
1872    /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] /
1873    /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`] /
1874    /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT`].
1875    /// Folds the uniform `{ kind, path }` two-slot construction onto one
1876    /// substrate primitive so every [`StandardLayout::verify`] wire-up
1877    /// on this variant reads through one dispatch rather than the
1878    /// pre-lift open-coded struct-literal block.
1879    #[must_use]
1880    pub fn missing_entry(kind: &'static str, path: PathBuf) -> Self {
1881        Self::MissingEntry { kind, path }
1882    }
1883}
1884
1885// Fold the six `LayoutError::<Variant>(caixa.nome().to_string())` nome-
1886// only tuple-variant wire-up sites at [`StandardLayout::verify`] onto one
1887// substrate primitive per typed variant — the fourth uniform-shape
1888// envelope on `LayoutError` after the `{ caixa, issue }` family the
1889// [`layout_violation_ctors!`] macro closed (131ca0d), the
1890// `{ caixa, kind, slots }` family the peer [`layout_slot_kind_ctors!`]
1891// macro closed (0419438), and the `{ kind, path }`
1892// [`LayoutError::missing_entry`] one-variant ctor (1b09f9d). Each of the
1893// six wire-up sites on this shape (`SupervisorOwnsCode` /
1894// `AplicacaoOwnsCode` / `AcaoOwnsCode` at the no-code kind-coherence gate;
1895// `BinarioWithoutExe` / `ServicoWithoutServicos` / `MissingCi` at the
1896// required-slot gate) opened the identical one-line
1897// `LayoutError::<Variant>(caixa.nome().to_string())` tuple-literal — the
1898// exact "same block re-inlined at every consumer" shape the PRIME
1899// DIRECTIVE names as a bug, on the same altitude the peer `_violation` /
1900// `_slots_on_non_*` / `missing_entry` families each closed on the
1901// sibling `LayoutError` envelopes.
1902//
1903// The macro below generates one static constructor per variant of shape
1904// `fn <slot>(caixa: &Caixa) -> LayoutError`, so every wire-up site
1905// collapses onto one dispatch:
1906// `return Err(LayoutError::<slot>(caixa));`, byte-equal to the pre-lift
1907// tuple-literal. The uniform one-field construction (`caixa: caixa.nome()
1908// .to_string()`) is spelled once — inside the macro — rather than at
1909// every wire-up site. Every constructor is `#[must_use]` so a caller who
1910// mistakenly discards the constructed error (rather than routing it
1911// through `return Err(…)`) trips a compile warning at the wire-up site.
1912//
1913// Peer with the three prior `LayoutError`-envelope constructor families
1914// — the four together now fold every uniform-shape `LayoutError` variant
1915// carried by [`StandardLayout::verify`] onto one substrate primitive per
1916// typed variant, so every layout-side error-wrap on `LayoutError` reads
1917// through one dispatch per variant rather than N open-coded blocks.
1918// Every future consumer that wants to construct one of these variants
1919// outside the layout pipeline (a per-slot admission webhook probing an
1920// Acao's `:ci` slot, a `feira validate --kind <X>` verb refusing a
1921// no-code kind that declares `:bibliotecas` / `:exe` / `:servicos`, an
1922// overlay resolver rejecting a required-slot omission against a
1923// cluster-local snapshot) reaches its variant through one call, matching
1924// the `_violation` / `_slots_on_non_*` / `missing_entry` families'
1925// substrate-primitive discipline.
1926macro_rules! layout_nome_only_ctors {
1927    ($($ctor:ident => $variant:ident),* $(,)?) => {
1928        impl LayoutError {
1929            $(
1930                #[doc = concat!(
1931                    "Construct a [`LayoutError::",
1932                    stringify!($variant),
1933                    "`] naming the offending `caixa.nome()`. Folds the ",
1934                    "uniform `Self::",
1935                    stringify!($variant),
1936                    "(caixa.nome().to_string())` one-field tuple-",
1937                    "literal onto one substrate primitive so every ",
1938                    "[`StandardLayout::verify`] wire-up on this variant ",
1939                    "reads through one dispatch rather than the pre-lift ",
1940                    "open-coded block."
1941                )]
1942                #[must_use]
1943                pub fn $ctor(caixa: &crate::Caixa) -> Self {
1944                    Self::$variant(caixa.nome().to_string())
1945                }
1946            )*
1947        }
1948    };
1949}
1950
1951layout_nome_only_ctors! {
1952    binario_without_exe => BinarioWithoutExe,
1953    servico_without_servicos => ServicoWithoutServicos,
1954    missing_ci => MissingCi,
1955    supervisor_owns_code => SupervisorOwnsCode,
1956    aplicacao_owns_code => AplicacaoOwnsCode,
1957    acao_owns_code => AcaoOwnsCode,
1958}
1959
1960#[cfg(test)]
1961mod tests {
1962    use super::*;
1963    use crate::{Caixa, CaixaKind};
1964    use std::path::PathBuf;
1965
1966    fn caixa(kind: CaixaKind) -> Caixa {
1967        Caixa {
1968            nome: "demo".into(),
1969            versao: "0.1.0".into(),
1970            kind,
1971            edicao: None,
1972            descricao: None,
1973            repositorio: None,
1974            licenca: None,
1975            autores: vec![],
1976            etiquetas: vec![],
1977            deps: vec![],
1978            deps_dev: vec![],
1979            exe: vec![],
1980            bibliotecas: vec![],
1981            servicos: vec![],
1982            // M2 typed-substrate slots default to absent.
1983            limits: None,
1984            behavior: None,
1985            upgrade_from: vec![],
1986            estrategia: None,
1987            max_restarts: None,
1988            restart_window: None,
1989            children: vec![],
1990            // M3 Aplicacao slots default to absent.
1991            membros: vec![],
1992            contratos: vec![],
1993            politicas: None,
1994            placement: None,
1995            entrada: None,
1996            ci: None,
1997        }
1998    }
1999
2000    #[test]
2001    fn missing_manifest_errors() {
2002        let layout = StandardLayout::new().with_path_exists(|_| false);
2003        let err = layout
2004            .verify(&caixa(CaixaKind::Biblioteca), Path::new("/tmp/x"))
2005            .unwrap_err();
2006        assert!(matches!(err, LayoutError::MissingManifest(_)));
2007    }
2008
2009    // ── LayoutError::*_violation constructor family ──────────────────────
2010    //
2011    // The [`layout_violation_ctors!`] macro (below the `LayoutError` enum
2012    // definition) generates one static constructor per `*Violation { caixa,
2013    // issue }` variant that folds the uniform `{ caixa: caixa.nome()
2014    // .to_string(), issue: err.to_string() }` two-slot construction onto
2015    // one substrate primitive. The per-variant equivalence pins below
2016    // (fail-before-pass-after by construction — a byte-mismatched macro
2017    // arm would trip its equivalence pin first) lock each generated
2018    // constructor to its struct-literal peer under `PartialEq`, so every
2019    // wire-up in [`StandardLayout::verify`] on that variant produces a
2020    // byte-equal `LayoutError` to the pre-lift open-coded block. The
2021    // fixture caixa fires under `caixa("demo")` so the `caixa: "demo"`
2022    // half is pinned; the fixture error fires under a fixed `&str` so the
2023    // `issue: <literal>` half is pinned; the two together pin every field
2024    // of every generated variant.
2025
2026    fn layout_violation_ctor_fixture() -> (Caixa, &'static str) {
2027        (caixa(CaixaKind::Biblioteca), "sample issue text")
2028    }
2029
2030    // `assert_eq!` uses `PartialEq::eq(&self, &other)` under the hood, so
2031    // `actual`/`expected` are only ever read, never moved into anything —
2032    // the ergonomic tradeoff (owned + move-in vs. reference + &-borrow at
2033    // every call site) favors the owned form for a test-only assertion
2034    // helper called from 17 wire-up pins. The lint targets the general
2035    // API-shape case where callers still have downstream uses for the
2036    // moved value; the assertion helper terminates on the equality check.
2037    #[allow(clippy::needless_pass_by_value)]
2038    fn assert_violation_ctor_matches(actual: LayoutError, expected: LayoutError) {
2039        assert_eq!(
2040            actual, expected,
2041            "generated constructor must produce byte-equal LayoutError to open-coded struct-literal wrap",
2042        );
2043    }
2044
2045    #[test]
2046    fn nome_violation_ctor_matches_struct_literal_wrap() {
2047        let (c, issue) = layout_violation_ctor_fixture();
2048        assert_violation_ctor_matches(
2049            LayoutError::nome_violation(&c, issue),
2050            LayoutError::NomeViolation {
2051                caixa: c.nome().to_string(),
2052                issue: issue.to_string(),
2053            },
2054        );
2055    }
2056
2057    #[test]
2058    fn versao_violation_ctor_matches_struct_literal_wrap() {
2059        let (c, issue) = layout_violation_ctor_fixture();
2060        assert_violation_ctor_matches(
2061            LayoutError::versao_violation(&c, issue),
2062            LayoutError::VersaoViolation {
2063                caixa: c.nome().to_string(),
2064                issue: issue.to_string(),
2065            },
2066        );
2067    }
2068
2069    #[test]
2070    fn deps_violation_ctor_matches_struct_literal_wrap() {
2071        let (c, issue) = layout_violation_ctor_fixture();
2072        assert_violation_ctor_matches(
2073            LayoutError::deps_violation(&c, issue),
2074            LayoutError::DepsViolation {
2075                caixa: c.nome().to_string(),
2076                issue: issue.to_string(),
2077            },
2078        );
2079    }
2080
2081    #[test]
2082    fn etiquetas_violation_ctor_matches_struct_literal_wrap() {
2083        let (c, issue) = layout_violation_ctor_fixture();
2084        assert_violation_ctor_matches(
2085            LayoutError::etiquetas_violation(&c, issue),
2086            LayoutError::EtiquetasViolation {
2087                caixa: c.nome().to_string(),
2088                issue: issue.to_string(),
2089            },
2090        );
2091    }
2092
2093    #[test]
2094    fn autores_violation_ctor_matches_struct_literal_wrap() {
2095        let (c, issue) = layout_violation_ctor_fixture();
2096        assert_violation_ctor_matches(
2097            LayoutError::autores_violation(&c, issue),
2098            LayoutError::AutoresViolation {
2099                caixa: c.nome().to_string(),
2100                issue: issue.to_string(),
2101            },
2102        );
2103    }
2104
2105    #[test]
2106    fn repositorio_violation_ctor_matches_struct_literal_wrap() {
2107        let (c, issue) = layout_violation_ctor_fixture();
2108        assert_violation_ctor_matches(
2109            LayoutError::repositorio_violation(&c, issue),
2110            LayoutError::RepositorioViolation {
2111                caixa: c.nome().to_string(),
2112                issue: issue.to_string(),
2113            },
2114        );
2115    }
2116
2117    #[test]
2118    fn descricao_violation_ctor_matches_struct_literal_wrap() {
2119        let (c, issue) = layout_violation_ctor_fixture();
2120        assert_violation_ctor_matches(
2121            LayoutError::descricao_violation(&c, issue),
2122            LayoutError::DescricaoViolation {
2123                caixa: c.nome().to_string(),
2124                issue: issue.to_string(),
2125            },
2126        );
2127    }
2128
2129    #[test]
2130    fn licenca_violation_ctor_matches_struct_literal_wrap() {
2131        let (c, issue) = layout_violation_ctor_fixture();
2132        assert_violation_ctor_matches(
2133            LayoutError::licenca_violation(&c, issue),
2134            LayoutError::LicencaViolation {
2135                caixa: c.nome().to_string(),
2136                issue: issue.to_string(),
2137            },
2138        );
2139    }
2140
2141    #[test]
2142    fn edicao_violation_ctor_matches_struct_literal_wrap() {
2143        let (c, issue) = layout_violation_ctor_fixture();
2144        assert_violation_ctor_matches(
2145            LayoutError::edicao_violation(&c, issue),
2146            LayoutError::EdicaoViolation {
2147                caixa: c.nome().to_string(),
2148                issue: issue.to_string(),
2149            },
2150        );
2151    }
2152
2153    #[test]
2154    fn code_path_violation_ctor_matches_struct_literal_wrap() {
2155        let (c, issue) = layout_violation_ctor_fixture();
2156        assert_violation_ctor_matches(
2157            LayoutError::code_path_violation(&c, issue),
2158            LayoutError::CodePathViolation {
2159                caixa: c.nome().to_string(),
2160                issue: issue.to_string(),
2161            },
2162        );
2163    }
2164
2165    #[test]
2166    fn limits_violation_ctor_matches_struct_literal_wrap() {
2167        let (c, issue) = layout_violation_ctor_fixture();
2168        assert_violation_ctor_matches(
2169            LayoutError::limits_violation(&c, issue),
2170            LayoutError::LimitsViolation {
2171                caixa: c.nome().to_string(),
2172                issue: issue.to_string(),
2173            },
2174        );
2175    }
2176
2177    #[test]
2178    fn behavior_violation_ctor_matches_struct_literal_wrap() {
2179        let (c, issue) = layout_violation_ctor_fixture();
2180        assert_violation_ctor_matches(
2181            LayoutError::behavior_violation(&c, issue),
2182            LayoutError::BehaviorViolation {
2183                caixa: c.nome().to_string(),
2184                issue: issue.to_string(),
2185            },
2186        );
2187    }
2188
2189    #[test]
2190    fn upgrade_violation_ctor_matches_struct_literal_wrap() {
2191        let (c, issue) = layout_violation_ctor_fixture();
2192        assert_violation_ctor_matches(
2193            LayoutError::upgrade_violation(&c, issue),
2194            LayoutError::UpgradeViolation {
2195                caixa: c.nome().to_string(),
2196                issue: issue.to_string(),
2197            },
2198        );
2199    }
2200
2201    #[test]
2202    fn restart_window_violation_ctor_matches_struct_literal_wrap() {
2203        let (c, issue) = layout_violation_ctor_fixture();
2204        assert_violation_ctor_matches(
2205            LayoutError::restart_window_violation(&c, issue),
2206            LayoutError::RestartWindowViolation {
2207                caixa: c.nome().to_string(),
2208                issue: issue.to_string(),
2209            },
2210        );
2211    }
2212
2213    #[test]
2214    fn supervisor_violation_ctor_matches_struct_literal_wrap() {
2215        let (c, issue) = layout_violation_ctor_fixture();
2216        assert_violation_ctor_matches(
2217            LayoutError::supervisor_violation(&c, issue),
2218            LayoutError::SupervisorViolation {
2219                caixa: c.nome().to_string(),
2220                issue: issue.to_string(),
2221            },
2222        );
2223    }
2224
2225    #[test]
2226    fn aplicacao_violation_ctor_matches_struct_literal_wrap() {
2227        let (c, issue) = layout_violation_ctor_fixture();
2228        assert_violation_ctor_matches(
2229            LayoutError::aplicacao_violation(&c, issue),
2230            LayoutError::AplicacaoViolation {
2231                caixa: c.nome().to_string(),
2232                issue: issue.to_string(),
2233            },
2234        );
2235    }
2236
2237    #[test]
2238    fn acao_violation_ctor_matches_struct_literal_wrap() {
2239        // Sibling of [`aplicacao_violation_ctor_matches_struct_literal_wrap`]
2240        // / [`supervisor_violation_ctor_matches_struct_literal_wrap`] on
2241        // the third per-kind compound-shape wrap envelope on
2242        // `LayoutError`. Pins the macro-generated `acao_violation`
2243        // constructor to its struct-literal peer under `PartialEq`, so
2244        // every wire-up in [`StandardLayout::verify`] on the
2245        // [`LayoutError::AcaoViolation`] variant produces a byte-equal
2246        // `LayoutError` to the pre-lift open-coded block. Closes the
2247        // pin family the peer per-kind shape wraps already carry.
2248        let (c, issue) = layout_violation_ctor_fixture();
2249        assert_violation_ctor_matches(
2250            LayoutError::acao_violation(&c, issue),
2251            LayoutError::AcaoViolation {
2252                caixa: c.nome().to_string(),
2253                issue: issue.to_string(),
2254            },
2255        );
2256    }
2257
2258    #[test]
2259    fn violation_ctor_routes_issue_through_display_impl() {
2260        // Pin the fold's `issue = err.to_string()` half against any type
2261        // implementing `Display` — a per-arm error type from a foreign
2262        // module (here, `std::io::Error`) threads through byte-equal to
2263        // the struct-literal `.to_string()` construction, so the fold
2264        // does not silently collapse onto `&str`-only inputs.
2265        let c = caixa(CaixaKind::Biblioteca);
2266        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "sample io display source");
2267        let expected_issue = io_err.to_string();
2268        let actual = LayoutError::deps_violation(&c, &io_err);
2269        assert_eq!(
2270            actual,
2271            LayoutError::DepsViolation {
2272                caixa: c.nome().to_string(),
2273                issue: expected_issue,
2274            },
2275        );
2276    }
2277
2278    #[test]
2279    fn violation_ctor_routes_caixa_prefix_through_nome_accessor() {
2280        // Pin the fold's `caixa = caixa.nome().to_string()` half against
2281        // a non-default `:nome` — the accessor threads the caller's
2282        // `:nome` verbatim into the wrap envelope, so the fold does not
2283        // silently collapse onto the default `"demo"` fixture nome.
2284        let mut c = caixa(CaixaKind::Biblioteca);
2285        c.nome = "alt-nome".into();
2286        let actual = LayoutError::behavior_violation(&c, "sample issue");
2287        assert_eq!(
2288            actual,
2289            LayoutError::BehaviorViolation {
2290                caixa: "alt-nome".to_string(),
2291                issue: "sample issue".to_string(),
2292            },
2293        );
2294    }
2295
2296    // ── Caixa::run_layout_gate — per-slot gate + LayoutError wrap fold ───
2297    //
2298    // The [`Caixa::run_layout_gate`] substrate primitive folds the 18
2299    // self-similar `caixa.validate_<slot>().map_err(|err| LayoutError::
2300    // <slot>_violation(caixa, err))?;` wire-up sites at
2301    // [`StandardLayout::verify`] onto one dispatch. The pins below
2302    // (fail-before-pass-after by construction — a silent regression that
2303    // de-folded either arm would trip its own pin first) lock the two
2304    // arms of the fold under `PartialEq`:
2305    //
2306    //   - The `Ok(())` identity-element arm passes through verbatim (no
2307    //     wrap runs, no `caixa` closure capture).
2308    //   - The `Err(E)` arm routes through the caller-supplied `wrap`
2309    //     ctor with `self` bound as the caixa slot, byte-equal to the
2310    //     pre-lift `.map_err(|err| CTOR(caixa, err))` closure.
2311    //
2312    // The third pin (per-arm equivalence) runs the primitive with the
2313    // canonical `Caixa::validate_nome` validator and the paired
2314    // `LayoutError::nome_violation` ctor on a fixture whose `:nome`
2315    // fails `validate_nome`'s DNS-1123 gate ("Bad_Nome" — the uppercase
2316    // + underscore double footgun) and asserts the primitive's
2317    // `Result<(), LayoutError>` matches the open-coded pre-lift cascade
2318    // on the same fixture. A silent regression that de-folded the wrap
2319    // (dropped the `self` binding, threaded a stale caixa nome, swapped
2320    // the wrap ctor) would surface here as a mismatch between the two
2321    // dispatches.
2322
2323    #[test]
2324    fn run_layout_gate_ok_arm_passes_through() {
2325        // Positive control on the identity-element arm: a gate returning
2326        // `Ok(())` short-circuits before the wrap runs, so the caller
2327        // receives `Ok(())` verbatim regardless of what the paired ctor
2328        // would have produced. Pins the fold's `Result::map_err`
2329        // short-circuit semantics — a regression that unconditionally
2330        // wrapped (e.g. always called the ctor) would trip here.
2331        let c = caixa(CaixaKind::Biblioteca);
2332        let result: Result<(), LayoutError> = c.run_layout_gate(
2333            |_c: &Caixa| Ok::<(), &'static str>(()),
2334            |_c: &Caixa, _err: &'static str| {
2335                panic!("wrap must not run on the Ok(()) arm of the fold")
2336            },
2337        );
2338        assert!(
2339            result.is_ok(),
2340            "run_layout_gate must pass Ok(()) through verbatim, got {result:?}"
2341        );
2342    }
2343
2344    #[test]
2345    fn run_layout_gate_err_arm_wraps_via_ctor() {
2346        // Positive control on the err arm: a gate returning `Err(E)`
2347        // threads `E` into the caller-supplied `wrap` ctor with `self`
2348        // bound as the caixa argument. Uses a synthetic `&'static str`
2349        // error and the canonical `LayoutError::deps_violation` ctor so
2350        // the pin covers the fold's two-callable dispatch shape without
2351        // depending on any per-slot validator's specific arm sweep.
2352        let c = caixa(CaixaKind::Biblioteca);
2353        let sample_reason = "sample gate reason";
2354        let result = c.run_layout_gate(
2355            |_c: &Caixa| Err::<(), &'static str>(sample_reason),
2356            LayoutError::deps_violation,
2357        );
2358        let err = result.expect_err("Err arm must reach the caller");
2359        assert_eq!(
2360            err,
2361            LayoutError::DepsViolation {
2362                caixa: c.nome().to_string(),
2363                issue: sample_reason.to_string(),
2364            },
2365            "run_layout_gate must wrap the gate's Err via the caller-supplied \
2366             ctor with `self` bound as the caixa slot"
2367        );
2368    }
2369
2370    #[test]
2371    fn run_layout_gate_folds_arm_matches_gate() {
2372        // Fail-before-pass-after per-arm equivalence pin on the err arm:
2373        // a fixture whose `:nome` fails `validate_nome`'s DNS-1123 gate
2374        // (`"Bad_Nome"` — uppercase + underscore double footgun the
2375        // [`crate::ManifestError::NomeInvalid`] arm rejects) surfaces
2376        // the same `LayoutError::NomeViolation` byte-equal through both
2377        // the primitive `Caixa::run_layout_gate(Caixa::validate_nome,
2378        // LayoutError::nome_violation)` and the open-coded pre-lift
2379        // cascade `Caixa::validate_nome().map_err(|err|
2380        // LayoutError::nome_violation(&c, err))` on the same Caixa
2381        // fixture. Pins the fold — a silent regression that de-folded
2382        // either arm (dropped the `self` binding, threaded a stale
2383        // caixa nome, swapped the wrap ctor) would surface here as a
2384        // mismatch between the two dispatches.
2385        let mut c = caixa(CaixaKind::Biblioteca);
2386        c.nome = "Bad_Nome".into();
2387        let via_primitive = c
2388            .run_layout_gate(Caixa::validate_nome, LayoutError::nome_violation)
2389            .expect_err("Bad_Nome must fail validate_nome");
2390        let via_open_coded = c
2391            .validate_nome()
2392            .map_err(|err| LayoutError::nome_violation(&c, err))
2393            .expect_err("Bad_Nome must fail validate_nome");
2394        assert_eq!(
2395            via_primitive, via_open_coded,
2396            "Caixa::run_layout_gate must surface the err-arm diagnostic \
2397             byte-equal to the open-coded `.map_err(|err| CTOR(caixa, err))` \
2398             cascade on the same Caixa fixture"
2399        );
2400        assert!(
2401            matches!(via_primitive, LayoutError::NomeViolation { .. }),
2402            "expected NomeViolation on Bad_Nome, got {via_primitive:?}"
2403        );
2404    }
2405
2406    // ── LayoutError kind-coherence constructor family ────────────────────
2407    //
2408    // The [`layout_slot_kind_ctors!`] macro (sibling of
2409    // [`layout_violation_ctors!`] beside the `LayoutError` enum definition)
2410    // generates one static constructor per `*SlotsOn*` / `ForeignCodeSlot`
2411    // variant that folds the uniform `{ caixa: caixa.nome().to_string(),
2412    // kind: caixa.kind(), slots: slots.join(" ") }` four-field construction
2413    // onto one substrate primitive. The per-variant equivalence pins below
2414    // (fail-before-pass-after by construction — a byte-mismatched macro arm
2415    // would trip its equivalence pin first) lock each generated constructor
2416    // to its struct-literal peer under `PartialEq`, so every wire-up in
2417    // [`StandardLayout::verify`] on that variant produces a byte-equal
2418    // `LayoutError` to the pre-lift open-coded block. The three cross-axis
2419    // pins that follow (non-default `:nome`, non-default kind, non-trivial
2420    // slots list) route each of the three constructor input axes through
2421    // its declared accessor / arg, so the fold does not silently collapse
2422    // onto a fixture default on any axis.
2423
2424    fn layout_slot_kind_ctor_fixture() -> (Caixa, Vec<&'static str>) {
2425        (caixa(CaixaKind::Biblioteca), vec![":membros", ":contratos"])
2426    }
2427
2428    // Same rationale as `assert_violation_ctor_matches` above: the helper
2429    // terminates on the equality check, so the owned-arg lint's general
2430    // API-shape target does not apply.
2431    #[allow(clippy::needless_pass_by_value)]
2432    fn assert_slot_kind_ctor_matches(actual: LayoutError, expected: LayoutError) {
2433        assert_eq!(
2434            actual, expected,
2435            "generated constructor must produce byte-equal LayoutError to open-coded struct-literal wrap",
2436        );
2437    }
2438
2439    #[test]
2440    fn mesh_slots_on_non_aplicacao_ctor_matches_struct_literal_wrap() {
2441        let (c, slots) = layout_slot_kind_ctor_fixture();
2442        assert_slot_kind_ctor_matches(
2443            LayoutError::mesh_slots_on_non_aplicacao(&c, slots.clone()),
2444            LayoutError::MeshSlotsOnNonAplicacao {
2445                caixa: c.nome().to_string(),
2446                kind: c.kind(),
2447                slots: slots.join(" "),
2448            },
2449        );
2450    }
2451
2452    #[test]
2453    fn supervisor_slots_on_non_supervisor_ctor_matches_struct_literal_wrap() {
2454        let (c, slots) = layout_slot_kind_ctor_fixture();
2455        assert_slot_kind_ctor_matches(
2456            LayoutError::supervisor_slots_on_non_supervisor(&c, slots.clone()),
2457            LayoutError::SupervisorSlotsOnNonSupervisor {
2458                caixa: c.nome().to_string(),
2459                kind: c.kind(),
2460                slots: slots.join(" "),
2461            },
2462        );
2463    }
2464
2465    #[test]
2466    fn servico_slots_on_non_servico_ctor_matches_struct_literal_wrap() {
2467        let (c, slots) = layout_slot_kind_ctor_fixture();
2468        assert_slot_kind_ctor_matches(
2469            LayoutError::servico_slots_on_non_servico(&c, slots.clone()),
2470            LayoutError::ServicoSlotsOnNonServico {
2471                caixa: c.nome().to_string(),
2472                kind: c.kind(),
2473                slots: slots.join(" "),
2474            },
2475        );
2476    }
2477
2478    #[test]
2479    fn foreign_code_slot_ctor_matches_struct_literal_wrap() {
2480        let (c, slots) = layout_slot_kind_ctor_fixture();
2481        assert_slot_kind_ctor_matches(
2482            LayoutError::foreign_code_slot(&c, slots.clone()),
2483            LayoutError::ForeignCodeSlot {
2484                caixa: c.nome().to_string(),
2485                kind: c.kind(),
2486                slots: slots.join(" "),
2487            },
2488        );
2489    }
2490
2491    #[test]
2492    fn slot_kind_ctor_routes_caixa_prefix_through_nome_accessor() {
2493        // Pin the fold's `caixa = caixa.nome().to_string()` half against a
2494        // non-default `:nome` — the accessor threads the caller's `:nome`
2495        // verbatim into the wrap envelope, so the fold does not silently
2496        // collapse onto the default `"demo"` fixture nome. Peer of the
2497        // sibling `violation_ctor_routes_caixa_prefix_through_nome_accessor`
2498        // pin on the `{ caixa, issue }` envelope; extended here onto the
2499        // `{ caixa, kind, slots }` envelope so both `LayoutError`-shape
2500        // constructor families guarantee the `:nome`-derived-caixa slot
2501        // routes through [`Caixa::nome`] rather than a hard-coded string.
2502        let mut c = caixa(CaixaKind::Biblioteca);
2503        c.nome = "alt-nome".into();
2504        let actual = LayoutError::mesh_slots_on_non_aplicacao(&c, vec![":membros"]);
2505        assert_eq!(
2506            actual,
2507            LayoutError::MeshSlotsOnNonAplicacao {
2508                caixa: "alt-nome".to_string(),
2509                kind: CaixaKind::Biblioteca,
2510                slots: ":membros".to_string(),
2511            },
2512        );
2513    }
2514
2515    #[test]
2516    fn slot_kind_ctor_routes_kind_through_caixa_kind_accessor() {
2517        // Pin the fold's `kind = caixa.kind()` half against a non-default
2518        // kind — the accessor threads the caller's `:kind` verbatim into
2519        // the wrap envelope, so the fold does not silently collapse onto
2520        // one hard-coded kind. Sweeps every non-Aplicacao / non-Supervisor
2521        // / non-Servico kind the corresponding gate can fire on so the
2522        // pin covers the kind-derivation axis on every downstream variant.
2523        for kind in [
2524            CaixaKind::Biblioteca,
2525            CaixaKind::Binario,
2526            CaixaKind::Servico,
2527            CaixaKind::Supervisor,
2528            CaixaKind::Aplicacao,
2529            CaixaKind::Acao,
2530        ] {
2531            let c = caixa(kind);
2532            let actual = LayoutError::foreign_code_slot(&c, vec![":exe"]);
2533            assert_eq!(
2534                actual,
2535                LayoutError::ForeignCodeSlot {
2536                    caixa: c.nome().to_string(),
2537                    kind,
2538                    slots: ":exe".to_string(),
2539                },
2540                "foreign_code_slot ctor must thread `caixa.kind()` verbatim on every kind",
2541            );
2542        }
2543    }
2544
2545    #[test]
2546    fn slot_kind_ctor_routes_slots_through_join_separator() {
2547        // Pin the fold's `slots = slots.join(" ")` half against a
2548        // multi-entry slots list — the join threads exactly one ASCII
2549        // space between entries, in caller-supplied order, so the fold
2550        // does not silently collapse onto a fixed separator (`", "`, `";
2551        // "`, `"\n"`), a sorted order, or a single-entry pass-through.
2552        // Uses the M2 servico-slot vocabulary since these are what the
2553        // corresponding `servico_slots_on_non_servico` gate reports.
2554        let c = caixa(CaixaKind::Biblioteca);
2555        let actual = LayoutError::servico_slots_on_non_servico(
2556            &c,
2557            vec![":limits", ":behavior", ":upgrade-from"],
2558        );
2559        assert_eq!(
2560            actual,
2561            LayoutError::ServicoSlotsOnNonServico {
2562                caixa: c.nome().to_string(),
2563                kind: c.kind(),
2564                slots: ":limits :behavior :upgrade-from".to_string(),
2565            },
2566        );
2567    }
2568
2569    // ── LayoutError::missing_entry substrate-primitive constructor ───────
2570    //
2571    // The [`LayoutError::missing_entry`] constructor beside the enum
2572    // definition folds the `{ kind: &'static str, path: PathBuf }`
2573    // uniform-shape envelope onto one substrate primitive — the third
2574    // and last uniform-shape envelope on `LayoutError` after the
2575    // `{ caixa, issue }` family the [`layout_violation_ctors!`] macro
2576    // closed (131ca0d) and the `{ caixa, kind, slots }` family the peer
2577    // [`layout_slot_kind_ctors!`] macro closed (0419438). The pins below
2578    // (fail-before-pass-after by construction — a byte-mismatched
2579    // constructor arm would trip its equivalence pin first) lock the
2580    // constructor to its struct-literal peer under `PartialEq`, so every
2581    // wire-up in [`StandardLayout::verify`] on this variant produces a
2582    // byte-equal `LayoutError` to the pre-lift open-coded block. The two
2583    // cross-axis pins that follow (canonical-kind-label sweep, non-
2584    // default path) route each of the two constructor input axes through
2585    // its arg verbatim, so the fold does not silently collapse onto a
2586    // fixture default on either axis.
2587
2588    #[test]
2589    fn missing_entry_ctor_matches_struct_literal_wrap() {
2590        // Per-envelope equivalence pin — the `missing_entry` constructor
2591        // produces a `LayoutError::MissingEntry` byte-equal under
2592        // `PartialEq` to the open-coded four-line struct-literal wrap on
2593        // the same `(kind, path)` fixture. Peer of the sibling
2594        // `<slot>_violation_ctor_matches_struct_literal_wrap` /
2595        // `<slot>_slots_on_non_<owner>_ctor_matches_struct_literal_wrap`
2596        // pins on the two prior uniform-shape envelopes on the same
2597        // `LayoutError`.
2598        let path = PathBuf::from("/tmp/x/lib/demo.lisp");
2599        assert_eq!(
2600            LayoutError::missing_entry(
2601                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2602                path.clone(),
2603            ),
2604            LayoutError::MissingEntry {
2605                kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2606                path,
2607            },
2608        );
2609    }
2610
2611    #[test]
2612    fn missing_entry_ctor_routes_kind_through_arg_verbatim() {
2613        // Pin the fold's `kind: &'static str` arg through every canonical
2614        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] label the five
2615        // wire-up sites in [`StandardLayout::verify`] pass — so the fold
2616        // does not silently collapse onto one hard-coded label. Sweep
2617        // matches the arm set the peer
2618        // `layout_missing_entry_kind_m2_consts_pin_canonical_kebab_case_labels`
2619        // pin (below) covers on the const-label declarations.
2620        let path = PathBuf::from("/tmp/x/entry");
2621        for kind in [
2622            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2623            crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2624            crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2625            crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
2626            crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
2627        ] {
2628            assert_eq!(
2629                LayoutError::missing_entry(kind, path.clone()),
2630                LayoutError::MissingEntry {
2631                    kind,
2632                    path: path.clone(),
2633                },
2634                "missing_entry ctor must thread `kind` verbatim on every canonical label",
2635            );
2636        }
2637    }
2638
2639    #[test]
2640    fn missing_entry_ctor_routes_path_through_arg_verbatim() {
2641        // Pin the fold's `path: PathBuf` arg against a non-default,
2642        // multi-component `PathBuf` — the ctor threads the caller's
2643        // `PathBuf` verbatim into the wrap envelope, so the fold does
2644        // not silently collapse onto a fixed component prefix, a
2645        // canonicalized form, or a single-component pass-through.
2646        let path = PathBuf::from("/alt/root")
2647            .join("servicos")
2648            .join("hello-rio.computeunit.yaml");
2649        assert_eq!(
2650            LayoutError::missing_entry(
2651                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2652                path.clone(),
2653            ),
2654            LayoutError::MissingEntry {
2655                kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2656                path,
2657            },
2658        );
2659    }
2660
2661    // ── StandardLayout::probe_declared_entry substrate primitive ─────────
2662    //
2663    // The [`StandardLayout::probe_declared_entry`] method
2664    // (`layout.rs`) folds the five self-similar
2665    // `let full = root.join(p); if !self.exists(&full) { return
2666    // Err(LayoutError::missing_entry(<kind>, full)); }` existence-probe
2667    // blocks at [`StandardLayout::verify`] (`:bibliotecas` iteration,
2668    // `:exe` iteration, `:servicos` iteration, `:behavior` on-disk
2669    // callback-path iteration, `:upgrade-from` per-instruction script-
2670    // path iteration) onto one substrate primitive. The pins below
2671    // (fail-before-pass-after by construction — a byte-mismatched
2672    // primitive body would trip its equivalence pin first) lock the
2673    // fold to its pre-lift open-coded shape under `PartialEq`, so every
2674    // wire-up in [`StandardLayout::verify`] on this primitive produces
2675    // a byte-equal `LayoutError` on miss and a byte-equal `PathBuf` on
2676    // hit.
2677
2678    #[test]
2679    fn probe_declared_entry_folds_miss_returns_missing_entry() {
2680        // Per-primitive equivalence pin on the miss arm — a
2681        // [`StandardLayout`] whose oracle returns `false` for every
2682        // path yields a `MissingEntry` byte-equal under `PartialEq` to
2683        // the open-coded `LayoutError::missing_entry(<kind>,
2684        // root.join(path))` wrap the pre-lift block carried at each of
2685        // the five wire-up sites. Peer of the sibling
2686        // `missing_entry_ctor_matches_struct_literal_wrap` pin on the
2687        // constructor's own byte-equal shape.
2688        let layout = StandardLayout::new().with_path_exists(|_| false);
2689        let root = PathBuf::from("/tmp/x");
2690        let path = Path::new("lib/demo.lisp");
2691        let err = layout
2692            .probe_declared_entry(
2693                path,
2694                &root,
2695                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2696            )
2697            .unwrap_err();
2698        assert_eq!(
2699            err,
2700            LayoutError::missing_entry(
2701                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2702                root.join(path),
2703            ),
2704            "probe_declared_entry miss arm must produce byte-equal LayoutError to \
2705             open-coded `missing_entry(<kind>, root.join(path))` wrap",
2706        );
2707    }
2708
2709    #[test]
2710    fn probe_declared_entry_folds_hit_returns_resolved_full() {
2711        // Per-primitive equivalence pin on the hit arm — a
2712        // [`StandardLayout`] whose oracle returns `true` for the probed
2713        // resolved path yields `Ok(root.join(path))` byte-equal under
2714        // `PartialEq`. The pre-lift `:exe` / `:servicos` wire-up sites
2715        // needed the resolved `full` for the follow-up sandbox-directory-
2716        // containment check; the fold preserves that hand-off through
2717        // the primitive's `Ok(PathBuf)` return arm rather than
2718        // re-computing `root.join(path)` at the follow-up gate.
2719        let root = PathBuf::from("/tmp/x");
2720        let path = Path::new("exe/tool.lisp");
2721        let full = root.join(path);
2722        let full_probe = full.clone();
2723        let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
2724        let resolved = layout
2725            .probe_declared_entry(path, &root, crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE)
2726            .expect("probe_declared_entry must return Ok(root.join(path)) when the oracle hits");
2727        assert_eq!(
2728            resolved, full,
2729            "probe_declared_entry hit arm must return the resolved `root.join(path)` \
2730             byte-equal so the `:exe` / `:servicos` sandbox-containment follow-up \
2731             reads it verbatim without re-computing",
2732        );
2733    }
2734
2735    #[test]
2736    fn probe_declared_entry_threads_kind_through_arg_verbatim() {
2737        // Cross-axis pin — sweep every canonical
2738        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] label the five
2739        // wire-up sites in [`StandardLayout::verify`] pass. Each miss
2740        // must return a `MissingEntry` whose `kind:` field byte-equals
2741        // the caller-provided arg, so the fold does not silently
2742        // collapse onto one hard-coded label. Sweep matches the arm
2743        // set the peer
2744        // `missing_entry_ctor_routes_kind_through_arg_verbatim` pin
2745        // covers on the constructor arg.
2746        let layout = StandardLayout::new().with_path_exists(|_| false);
2747        let root = PathBuf::from("/tmp/x");
2748        let path = Path::new("some/entry");
2749        for kind in [
2750            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2751            crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2752            crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2753            crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
2754            crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
2755        ] {
2756            let err = layout.probe_declared_entry(path, &root, kind).unwrap_err();
2757            assert_eq!(
2758                err,
2759                LayoutError::missing_entry(kind, root.join(path)),
2760                "probe_declared_entry must thread `kind` verbatim on every canonical label",
2761            );
2762        }
2763    }
2764
2765    #[test]
2766    fn probe_declared_entry_threads_path_through_arg_verbatim() {
2767        // Cross-axis pin — the primitive must compose the `path` arg
2768        // through `root.join` verbatim on the miss arm's `MissingEntry
2769        // { path: … }` field, so the fold does not silently collapse
2770        // onto a fixed component prefix, a canonicalized form, or a
2771        // hand-authored `root.join(<literal>)`. Sweep two multi-
2772        // component `Path` fixtures (one under `servicos/`, one under
2773        // `lib/`) so a byte-drifted composition on either axis would
2774        // trip.
2775        let layout = StandardLayout::new().with_path_exists(|_| false);
2776        let root = PathBuf::from("/alt/root");
2777        for path in [
2778            Path::new("servicos/hello-rio.computeunit.yaml"),
2779            Path::new("lib/demo.lisp"),
2780        ] {
2781            let err = layout
2782                .probe_declared_entry(
2783                    path,
2784                    &root,
2785                    crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2786                )
2787                .unwrap_err();
2788            assert_eq!(
2789                err,
2790                LayoutError::missing_entry(
2791                    crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2792                    root.join(path),
2793                ),
2794                "probe_declared_entry must compose `path` through `root.join` verbatim",
2795            );
2796        }
2797    }
2798
2799    #[test]
2800    fn probe_declared_entry_routes_through_configurable_exists_oracle() {
2801        // Cross-axis pin — the primitive must consult the injected
2802        // [`StandardLayout::with_path_exists`] oracle, not the ambient
2803        // `Path::exists` filesystem probe. Configure a per-path
2804        // discriminator that returns `true` only for one canonical
2805        // resolved path and assert both arms:
2806        // - the "hit" path resolves to `Ok(full)` byte-equal
2807        // - every other path resolves to `MissingEntry` on the same
2808        //   [`StandardLayout`] instance
2809        // The pin traps a regression that reroutes the primitive off
2810        // the injected oracle onto the ambient `Path::exists` (which
2811        // would silently return `false` for every path in `/tmp/x/…`
2812        // and mask the miss-arm hand-off on the hit fixture, or
2813        // silently return `true` for a real system path and mask the
2814        // hit-arm hand-off on the miss fixture).
2815        let root = PathBuf::from("/tmp/x");
2816        let hit_path = Path::new("servicos/keep.computeunit.yaml");
2817        let miss_path = Path::new("servicos/drop.computeunit.yaml");
2818        let hit_full = root.join(hit_path);
2819        let hit_full_probe = hit_full.clone();
2820        let layout = StandardLayout::new().with_path_exists(move |p| p == hit_full_probe);
2821
2822        let resolved = layout
2823            .probe_declared_entry(
2824                hit_path,
2825                &root,
2826                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2827            )
2828            .expect("probe_declared_entry must consult the injected oracle on the hit arm");
2829        assert_eq!(
2830            resolved, hit_full,
2831            "probe_declared_entry hit arm must return the oracle-approved resolved path",
2832        );
2833
2834        let err = layout
2835            .probe_declared_entry(
2836                miss_path,
2837                &root,
2838                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2839            )
2840            .unwrap_err();
2841        assert_eq!(
2842            err,
2843            LayoutError::missing_entry(
2844                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2845                root.join(miss_path),
2846            ),
2847            "probe_declared_entry miss arm must fire when the injected oracle rejects the path",
2848        );
2849    }
2850
2851    // ── StandardLayout::probe_sandboxed_declared_entry substrate primitive ─
2852    //
2853    // The [`StandardLayout::probe_sandboxed_declared_entry`] method
2854    // (`layout.rs`) folds the two self-similar `let full = self.
2855    // probe_declared_entry(p, root, <kind>)?; if !full.starts_with(&<slot>_dir)
2856    // { return Err(LayoutError::<Slot>OutsideDir(full)); }` blocks at
2857    // [`StandardLayout::verify`] (`:exe` iteration, `:servicos` iteration)
2858    // onto one substrate primitive. The pins below (fail-before-pass-after
2859    // by construction — a byte-mismatched primitive body would trip its
2860    // equivalence pin first) lock the fold to its pre-lift open-coded
2861    // shape under `PartialEq`, so every wire-up in
2862    // [`StandardLayout::verify`] on this primitive produces a byte-equal
2863    // `LayoutError` on miss / sandbox-escape and a byte-equal `PathBuf`
2864    // on hit.
2865
2866    #[test]
2867    fn probe_sandboxed_declared_entry_folds_miss_returns_missing_entry() {
2868        // Per-primitive equivalence pin on the miss arm — a
2869        // [`StandardLayout`] whose oracle returns `false` for every
2870        // path yields a `MissingEntry` byte-equal under `PartialEq` to
2871        // the open-coded `LayoutError::missing_entry(<kind>,
2872        // root.join(path))` wrap the sibling
2873        // [`StandardLayout::probe_declared_entry`] primitive routes
2874        // through. Diagnostic-order pin: `MissingEntry` outranks the
2875        // `outside_ctor` sandbox-escape arm on the same iteration, so
2876        // an entry that is both absent *and* outside the sandbox fires
2877        // the `MissingEntry` diagnostic (the pre-lift order the two
2878        // wire-up sites carried).
2879        let layout = StandardLayout::new().with_path_exists(|_| false);
2880        let root = PathBuf::from("/tmp/x");
2881        let path = Path::new("lib/tool");
2882        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
2883        let err = layout
2884            .probe_sandboxed_declared_entry(
2885                path,
2886                &root,
2887                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2888                &exe_dir,
2889                LayoutError::ExeOutsideDir,
2890            )
2891            .unwrap_err();
2892        assert_eq!(
2893            err,
2894            LayoutError::missing_entry(
2895                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2896                root.join(path),
2897            ),
2898            "probe_sandboxed_declared_entry miss arm must produce byte-equal \
2899             LayoutError to the sibling probe_declared_entry primitive's miss wrap, \
2900             preserving the pre-lift MissingEntry-before-<Slot>OutsideDir diagnostic order",
2901        );
2902    }
2903
2904    #[test]
2905    fn probe_sandboxed_declared_entry_folds_sandbox_escape_via_outside_ctor() {
2906        // Per-primitive equivalence pin on the sandbox-escape arm — a
2907        // [`StandardLayout`] whose oracle admits the probed resolved
2908        // path (so the miss arm passes) but whose resolved path lies
2909        // outside the caller-provided `sandbox_dir` yields the paired
2910        // `outside_ctor(full)` byte-equal under `PartialEq`. The
2911        // primitive threads the resolved `full` through the caller-
2912        // supplied `fn(PathBuf) -> LayoutError` constructor rather
2913        // than a hard-coded variant, so the fold does not silently
2914        // collapse onto one of the two `:exe` / `:servicos` outside-
2915        // dir variants.
2916        let root = PathBuf::from("/tmp/x");
2917        let path = Path::new("lib/tool");
2918        let full = root.join(path);
2919        let full_probe = full.clone();
2920        let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
2921        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
2922        let err = layout
2923            .probe_sandboxed_declared_entry(
2924                path,
2925                &root,
2926                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2927                &exe_dir,
2928                LayoutError::ExeOutsideDir,
2929            )
2930            .unwrap_err();
2931        assert_eq!(
2932            err,
2933            LayoutError::ExeOutsideDir(full),
2934            "probe_sandboxed_declared_entry sandbox-escape arm must route the \
2935             resolved `full` through the caller-supplied outside_ctor byte-equal to \
2936             the pre-lift `LayoutError::ExeOutsideDir(full)` tuple-literal",
2937        );
2938    }
2939
2940    #[test]
2941    fn probe_sandboxed_declared_entry_folds_hit_returns_resolved_full() {
2942        // Per-primitive equivalence pin on the hit arm — a
2943        // [`StandardLayout`] whose oracle admits the probed path *and*
2944        // whose resolved path lives inside `sandbox_dir` yields
2945        // `Ok(root.join(path))` byte-equal under `PartialEq`. The
2946        // pre-lift wire-ups discarded the resolved `Ok(PathBuf)` since
2947        // no follow-up per-path gate consumes it after the sandbox
2948        // check; the fold preserves the same hit-arm hand-off through
2949        // the primitive's `Ok(PathBuf)` return so a future consumer
2950        // that wants to run a per-path successor gate reaches the
2951        // resolved path without re-computing `root.join(path)`.
2952        let root = PathBuf::from("/tmp/x");
2953        let path = Path::new("exe/tool");
2954        let full = root.join(path);
2955        let full_probe = full.clone();
2956        let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
2957        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
2958        let resolved = layout
2959            .probe_sandboxed_declared_entry(
2960                path,
2961                &root,
2962                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2963                &exe_dir,
2964                LayoutError::ExeOutsideDir,
2965            )
2966            .expect(
2967                "probe_sandboxed_declared_entry must return Ok(root.join(path)) \
2968                 when the oracle admits the path and it lives inside sandbox_dir",
2969            );
2970        assert_eq!(
2971            resolved, full,
2972            "probe_sandboxed_declared_entry hit arm must return the resolved \
2973             `root.join(path)` byte-equal so a future per-path successor gate \
2974             reaches it without re-computing",
2975        );
2976    }
2977
2978    #[test]
2979    fn probe_sandboxed_declared_entry_threads_outside_ctor_through_arg_verbatim() {
2980        // Cross-axis pin — the primitive must thread the caller-
2981        // supplied `outside_ctor` verbatim into the sandbox-escape
2982        // arm's `LayoutError` return, so the fold does not silently
2983        // collapse onto one hard-coded variant. Sweep both
2984        // [`LayoutError`] tuple-variants the two wire-up sites in
2985        // [`StandardLayout::verify`] pass — [`LayoutError::ExeOutsideDir`]
2986        // and [`LayoutError::ServicoOutsideDir`] — so a byte-drifted
2987        // ctor-routing on either axis would trip.
2988        let root = PathBuf::from("/tmp/x");
2989        let outside_dir = root.join(crate::render::LAYOUT_DIR_LIB);
2990        for (kind, sandbox_component, ctor, expected_variant) in [
2991            (
2992                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2993                crate::render::LAYOUT_DIR_EXE,
2994                LayoutError::ExeOutsideDir as fn(PathBuf) -> LayoutError,
2995                LayoutError::ExeOutsideDir as fn(PathBuf) -> LayoutError,
2996            ),
2997            (
2998                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2999                crate::render::LAYOUT_DIR_SERVICOS,
3000                LayoutError::ServicoOutsideDir as fn(PathBuf) -> LayoutError,
3001                LayoutError::ServicoOutsideDir as fn(PathBuf) -> LayoutError,
3002            ),
3003        ] {
3004            let path = outside_dir.strip_prefix(&root).unwrap().join("tool");
3005            let full = root.join(&path);
3006            let full_probe = full.clone();
3007            let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3008            let sandbox_dir = root.join(sandbox_component);
3009            let err = layout
3010                .probe_sandboxed_declared_entry(&path, &root, kind, &sandbox_dir, ctor)
3011                .unwrap_err();
3012            assert_eq!(
3013                err,
3014                expected_variant(full),
3015                "probe_sandboxed_declared_entry must thread `outside_ctor` verbatim \
3016                 on every canonical `<Slot>OutsideDir` variant the two wire-up sites pass",
3017            );
3018        }
3019    }
3020
3021    #[test]
3022    fn probe_sandboxed_declared_entry_routes_miss_arm_through_probe_declared_entry() {
3023        // Cross-primitive pin — the sandboxed probe must route its
3024        // miss arm through the sibling [`StandardLayout::
3025        // probe_declared_entry`] primitive rather than re-inlining the
3026        // `root.join` + `exists` + `missing_entry` cascade, so a
3027        // future edit to the miss-arm shape on either primitive lands
3028        // in exactly one place. Byte-parity assertion: on a fixture
3029        // that misses the oracle, the sandboxed primitive's `Err`
3030        // arm must equal the peer [`StandardLayout::
3031        // probe_declared_entry`] primitive's `Err` arm on the same
3032        // fixture — otherwise the fold has drifted from the substrate
3033        // primitive.
3034        let layout = StandardLayout::new().with_path_exists(|_| false);
3035        let root = PathBuf::from("/tmp/x");
3036        let path = Path::new("servicos/hello-rio.computeunit.yaml");
3037        let sandbox_dir = root.join(crate::render::LAYOUT_DIR_SERVICOS);
3038        let via_sandboxed = layout
3039            .probe_sandboxed_declared_entry(
3040                path,
3041                &root,
3042                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3043                &sandbox_dir,
3044                LayoutError::ServicoOutsideDir,
3045            )
3046            .unwrap_err();
3047        let via_probe = layout
3048            .probe_declared_entry(
3049                path,
3050                &root,
3051                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3052            )
3053            .unwrap_err();
3054        assert_eq!(
3055            via_sandboxed, via_probe,
3056            "probe_sandboxed_declared_entry's miss arm must equal the sibling \
3057             probe_declared_entry primitive's miss arm byte-equal — pins that the \
3058             sandboxed primitive routes through the substrate primitive rather than \
3059             re-inlining the `root.join` + `exists` + `missing_entry` cascade",
3060        );
3061    }
3062
3063    // ── StandardLayout::probe_declared_entries substrate primitive ───────
3064    //
3065    // The [`StandardLayout::probe_declared_entries`] method (`layout.rs`)
3066    // folds the three self-similar per-slot existence-probe *loop* blocks
3067    // at [`StandardLayout::verify`] (`:bibliotecas` iteration,
3068    // `:behavior` on-disk callback-path iteration, `:upgrade-from` per-
3069    // instruction script-path iteration) onto one substrate primitive.
3070    // The pins below (fail-before-pass-after by construction — a byte-
3071    // mismatched primitive body would trip its equivalence pin first)
3072    // lock the fold to its pre-lift open-coded shape under `PartialEq`,
3073    // so every wire-up in [`StandardLayout::verify`] on this primitive
3074    // produces a byte-equal `LayoutError` on miss (via the sibling
3075    // [`StandardLayout::probe_declared_entry`] miss arm) and a byte-
3076    // equal `Ok(())` on the empty / all-hit arms (the fold's identity
3077    // element on an empty slot list; the pre-lift `for … { … }` loop's
3078    // vacuous pass-through).
3079    //
3080    // Sibling of the peer per-arm-probe [`StandardLayout::probe_declared_entry`]
3081    // (fda1e35) and two-arm-sandboxed [`StandardLayout::
3082    // probe_sandboxed_declared_entry`] (4940d55) primitive test blocks
3083    // — same substrate-primitive discipline extended onto the per-slot
3084    // batch axis these two per-path primitives compose under.
3085
3086    #[test]
3087    fn probe_declared_entries_folds_empty_iterator_returns_ok() {
3088        // Per-primitive identity-element pin on the empty-iterator arm —
3089        // the fold's `Ok(())` return on an empty `IntoIterator` is byte-
3090        // equal to the pre-lift `for _ in <empty> { … }` loop's vacuous
3091        // pass-through. Peer of the peer-primitive `Option::None →
3092        // Ok(())` identity elements the sibling per-Caixa compound
3093        // gates ([`crate::Caixa::validate_limits`] baa4688,
3094        // [`crate::Caixa::validate_behavior`] 0d2877a) each carry on
3095        // the sibling per-slot compound-gate axis.
3096        let layout = StandardLayout::new().with_path_exists(|_| false);
3097        let root = PathBuf::from("/tmp/x");
3098        let empty: [&Path; 0] = [];
3099        layout
3100            .probe_declared_entries(
3101                empty,
3102                &root,
3103                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3104            )
3105            .expect(
3106                "probe_declared_entries must return Ok(()) on an empty iterator \
3107                 — the fold's identity element on an empty per-slot batch",
3108            );
3109    }
3110
3111    #[test]
3112    fn probe_declared_entries_folds_all_hits_returns_ok() {
3113        // Per-primitive equivalence pin on the all-hit arm — a
3114        // [`StandardLayout`] whose oracle admits every path in the batch
3115        // yields `Ok(())` byte-equal to the pre-lift `for p in … {
3116        // self.probe_declared_entry(p, root, kind)?; }` loop's full-
3117        // consumption pass-through.
3118        let root = PathBuf::from("/tmp/x");
3119        let a = root.join("lib/a.lisp");
3120        let b = root.join("lib/b.lisp");
3121        let a_probe = a.clone();
3122        let b_probe = b.clone();
3123        let layout = StandardLayout::new().with_path_exists(move |p| p == a_probe || p == b_probe);
3124        layout
3125            .probe_declared_entries(
3126                [Path::new("lib/a.lisp"), Path::new("lib/b.lisp")],
3127                &root,
3128                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3129            )
3130            .expect(
3131                "probe_declared_entries must return Ok(()) when every path in the \
3132                 batch is admitted by the oracle",
3133            );
3134    }
3135
3136    #[test]
3137    fn probe_declared_entries_folds_first_miss_short_circuits_via_probe_declared_entry() {
3138        // Per-primitive equivalence pin on the first-miss arm — a
3139        // [`StandardLayout`] whose oracle admits the first path and
3140        // rejects the second yields a `MissingEntry` byte-equal under
3141        // `PartialEq` to the sibling [`StandardLayout::
3142        // probe_declared_entry`] primitive's miss wrap on the *second*
3143        // path (the pre-lift `for … { … ? }` loop's first-error return
3144        // semantics), *not* on the first (admitted) path.
3145        let root = PathBuf::from("/tmp/x");
3146        let hit = root.join("lib/a.lisp");
3147        let hit_probe = hit.clone();
3148        let layout = StandardLayout::new().with_path_exists(move |p| p == hit_probe);
3149        let miss = Path::new("lib/b.lisp");
3150        let err = layout
3151            .probe_declared_entries(
3152                [Path::new("lib/a.lisp"), miss],
3153                &root,
3154                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3155            )
3156            .unwrap_err();
3157        assert_eq!(
3158            err,
3159            LayoutError::missing_entry(
3160                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3161                root.join(miss),
3162            ),
3163            "probe_declared_entries must short-circuit on the first missing entry \
3164             with a `MissingEntry` byte-equal to the sibling probe_declared_entry \
3165             primitive's miss wrap on that entry — the pre-lift `for … {{ … ? }}` \
3166             loop's first-error return semantics",
3167        );
3168    }
3169
3170    #[test]
3171    fn probe_declared_entries_folds_first_miss_short_circuits_before_later_paths() {
3172        // Diagnostic-order pin — the primitive must return on the *first*
3173        // miss in iterator order rather than probing every path and
3174        // returning the last miss (which would silently drop the pre-
3175        // lift `for … { … ? }` loop's first-error contract). Fixture:
3176        // the oracle rejects both paths, so a byte-equal `MissingEntry`
3177        // on the *first* path in the iterator distinguishes the two
3178        // return-order shapes.
3179        let layout = StandardLayout::new().with_path_exists(|_| false);
3180        let root = PathBuf::from("/tmp/x");
3181        let first = Path::new("lib/first.lisp");
3182        let second = Path::new("lib/second.lisp");
3183        let err = layout
3184            .probe_declared_entries(
3185                [first, second],
3186                &root,
3187                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3188            )
3189            .unwrap_err();
3190        assert_eq!(
3191            err,
3192            LayoutError::missing_entry(
3193                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3194                root.join(first),
3195            ),
3196            "probe_declared_entries must return on the *first* miss in iterator order \
3197             — a `MissingEntry` on the second path would silently drop the pre-lift \
3198             `for … {{ … ? }}` loop's first-error contract",
3199        );
3200    }
3201
3202    #[test]
3203    fn probe_declared_entries_threads_kind_through_arg_verbatim() {
3204        // Cross-axis pin — sweep every canonical
3205        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] label the three
3206        // batch wire-up sites in [`StandardLayout::verify`] pass through
3207        // this primitive (`:bibliotecas`, `:behavior` callback,
3208        // `:upgrade-from` script). Each miss must return a `MissingEntry`
3209        // whose `kind:` field byte-equals the caller-provided arg, so
3210        // the fold does not silently collapse onto one hard-coded label.
3211        // Sibling of the peer `probe_declared_entry_threads_kind_through_arg_verbatim`
3212        // pin's five-label sweep on the per-arm primitive.
3213        let layout = StandardLayout::new().with_path_exists(|_| false);
3214        let root = PathBuf::from("/tmp/x");
3215        let path = Path::new("some/entry");
3216        for kind in [
3217            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3218            crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
3219            crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
3220        ] {
3221            let err = layout
3222                .probe_declared_entries([path], &root, kind)
3223                .unwrap_err();
3224            assert_eq!(
3225                err,
3226                LayoutError::missing_entry(kind, root.join(path)),
3227                "probe_declared_entries must thread `kind` verbatim on every canonical \
3228                 batch label",
3229            );
3230        }
3231    }
3232
3233    #[test]
3234    fn probe_declared_entries_accepts_asref_path_shape_wire_up_sites_pass() {
3235        // Cross-axis pin — the primitive's `P: AsRef<Path>` bound must
3236        // admit every concrete iterator element type the three
3237        // [`StandardLayout::verify`] wire-up sites pass:
3238        // - `&String` (from `caixa.bibliotecas(): &[String]`),
3239        // - `&Path` (from `b.declared_paths(): impl Iterator<Item = &Path>`),
3240        // - `&PathBuf` (from the flattened `.filter_map(_::declared_path)`
3241        //   on `:upgrade-from` instructions, whose `declared_path`
3242        //   returns `Option<&PathBuf>`).
3243        //
3244        // Byte-parity assertion: on a shared `/tmp/x/lib/demo.lisp`
3245        // fixture the miss-arm return must be byte-equal across all
3246        // three element-type flavors, so a future re-shape of the
3247        // bound (a narrower `P: Into<PathBuf>` collapse, a stricter
3248        // `&Path`-only signature) would surface here rather than at
3249        // the caller wire-up site.
3250        let layout = StandardLayout::new().with_path_exists(|_| false);
3251        let root = PathBuf::from("/tmp/x");
3252        let literal = "lib/demo.lisp";
3253        let expected = LayoutError::missing_entry(
3254            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3255            root.join(Path::new(literal)),
3256        );
3257
3258        let via_string_slice: Vec<String> = vec![literal.to_string()];
3259        let via_string_err = layout
3260            .probe_declared_entries(
3261                via_string_slice.iter(),
3262                &root,
3263                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3264            )
3265            .unwrap_err();
3266        assert_eq!(
3267            via_string_err, expected,
3268            "probe_declared_entries must accept `&String` items (the `caixa.bibliotecas() \
3269             : &[String]` wire-up shape)",
3270        );
3271
3272        let via_path_slice: [&Path; 1] = [Path::new(literal)];
3273        let via_path_err = layout
3274            .probe_declared_entries(
3275                via_path_slice,
3276                &root,
3277                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3278            )
3279            .unwrap_err();
3280        assert_eq!(
3281            via_path_err, expected,
3282            "probe_declared_entries must accept `&Path` items (the `b.declared_paths() \
3283             : impl Iterator<Item = &Path>` wire-up shape)",
3284        );
3285
3286        let via_pathbuf_slice: Vec<PathBuf> = vec![PathBuf::from(literal)];
3287        let via_pathbuf_err = layout
3288            .probe_declared_entries(
3289                via_pathbuf_slice.iter(),
3290                &root,
3291                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3292            )
3293            .unwrap_err();
3294        assert_eq!(
3295            via_pathbuf_err, expected,
3296            "probe_declared_entries must accept `&PathBuf` items (the `.filter_map( \
3297             _::declared_path)` on `:upgrade-from` instructions wire-up shape)",
3298        );
3299    }
3300
3301    #[test]
3302    fn probe_declared_entries_routes_miss_arm_through_probe_declared_entry() {
3303        // Cross-primitive pin — the batch primitive must route its miss
3304        // arm through the sibling [`StandardLayout::probe_declared_entry`]
3305        // primitive rather than re-inlining the `root.join` + `exists`
3306        // + `missing_entry` cascade, so a future edit to the miss-arm
3307        // shape on either primitive lands in exactly one place. Byte-
3308        // parity assertion: on a fixture that misses the oracle, the
3309        // batch primitive's `Err` arm must equal the peer per-arm
3310        // [`StandardLayout::probe_declared_entry`] primitive's `Err` arm
3311        // on the same fixture — otherwise the batch fold has drifted
3312        // from the substrate primitive. Same discipline the peer
3313        // `probe_sandboxed_declared_entry_routes_miss_arm_through_probe_declared_entry`
3314        // pin establishes on the sibling sandboxed-primitive axis.
3315        let layout = StandardLayout::new().with_path_exists(|_| false);
3316        let root = PathBuf::from("/tmp/x");
3317        let path = Path::new("lib/demo.lisp");
3318        let via_batch = layout
3319            .probe_declared_entries(
3320                [path],
3321                &root,
3322                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3323            )
3324            .unwrap_err();
3325        let via_probe = layout
3326            .probe_declared_entry(
3327                path,
3328                &root,
3329                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3330            )
3331            .unwrap_err();
3332        assert_eq!(
3333            via_batch, via_probe,
3334            "probe_declared_entries's miss arm must equal the sibling probe_declared_entry \
3335             primitive's miss arm byte-equal — pins that the batch primitive routes \
3336             through the substrate primitive rather than re-inlining the `root.join` + \
3337             `exists` + `missing_entry` cascade",
3338        );
3339    }
3340
3341    // ── StandardLayout::probe_sandboxed_declared_entries substrate primitive ─
3342    //
3343    // The [`StandardLayout::probe_sandboxed_declared_entries`] method
3344    // (`layout.rs`) folds the two self-similar per-slot sandboxed-
3345    // existence-probe *loop* blocks at [`StandardLayout::verify`]
3346    // (`:exe` iteration, `:servicos` iteration) onto one substrate
3347    // primitive. The pins below (fail-before-pass-after by construction
3348    // — a byte-mismatched primitive body would trip its equivalence pin
3349    // first) lock the fold to its pre-lift open-coded shape under
3350    // `PartialEq`, so every wire-up in [`StandardLayout::verify`] on
3351    // this primitive produces a byte-equal `LayoutError` on miss / on
3352    // sandbox-escape (via the sibling
3353    // [`StandardLayout::probe_sandboxed_declared_entry`] arms) and a
3354    // byte-equal `Ok(())` on the empty / all-hit arms (the fold's
3355    // identity element on an empty slot list; the pre-lift `for … {
3356    // … }` loop's vacuous pass-through).
3357    //
3358    // Sibling of the peer per-slot batch bare-probe
3359    // [`StandardLayout::probe_declared_entries`] (d1ccb0b), per-arm
3360    // bare-probe [`StandardLayout::probe_declared_entry`] (fda1e35), and
3361    // per-arm sandboxed-probe
3362    // [`StandardLayout::probe_sandboxed_declared_entry`] (4940d55)
3363    // primitive test blocks — same substrate-primitive discipline
3364    // extended onto the fourth and last quadrant of the
3365    // (per-arm | per-slot batch) × (bare | sandboxed) existence-probe
3366    // algebra.
3367
3368    #[test]
3369    fn probe_sandboxed_declared_entries_folds_empty_iterator_returns_ok() {
3370        // Per-primitive identity-element pin on the empty-iterator arm —
3371        // the fold's `Ok(())` return on an empty `IntoIterator` is byte-
3372        // equal to the pre-lift `for _ in <empty> { … }` loop's vacuous
3373        // pass-through. Peer of the sibling
3374        // `probe_declared_entries_folds_empty_iterator_returns_ok` pin
3375        // on the bare-batch axis and the `Option::None → Ok(())`
3376        // identity elements the per-Caixa compound gates
3377        // ([`crate::Caixa::validate_limits`] baa4688,
3378        // [`crate::Caixa::validate_behavior`] 0d2877a) each carry on the
3379        // per-slot compound-gate axis.
3380        let layout = StandardLayout::new().with_path_exists(|_| false);
3381        let root = PathBuf::from("/tmp/x");
3382        let empty: [&Path; 0] = [];
3383        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3384        layout
3385            .probe_sandboxed_declared_entries(
3386                empty,
3387                &root,
3388                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3389                &exe_dir,
3390                LayoutError::ExeOutsideDir,
3391            )
3392            .expect(
3393                "probe_sandboxed_declared_entries must return Ok(()) on an empty iterator \
3394                 — the fold's identity element on an empty per-slot sandboxed batch",
3395            );
3396    }
3397
3398    #[test]
3399    fn probe_sandboxed_declared_entries_folds_all_hits_returns_ok() {
3400        // Per-primitive equivalence pin on the all-hit arm — a
3401        // [`StandardLayout`] whose oracle admits every path in the batch
3402        // *and* whose resolved paths live inside `sandbox_dir` yields
3403        // `Ok(())` byte-equal to the pre-lift `for p in … {
3404        // self.probe_sandboxed_declared_entry(p, root, kind,
3405        // &sandbox_dir, outside_ctor)?; }` loop's full-consumption
3406        // pass-through.
3407        let root = PathBuf::from("/tmp/x");
3408        let a = root.join("exe/a");
3409        let b = root.join("exe/b");
3410        let a_probe = a.clone();
3411        let b_probe = b.clone();
3412        let layout = StandardLayout::new().with_path_exists(move |p| p == a_probe || p == b_probe);
3413        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3414        layout
3415            .probe_sandboxed_declared_entries(
3416                [Path::new("exe/a"), Path::new("exe/b")],
3417                &root,
3418                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3419                &exe_dir,
3420                LayoutError::ExeOutsideDir,
3421            )
3422            .expect(
3423                "probe_sandboxed_declared_entries must return Ok(()) when every path in \
3424                 the batch is admitted by the oracle and lives inside sandbox_dir",
3425            );
3426    }
3427
3428    #[test]
3429    fn probe_sandboxed_declared_entries_folds_first_miss_short_circuits_via_probe_sandboxed_declared_entry()
3430     {
3431        // Per-primitive equivalence pin on the first-miss arm — a
3432        // [`StandardLayout`] whose oracle admits the first path (inside
3433        // sandbox_dir) and rejects the second yields a `MissingEntry`
3434        // byte-equal under `PartialEq` to the sibling
3435        // [`StandardLayout::probe_sandboxed_declared_entry`] primitive's
3436        // miss wrap on the *second* path (the pre-lift `for … { … ? }`
3437        // loop's first-error return semantics), *not* on the first
3438        // (admitted) path.
3439        let root = PathBuf::from("/tmp/x");
3440        let hit = root.join("exe/a");
3441        let hit_probe = hit.clone();
3442        let layout = StandardLayout::new().with_path_exists(move |p| p == hit_probe);
3443        let miss = Path::new("exe/b");
3444        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3445        let err = layout
3446            .probe_sandboxed_declared_entries(
3447                [Path::new("exe/a"), miss],
3448                &root,
3449                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3450                &exe_dir,
3451                LayoutError::ExeOutsideDir,
3452            )
3453            .unwrap_err();
3454        assert_eq!(
3455            err,
3456            LayoutError::missing_entry(
3457                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3458                root.join(miss),
3459            ),
3460            "probe_sandboxed_declared_entries must short-circuit on the first missing \
3461             entry with a `MissingEntry` byte-equal to the sibling \
3462             probe_sandboxed_declared_entry primitive's miss wrap on that entry — the \
3463             pre-lift `for … {{ … ? }}` loop's first-error return semantics",
3464        );
3465    }
3466
3467    #[test]
3468    fn probe_sandboxed_declared_entries_folds_first_miss_short_circuits_before_later_paths() {
3469        // Diagnostic-order pin — the primitive must return on the *first*
3470        // miss in iterator order rather than probing every path and
3471        // returning the last miss (which would silently drop the pre-
3472        // lift `for … { … ? }` loop's first-error contract). Fixture:
3473        // the oracle rejects both paths, so a byte-equal `MissingEntry`
3474        // on the *first* path in the iterator distinguishes the two
3475        // return-order shapes. Sibling of the peer
3476        // `probe_declared_entries_folds_first_miss_short_circuits_before_later_paths`
3477        // pin on the bare-batch axis.
3478        let layout = StandardLayout::new().with_path_exists(|_| false);
3479        let root = PathBuf::from("/tmp/x");
3480        let first = Path::new("exe/first");
3481        let second = Path::new("exe/second");
3482        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3483        let err = layout
3484            .probe_sandboxed_declared_entries(
3485                [first, second],
3486                &root,
3487                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3488                &exe_dir,
3489                LayoutError::ExeOutsideDir,
3490            )
3491            .unwrap_err();
3492        assert_eq!(
3493            err,
3494            LayoutError::missing_entry(
3495                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3496                root.join(first),
3497            ),
3498            "probe_sandboxed_declared_entries must return on the *first* miss in iterator \
3499             order — a `MissingEntry` on the second path would silently drop the pre-lift \
3500             `for … {{ … ? }}` loop's first-error contract",
3501        );
3502    }
3503
3504    #[test]
3505    fn probe_sandboxed_declared_entries_folds_sandbox_escape_via_outside_ctor() {
3506        // Per-primitive equivalence pin on the sandbox-escape arm — a
3507        // [`StandardLayout`] whose oracle admits the probed resolved
3508        // path (so the miss arm passes) but whose resolved path lies
3509        // outside the caller-provided `sandbox_dir` yields the paired
3510        // `outside_ctor(full)` byte-equal under `PartialEq`. The
3511        // primitive threads the resolved `full` through the caller-
3512        // supplied `fn(PathBuf) -> LayoutError` constructor rather than
3513        // a hard-coded variant, so the fold does not silently collapse
3514        // onto one of the two `:exe` / `:servicos` outside-dir variants.
3515        let root = PathBuf::from("/tmp/x");
3516        let path = Path::new("lib/tool");
3517        let full = root.join(path);
3518        let full_probe = full.clone();
3519        let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3520        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3521        let err = layout
3522            .probe_sandboxed_declared_entries(
3523                [path],
3524                &root,
3525                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3526                &exe_dir,
3527                LayoutError::ExeOutsideDir,
3528            )
3529            .unwrap_err();
3530        assert_eq!(
3531            err,
3532            LayoutError::ExeOutsideDir(full),
3533            "probe_sandboxed_declared_entries sandbox-escape arm must route the resolved \
3534             `full` through the caller-supplied outside_ctor byte-equal to the pre-lift \
3535             `LayoutError::ExeOutsideDir(full)` tuple-literal",
3536        );
3537    }
3538
3539    #[test]
3540    fn probe_sandboxed_declared_entries_threads_outside_ctor_through_arg_verbatim() {
3541        // Cross-axis pin — the primitive must thread the caller-supplied
3542        // `outside_ctor` verbatim into the sandbox-escape arm's
3543        // `LayoutError` return, so the fold does not silently collapse
3544        // onto one hard-coded variant. Sweep both [`LayoutError`]
3545        // tuple-variants the two wire-up sites in
3546        // [`StandardLayout::verify`] pass — [`LayoutError::ExeOutsideDir`]
3547        // and [`LayoutError::ServicoOutsideDir`] — so a byte-drifted
3548        // ctor-routing on either axis would trip. Sibling of the peer
3549        // `probe_sandboxed_declared_entry_threads_outside_ctor_through_arg_verbatim`
3550        // pin on the per-arm sandboxed-primitive axis.
3551        let root = PathBuf::from("/tmp/x");
3552        let outside_dir = root.join(crate::render::LAYOUT_DIR_LIB);
3553        for (kind, sandbox_component, ctor) in [
3554            (
3555                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3556                crate::render::LAYOUT_DIR_EXE,
3557                LayoutError::ExeOutsideDir as fn(PathBuf) -> LayoutError,
3558            ),
3559            (
3560                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3561                crate::render::LAYOUT_DIR_SERVICOS,
3562                LayoutError::ServicoOutsideDir as fn(PathBuf) -> LayoutError,
3563            ),
3564        ] {
3565            let path = outside_dir.strip_prefix(&root).unwrap().join("tool");
3566            let full = root.join(&path);
3567            let full_probe = full.clone();
3568            let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3569            let sandbox_dir = root.join(sandbox_component);
3570            let err = layout
3571                .probe_sandboxed_declared_entries([&path], &root, kind, &sandbox_dir, ctor)
3572                .unwrap_err();
3573            assert_eq!(
3574                err,
3575                ctor(full),
3576                "probe_sandboxed_declared_entries must thread `outside_ctor` verbatim on \
3577                 every canonical `<Slot>OutsideDir` variant the two wire-up sites pass",
3578            );
3579        }
3580    }
3581
3582    #[test]
3583    fn probe_sandboxed_declared_entries_threads_kind_through_arg_verbatim() {
3584        // Cross-axis pin — sweep every canonical
3585        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] label the two
3586        // sandboxed-batch wire-up sites in [`StandardLayout::verify`]
3587        // pass through this primitive (`:exe`, `:servicos`). Each miss
3588        // must return a `MissingEntry` whose `kind:` field byte-equals
3589        // the caller-provided arg, so the fold does not silently
3590        // collapse onto one hard-coded label. Sibling of the peer
3591        // `probe_declared_entries_threads_kind_through_arg_verbatim`
3592        // pin's label sweep on the bare-batch axis.
3593        let layout = StandardLayout::new().with_path_exists(|_| false);
3594        let root = PathBuf::from("/tmp/x");
3595        let path = Path::new("some/entry");
3596        for (kind, sandbox_component, ctor) in [
3597            (
3598                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3599                crate::render::LAYOUT_DIR_EXE,
3600                LayoutError::ExeOutsideDir as fn(PathBuf) -> LayoutError,
3601            ),
3602            (
3603                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3604                crate::render::LAYOUT_DIR_SERVICOS,
3605                LayoutError::ServicoOutsideDir as fn(PathBuf) -> LayoutError,
3606            ),
3607        ] {
3608            let sandbox_dir = root.join(sandbox_component);
3609            let err = layout
3610                .probe_sandboxed_declared_entries([path], &root, kind, &sandbox_dir, ctor)
3611                .unwrap_err();
3612            assert_eq!(
3613                err,
3614                LayoutError::missing_entry(kind, root.join(path)),
3615                "probe_sandboxed_declared_entries must thread `kind` verbatim on every \
3616                 canonical sandboxed-batch label",
3617            );
3618        }
3619    }
3620
3621    #[test]
3622    fn probe_sandboxed_declared_entries_accepts_asref_path_shape_wire_up_sites_pass() {
3623        // Cross-axis pin — the primitive's `P: AsRef<Path>` bound must
3624        // admit every concrete iterator element type the two
3625        // [`StandardLayout::verify`] wire-up sites pass:
3626        // - `&String` (from `caixa.exe(): &[String]`),
3627        // - `&String` (from `caixa.servicos(): &[String]`).
3628        //
3629        // Byte-parity assertion: on a shared `/tmp/x/lib/demo`
3630        // fixture the miss-arm return must be byte-equal across
3631        // `&String` and the ergonomic `&Path` / `&PathBuf` element-type
3632        // flavors, so a future re-shape of the bound (a narrower `P:
3633        // Into<PathBuf>` collapse, a stricter `&Path`-only signature)
3634        // would surface here rather than at the caller wire-up site.
3635        // Sibling of the peer
3636        // `probe_declared_entries_accepts_asref_path_shape_wire_up_sites_pass`
3637        // pin on the bare-batch axis.
3638        let layout = StandardLayout::new().with_path_exists(|_| false);
3639        let root = PathBuf::from("/tmp/x");
3640        let literal = "exe/demo";
3641        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3642        let expected = LayoutError::missing_entry(
3643            crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3644            root.join(Path::new(literal)),
3645        );
3646
3647        let via_string_slice: Vec<String> = vec![literal.to_string()];
3648        let via_string_err = layout
3649            .probe_sandboxed_declared_entries(
3650                via_string_slice.iter(),
3651                &root,
3652                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3653                &exe_dir,
3654                LayoutError::ExeOutsideDir,
3655            )
3656            .unwrap_err();
3657        assert_eq!(
3658            via_string_err, expected,
3659            "probe_sandboxed_declared_entries must accept `&String` items (the `caixa.exe() \
3660             : &[String]` / `caixa.servicos(): &[String]` wire-up shape)",
3661        );
3662
3663        let via_path_slice: [&Path; 1] = [Path::new(literal)];
3664        let via_path_err = layout
3665            .probe_sandboxed_declared_entries(
3666                via_path_slice,
3667                &root,
3668                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3669                &exe_dir,
3670                LayoutError::ExeOutsideDir,
3671            )
3672            .unwrap_err();
3673        assert_eq!(
3674            via_path_err, expected,
3675            "probe_sandboxed_declared_entries must accept `&Path` items",
3676        );
3677
3678        let via_pathbuf_slice: Vec<PathBuf> = vec![PathBuf::from(literal)];
3679        let via_pathbuf_err = layout
3680            .probe_sandboxed_declared_entries(
3681                via_pathbuf_slice.iter(),
3682                &root,
3683                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3684                &exe_dir,
3685                LayoutError::ExeOutsideDir,
3686            )
3687            .unwrap_err();
3688        assert_eq!(
3689            via_pathbuf_err, expected,
3690            "probe_sandboxed_declared_entries must accept `&PathBuf` items",
3691        );
3692    }
3693
3694    #[test]
3695    fn probe_sandboxed_declared_entries_routes_miss_arm_through_probe_sandboxed_declared_entry() {
3696        // Cross-primitive pin — the batch primitive must route its miss
3697        // arm through the sibling
3698        // [`StandardLayout::probe_sandboxed_declared_entry`] primitive
3699        // rather than re-inlining the `probe_declared_entry` +
3700        // `starts_with` cascade, so a future edit to the miss-arm shape
3701        // on either primitive lands in exactly one place. Byte-parity
3702        // assertion: on a fixture that misses the oracle, the batch
3703        // primitive's `Err` arm must equal the peer per-arm
3704        // [`StandardLayout::probe_sandboxed_declared_entry`] primitive's
3705        // `Err` arm on the same fixture — otherwise the batch fold has
3706        // drifted from the substrate primitive. Same discipline the peer
3707        // `probe_declared_entries_routes_miss_arm_through_probe_declared_entry`
3708        // pin establishes on the sibling bare-batch axis.
3709        let layout = StandardLayout::new().with_path_exists(|_| false);
3710        let root = PathBuf::from("/tmp/x");
3711        let path = Path::new("servicos/hello-rio.computeunit.yaml");
3712        let sandbox_dir = root.join(crate::render::LAYOUT_DIR_SERVICOS);
3713        let via_batch = layout
3714            .probe_sandboxed_declared_entries(
3715                [path],
3716                &root,
3717                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3718                &sandbox_dir,
3719                LayoutError::ServicoOutsideDir,
3720            )
3721            .unwrap_err();
3722        let via_probe = layout
3723            .probe_sandboxed_declared_entry(
3724                path,
3725                &root,
3726                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3727                &sandbox_dir,
3728                LayoutError::ServicoOutsideDir,
3729            )
3730            .unwrap_err();
3731        assert_eq!(
3732            via_batch, via_probe,
3733            "probe_sandboxed_declared_entries's miss arm must equal the sibling \
3734             probe_sandboxed_declared_entry primitive's miss arm byte-equal — pins that \
3735             the batch primitive routes through the substrate primitive rather than \
3736             re-inlining the `probe_declared_entry` + `starts_with` cascade",
3737        );
3738    }
3739
3740    #[test]
3741    fn probe_sandboxed_declared_entries_routes_sandbox_escape_arm_through_probe_sandboxed_declared_entry()
3742     {
3743        // Cross-primitive pin — the batch primitive's sandbox-escape arm
3744        // must equal the sibling
3745        // [`StandardLayout::probe_sandboxed_declared_entry`] primitive's
3746        // sandbox-escape arm on the same fixture. Byte-parity assertion:
3747        // on a fixture where the oracle admits the probed path but the
3748        // resolved path lies outside sandbox_dir, both primitives must
3749        // return the same `outside_ctor(full)` byte-equal under
3750        // `PartialEq` — otherwise the batch fold has drifted from the
3751        // per-arm primitive on the second-arm dispatch.
3752        let root = PathBuf::from("/tmp/x");
3753        let path = Path::new("lib/escape");
3754        let full = root.join(path);
3755        let full_probe = full.clone();
3756        let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3757        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3758        let via_batch = layout
3759            .probe_sandboxed_declared_entries(
3760                [path],
3761                &root,
3762                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3763                &exe_dir,
3764                LayoutError::ExeOutsideDir,
3765            )
3766            .unwrap_err();
3767        let via_probe = layout
3768            .probe_sandboxed_declared_entry(
3769                path,
3770                &root,
3771                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3772                &exe_dir,
3773                LayoutError::ExeOutsideDir,
3774            )
3775            .unwrap_err();
3776        assert_eq!(
3777            via_batch, via_probe,
3778            "probe_sandboxed_declared_entries's sandbox-escape arm must equal the sibling \
3779             probe_sandboxed_declared_entry primitive's sandbox-escape arm byte-equal — \
3780             pins that the batch primitive routes through the substrate primitive on both \
3781             the miss and sandbox-escape arms rather than re-inlining either cascade",
3782        );
3783    }
3784
3785    // ── LayoutError::<nome-only> constructor family ──────────────────────
3786    //
3787    // The [`layout_nome_only_ctors!`] macro (below the `LayoutError` enum
3788    // definition) generates one static constructor per `<Variant>(String)`
3789    // tuple-variant that folds the uniform `Self::<Variant>(caixa.nome()
3790    // .to_string())` one-field construction onto one substrate primitive.
3791    // The per-variant equivalence pins below (fail-before-pass-after by
3792    // construction — a byte-mismatched macro arm would trip its
3793    // equivalence pin first) lock each generated constructor to its
3794    // tuple-literal peer under `PartialEq`, so every wire-up in
3795    // [`StandardLayout::verify`] on that variant produces a byte-equal
3796    // `LayoutError` to the pre-lift open-coded tuple-literal. The
3797    // cross-axis pin that follows (non-default `:nome`) routes the sole
3798    // constructor input axis through its declared accessor, so the fold
3799    // does not silently collapse onto the fixture default `:nome`.
3800
3801    fn layout_nome_only_ctor_fixture() -> Caixa {
3802        caixa(CaixaKind::Biblioteca)
3803    }
3804
3805    // Same rationale as `assert_violation_ctor_matches` / `assert_slot_
3806    // kind_ctor_matches` above: the helper terminates on the equality
3807    // check, so the owned-arg lint's general API-shape target does not
3808    // apply.
3809    #[allow(clippy::needless_pass_by_value)]
3810    fn assert_nome_only_ctor_matches(actual: LayoutError, expected: LayoutError) {
3811        assert_eq!(
3812            actual, expected,
3813            "generated constructor must produce byte-equal LayoutError to open-coded tuple-literal wrap",
3814        );
3815    }
3816
3817    #[test]
3818    fn binario_without_exe_ctor_matches_tuple_literal_wrap() {
3819        let c = layout_nome_only_ctor_fixture();
3820        assert_nome_only_ctor_matches(
3821            LayoutError::binario_without_exe(&c),
3822            LayoutError::BinarioWithoutExe(c.nome().to_string()),
3823        );
3824    }
3825
3826    #[test]
3827    fn servico_without_servicos_ctor_matches_tuple_literal_wrap() {
3828        let c = layout_nome_only_ctor_fixture();
3829        assert_nome_only_ctor_matches(
3830            LayoutError::servico_without_servicos(&c),
3831            LayoutError::ServicoWithoutServicos(c.nome().to_string()),
3832        );
3833    }
3834
3835    #[test]
3836    fn missing_ci_ctor_matches_tuple_literal_wrap() {
3837        let c = layout_nome_only_ctor_fixture();
3838        assert_nome_only_ctor_matches(
3839            LayoutError::missing_ci(&c),
3840            LayoutError::MissingCi(c.nome().to_string()),
3841        );
3842    }
3843
3844    #[test]
3845    fn supervisor_owns_code_ctor_matches_tuple_literal_wrap() {
3846        let c = layout_nome_only_ctor_fixture();
3847        assert_nome_only_ctor_matches(
3848            LayoutError::supervisor_owns_code(&c),
3849            LayoutError::SupervisorOwnsCode(c.nome().to_string()),
3850        );
3851    }
3852
3853    #[test]
3854    fn aplicacao_owns_code_ctor_matches_tuple_literal_wrap() {
3855        let c = layout_nome_only_ctor_fixture();
3856        assert_nome_only_ctor_matches(
3857            LayoutError::aplicacao_owns_code(&c),
3858            LayoutError::AplicacaoOwnsCode(c.nome().to_string()),
3859        );
3860    }
3861
3862    #[test]
3863    fn acao_owns_code_ctor_matches_tuple_literal_wrap() {
3864        let c = layout_nome_only_ctor_fixture();
3865        assert_nome_only_ctor_matches(
3866            LayoutError::acao_owns_code(&c),
3867            LayoutError::AcaoOwnsCode(c.nome().to_string()),
3868        );
3869    }
3870
3871    #[test]
3872    fn nome_only_ctor_routes_caixa_through_nome_accessor() {
3873        // Pin the fold's `caixa.nome().to_string()` sole-field construction
3874        // against a non-default `:nome` — the accessor threads the caller's
3875        // `:nome` verbatim into the tuple-variant, so the fold does not
3876        // silently collapse onto the default `"demo"` fixture nome. Peer of
3877        // the sibling `violation_ctor_routes_caixa_prefix_through_nome_
3878        // accessor` / `slot_kind_ctor_routes_caixa_prefix_through_nome_
3879        // accessor` pins on the `{ caixa, issue }` / `{ caixa, kind,
3880        // slots }` envelopes; extended here onto the sixth
3881        // `<Variant>(String)` envelope so every LayoutError-shape ctor
3882        // family guarantees the `:nome`-derived-caixa slot routes through
3883        // [`Caixa::nome`] rather than a hard-coded string. Sweeps the six
3884        // ctors in the [`layout_nome_only_ctors!`] macro so the pin covers
3885        // every generated arm.
3886        let mut c = caixa(CaixaKind::Biblioteca);
3887        c.nome = "alt-nome".into();
3888        assert_eq!(
3889            LayoutError::binario_without_exe(&c),
3890            LayoutError::BinarioWithoutExe("alt-nome".to_string()),
3891        );
3892        assert_eq!(
3893            LayoutError::servico_without_servicos(&c),
3894            LayoutError::ServicoWithoutServicos("alt-nome".to_string()),
3895        );
3896        assert_eq!(
3897            LayoutError::missing_ci(&c),
3898            LayoutError::MissingCi("alt-nome".to_string()),
3899        );
3900        assert_eq!(
3901            LayoutError::supervisor_owns_code(&c),
3902            LayoutError::SupervisorOwnsCode("alt-nome".to_string()),
3903        );
3904        assert_eq!(
3905            LayoutError::aplicacao_owns_code(&c),
3906            LayoutError::AplicacaoOwnsCode("alt-nome".to_string()),
3907        );
3908        assert_eq!(
3909            LayoutError::acao_owns_code(&c),
3910            LayoutError::AcaoOwnsCode("alt-nome".to_string()),
3911        );
3912    }
3913
3914    #[test]
3915    fn biblioteca_needs_default_lib_path() {
3916        let root = PathBuf::from("/tmp/x");
3917        let expect_manifest = root.join("caixa.lisp");
3918        let layout = StandardLayout::new().with_path_exists(move |p| p == expect_manifest);
3919        let err = layout
3920            .verify(&caixa(CaixaKind::Biblioteca), &root)
3921            .unwrap_err();
3922        assert!(matches!(err, LayoutError::MissingLib { .. }));
3923    }
3924
3925    #[test]
3926    fn biblioteca_passes_when_default_lib_exists() {
3927        let root = PathBuf::from("/tmp/x");
3928        let manifest = root.join("caixa.lisp");
3929        let default_lib = root.join("lib").join("demo.lisp");
3930        let layout =
3931            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
3932        layout
3933            .verify(&caixa(CaixaKind::Biblioteca), &root)
3934            .expect("should pass");
3935    }
3936
3937    #[test]
3938    fn binario_without_exe_errors() {
3939        let root = PathBuf::from("/tmp/x");
3940        let manifest = root.join("caixa.lisp");
3941        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3942        let err = layout
3943            .verify(&caixa(CaixaKind::Binario), &root)
3944            .unwrap_err();
3945        assert!(matches!(err, LayoutError::BinarioWithoutExe(_)));
3946    }
3947
3948    #[test]
3949    fn exe_outside_dir_errors() {
3950        // A relative entry that lives under the caixa root but *not*
3951        // under `exe/` — the canonical case the `starts_with(exe_dir)`
3952        // fence catches. The prior parent-escape shape this test used
3953        // (`"../sibling/tool"`) is now caught at validate time by
3954        // [`Caixa::validate_code_paths`] with the narrower
3955        // [`crate::ManifestError::CodePathParentEscape`] diagnostic
3956        // (see the layout-level integration pin
3957        // `code_path_violation_on_parent_escape_fires_before_existence_check`),
3958        // so this fence pin uses a non-`..` non-absolute shape outside
3959        // `exe/` to preserve coverage of the ExeOutsideDir surface.
3960        let root = PathBuf::from("/tmp/x");
3961        let manifest = root.join("caixa.lisp");
3962        let outside = root.join("lib/tool");
3963        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == outside);
3964        let mut c = caixa(CaixaKind::Binario);
3965        c.exe = vec!["lib/tool".into()];
3966        let err = layout.verify(&c, &root).unwrap_err();
3967        assert!(matches!(err, LayoutError::ExeOutsideDir(_)));
3968    }
3969
3970    // ── code-path shape gate (lifted to layout-level verify) ─────────────
3971
3972    #[test]
3973    fn code_path_violation_on_empty_bibliotecas_entry() {
3974        let root = PathBuf::from("/tmp/x");
3975        let manifest = root.join("caixa.lisp");
3976        let default_lib = root.join("lib").join("demo.lisp");
3977        let layout =
3978            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
3979        let mut c = caixa(CaixaKind::Biblioteca);
3980        c.bibliotecas = vec![String::new()];
3981        let err = layout.verify(&c, &root).unwrap_err();
3982        // The wire-up wraps `ManifestError` Display into the
3983        // CodePathViolation envelope (peer of LimitsViolation /
3984        // BehaviorViolation / UpgradeViolation), so the issue string
3985        // names the offending slot at the source.
3986        let LayoutError::CodePathViolation { caixa, issue } = err else {
3987            panic!("expected LayoutError::CodePathViolation, got {err:?}");
3988        };
3989        assert_eq!(caixa, "demo");
3990        assert!(
3991            issue.contains(":bibliotecas"),
3992            "issue must name the offending slot: {issue}",
3993        );
3994    }
3995
3996    #[test]
3997    fn code_path_violation_on_absolute_servicos_entry() {
3998        let root = PathBuf::from("/tmp/x");
3999        let manifest = root.join("caixa.lisp");
4000        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
4001        let mut c = caixa(CaixaKind::Servico);
4002        c.servicos = vec!["/etc/servicos/escape.yaml".into()];
4003        let err = layout.verify(&c, &root).unwrap_err();
4004        let LayoutError::CodePathViolation { caixa, issue } = err else {
4005            panic!("expected LayoutError::CodePathViolation, got {err:?}");
4006        };
4007        assert_eq!(caixa, "demo");
4008        assert!(
4009            issue.contains(":servicos"),
4010            "issue must name the offending slot: {issue}",
4011        );
4012        assert!(
4013            issue.contains("/etc/servicos/escape.yaml"),
4014            "issue must quote the offending path: {issue}",
4015        );
4016    }
4017
4018    #[test]
4019    fn code_path_violation_on_parent_escape_fires_before_existence_check() {
4020        // The new gate runs BEFORE the existence loops, so a
4021        // parent-escaping `:exe` entry surfaces CodePathViolation
4022        // (naming `:exe` at the source) rather than the downstream
4023        // ExeOutsideDir / MissingEntry against the resolved sandbox-
4024        // escape path. Even if the resolved escape target exists
4025        // on disk (which we simulate here by claiming it does), the
4026        // shape diagnostic wins.
4027        let root = PathBuf::from("/tmp/x");
4028        let manifest = root.join("caixa.lisp");
4029        let resolved_escape = root.join("exe/../../escape.lisp");
4030        let layout =
4031            StandardLayout::new().with_path_exists(move |p| p == manifest || p == resolved_escape);
4032        let mut c = caixa(CaixaKind::Binario);
4033        c.exe = vec!["exe/../../escape.lisp".into()];
4034        let err = layout.verify(&c, &root).unwrap_err();
4035        let LayoutError::CodePathViolation { caixa, issue } = err else {
4036            panic!("expected LayoutError::CodePathViolation, got {err:?}");
4037        };
4038        assert_eq!(caixa, "demo");
4039        assert!(
4040            issue.contains(":exe"),
4041            "issue must name the offending slot: {issue}",
4042        );
4043    }
4044
4045    // ── etiquetas universal-axis gate wired into verify ─────────────────
4046    //
4047    // Pins the layout-pipeline wire-up of [`Caixa::validate_etiquetas`]:
4048    // the fourth universal-axis Caixa-level value-shape gate (peer of
4049    // `validate_nome` / `validate_versao` / `validate_deps` /
4050    // `validate_code_paths`), wired before the kind-coherence gates so
4051    // a structurally-invalid `:etiquetas` entry on any kind surfaces
4052    // the per-axis `EtiquetasViolation { caixa, issue }` envelope at
4053    // the source rather than silently rendering as `keywords: [""]`
4054    // in `Chart.yaml` (Servico kind, via caixa-helm's `BTreeSet`
4055    // collect) or silently dedup'ing at chart render (every kind).
4056    // Until this wire-up landed `:etiquetas` had no shape gate at any
4057    // layer — the registry-search-tag axis was the largest universal
4058    // authoring surface on the typed Caixa surface with no validate
4059    // discipline.
4060    //
4061    // Same per-axis `*Violation { caixa, issue }` envelope every peer
4062    // per-axis wrap exposes; the wire-up runs after `validate_deps`
4063    // (universal axis ordering: `:nome` → `:versao` → `:deps` →
4064    // `:etiquetas`) and before every kind-coherence gate
4065    // (`:etiquetas` is universal so its shape diagnostic is more
4066    // fundamental than the partition-on-kind diagnostics).
4067
4068    #[test]
4069    fn etiquetas_violation_on_empty_entry() {
4070        // Canonical paste-from-blank-doc footgun on every kind. The
4071        // wrap envelope wraps [`ManifestError::EtiquetaEmpty`]'s
4072        // Display through verbatim, so the issue string names the
4073        // offending `:etiquetas` axis at the source — the author can
4074        // grep their caixa.lisp for `:etiquetas` and fix the empty
4075        // entry in one edit. Mirrors the peer
4076        // `code_path_violation_on_empty_bibliotecas_entry` shape
4077        // (b868442) on the `:bibliotecas` axis.
4078        let root = PathBuf::from("/tmp/x");
4079        let manifest = root.join("caixa.lisp");
4080        let default_lib = root.join("lib").join("demo.lisp");
4081        let layout =
4082            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4083        let mut c = caixa(CaixaKind::Biblioteca);
4084        c.etiquetas = vec![String::new()];
4085        let err = layout.verify(&c, &root).unwrap_err();
4086        let LayoutError::EtiquetasViolation { caixa, issue } = err else {
4087            panic!("expected LayoutError::EtiquetasViolation, got {err:?}");
4088        };
4089        assert_eq!(caixa, "demo");
4090        assert!(
4091            issue.contains(":etiquetas"),
4092            "issue must name the offending slot: {issue}",
4093        );
4094    }
4095
4096    #[test]
4097    fn etiquetas_violation_on_duplicate_entry() {
4098        // Canonical copy-paste-the-wrong-tag footgun. Without the wire-
4099        // up the duplicate was silently dedup'd by caixa-helm's
4100        // `BTreeSet` collect at chart render — a "second wins / one
4101        // silently disappears" shape. The wrap envelope names the
4102        // offending tag verbatim through the inner
4103        // [`ManifestError::EtiquetaDuplicate`]'s Display.
4104        let root = PathBuf::from("/tmp/x");
4105        let manifest = root.join("caixa.lisp");
4106        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
4107        let mut c = caixa(CaixaKind::Servico);
4108        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
4109        c.etiquetas = vec!["demo".into(), "demo".into()];
4110        // The servicos path doesn't exist in this fixture, but the
4111        // `:etiquetas` gate fires before the existence loop (universal
4112        // axis dominates kind-specific existence checks). Wire is
4113        // intact iff the wrap envelope surfaces first.
4114        let err = layout.verify(&c, &root).unwrap_err();
4115        let LayoutError::EtiquetasViolation { caixa, issue } = err else {
4116            panic!("expected LayoutError::EtiquetasViolation, got {err:?}");
4117        };
4118        assert_eq!(caixa, "demo");
4119        assert!(
4120            issue.contains("demo"),
4121            "issue must quote the offending tag: {issue}",
4122        );
4123    }
4124
4125    #[test]
4126    fn etiquetas_violation_fires_before_kind_coherence_mesh_slot() {
4127        // Cross-axis precedence pin: a Biblioteca with malformed
4128        // `:etiquetas` *and* declared mesh slots (`:membros`) surfaces
4129        // the universal `:etiquetas` diagnostic first, not the
4130        // kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4131        // `:etiquetas` is universal (every kind owns the slot), so its
4132        // shape diagnostic is more fundamental than the partition-on-
4133        // kind diagnostic. Mirrors the peer
4134        // `deps_violation_fires_before_*` precedence pins (aa77d0f) on
4135        // the universal `:deps` axis vs the same kind-coherence gates.
4136        let root = PathBuf::from("/tmp/x");
4137        let manifest = root.join("caixa.lisp");
4138        let default_lib = root.join("lib").join("demo.lisp");
4139        let layout =
4140            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4141        let mut c = caixa(CaixaKind::Biblioteca);
4142        c.etiquetas = vec![String::new()];
4143        c.membros = vec![crate::aplicacao::Membro {
4144            caixa: "x".into(),
4145            versao: "^0.1".into(),
4146        }];
4147        let err = layout.verify(&c, &root).unwrap_err();
4148        assert!(
4149            matches!(err, LayoutError::EtiquetasViolation { .. }),
4150            "got {err:?}",
4151        );
4152    }
4153
4154    #[test]
4155    fn etiquetas_violation_fires_after_deps_violation() {
4156        // Cross-axis precedence pin (inside the universal-axis trio):
4157        // a caixa with both a malformed `:deps` entry *and* a malformed
4158        // `:etiquetas` entry surfaces `DepsViolation` first — `:deps`
4159        // is the third universal axis in declaration order
4160        // (`:nome` → `:versao` → `:deps` → `:etiquetas`) and runs first
4161        // in `verify`. Mirrors the peer
4162        // `nome_violation_fires_before_versao_violation` shape on the
4163        // identity-axis pair.
4164        let root = PathBuf::from("/tmp/x");
4165        let manifest = root.join("caixa.lisp");
4166        let default_lib = root.join("lib").join("demo.lisp");
4167        let layout =
4168            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4169        let mut c = caixa(CaixaKind::Biblioteca);
4170        c.deps = vec![crate::Dep::simple("Caixa-Teia", "^0.1")]; // uppercase :nome
4171        c.etiquetas = vec![String::new()];
4172        let err = layout.verify(&c, &root).unwrap_err();
4173        assert!(
4174            matches!(err, LayoutError::DepsViolation { .. }),
4175            "got {err:?}",
4176        );
4177    }
4178
4179    #[test]
4180    fn etiquetas_violation_accepts_canonical_template() {
4181        // Positive control sanity pin: the canonical `Caixa::template`
4182        // shape (`:etiquetas ()` — empty list) passes the gate
4183        // trivially. Mirrors the peer
4184        // `validate_code_paths_accepts_canonical_template` pin.
4185        let root = PathBuf::from("/tmp/x");
4186        let manifest = root.join("caixa.lisp");
4187        let default_lib = root.join("lib").join("demo.lisp");
4188        let layout =
4189            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4190        let c = caixa(CaixaKind::Biblioteca);
4191        layout.verify(&c, &root).expect("template must pass");
4192    }
4193
4194    #[test]
4195    fn etiquetas_violation_on_non_chart_keyword_shape() {
4196        // Canonical CSV-list-separator-confusion footgun: the author
4197        // confused the CSV-style separator with the `:etiquetas` list
4198        // grammar. The shape gate fires past the empty + duplicate
4199        // arms via [`Caixa::validate_etiquetas`]'s new
4200        // `is_chart_keyword_shape` cascade, and the layout envelope
4201        // wraps [`ManifestError::EtiquetaInvalid`]'s Display through
4202        // verbatim — the issue string names both the offending slot
4203        // and the offending value (debug-escaped). Peer with the
4204        // `autores_violation_on_non_chart_maintainer_shape` pin on
4205        // the sibling universal-axis `Vec<String>` surface — the
4206        // second layout pin on the Vec<String> per-entry shape
4207        // cascade.
4208        let root = PathBuf::from("/tmp/x");
4209        let manifest = root.join("caixa.lisp");
4210        let default_lib = root.join("lib").join("demo.lisp");
4211        let layout =
4212            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4213        let mut c = caixa(CaixaKind::Biblioteca);
4214        c.etiquetas = vec!["mesh,http,grpc".into()];
4215        let err = layout.verify(&c, &root).unwrap_err();
4216        let LayoutError::EtiquetasViolation { caixa, issue } = err else {
4217            panic!("expected LayoutError::EtiquetasViolation, got {err:?}");
4218        };
4219        assert_eq!(caixa, "demo");
4220        assert!(
4221            issue.contains(":etiquetas"),
4222            "issue must name the offending slot: {issue}",
4223        );
4224        assert!(
4225            issue.contains("mesh,http,grpc"),
4226            "issue must quote the offending value: {issue}",
4227        );
4228    }
4229
4230    // ── autores universal-axis gate wired into verify ───────────────────
4231    //
4232    // Pins the layout-pipeline wire-up of [`Caixa::validate_autores`]:
4233    // the fifth universal-axis Caixa-level value-shape gate (peer of
4234    // `validate_nome` / `validate_versao` / `validate_deps` /
4235    // `validate_etiquetas` / `validate_code_paths`), wired immediately
4236    // after `validate_etiquetas` so the two Vec-shaped universal
4237    // metadata axes sit adjacent in the cascade. Until this wire-up
4238    // landed `:autores` had no shape gate at any layer — the
4239    // maintainer-axis was the second largest universal authoring
4240    // surface on the typed Caixa surface with no validate discipline,
4241    // and unlike `:etiquetas` (caixa-helm dedups the rendered
4242    // `keywords:` array via `BTreeSet` collect at chart render),
4243    // `maintainers:` has *no* renderer-side dedup, so duplicate
4244    // `:autores` entries render verbatim as two identical
4245    // `Maintainer { name, email: None }` records — a strictly worse
4246    // footgun than the peer `:etiquetas` shape.
4247
4248    #[test]
4249    fn autores_violation_on_empty_entry() {
4250        // Canonical paste-from-blank-doc footgun on every kind. The
4251        // wrap envelope wraps [`ManifestError::AutorEmpty`]'s Display
4252        // through verbatim, so the issue string names the offending
4253        // `:autores` axis at the source — the author can grep their
4254        // caixa.lisp for `:autores` and fix the empty entry in one
4255        // edit. Mirrors the peer `etiquetas_violation_on_empty_entry`
4256        // shape (360a499) on the `:etiquetas` axis.
4257        let root = PathBuf::from("/tmp/x");
4258        let manifest = root.join("caixa.lisp");
4259        let default_lib = root.join("lib").join("demo.lisp");
4260        let layout =
4261            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4262        let mut c = caixa(CaixaKind::Biblioteca);
4263        c.autores = vec![String::new()];
4264        let err = layout.verify(&c, &root).unwrap_err();
4265        let LayoutError::AutoresViolation { caixa, issue } = err else {
4266            panic!("expected LayoutError::AutoresViolation, got {err:?}");
4267        };
4268        assert_eq!(caixa, "demo");
4269        assert!(
4270            issue.contains(":autores"),
4271            "issue must name the offending slot: {issue}",
4272        );
4273    }
4274
4275    #[test]
4276    fn autores_violation_on_duplicate_entry() {
4277        // Canonical copy-paste-the-wrong-author footgun. Unlike the
4278        // peer `:etiquetas` axis (silently dedup'd by caixa-helm's
4279        // `BTreeSet` collect at chart render), `:autores` duplicates
4280        // stack verbatim in the rendered `maintainers:` — the gate
4281        // closes the footgun at validate time before any renderer
4282        // sees it. The wrap envelope names the offending author
4283        // verbatim through the inner [`ManifestError::AutorDuplicate`]'s
4284        // Display.
4285        let root = PathBuf::from("/tmp/x");
4286        let manifest = root.join("caixa.lisp");
4287        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
4288        let mut c = caixa(CaixaKind::Servico);
4289        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
4290        c.autores = vec!["pleme-io".into(), "pleme-io".into()];
4291        // The servicos path doesn't exist in this fixture, but the
4292        // `:autores` gate fires before the existence loop (universal
4293        // axis dominates kind-specific existence checks). Wire is
4294        // intact iff the wrap envelope surfaces first.
4295        let err = layout.verify(&c, &root).unwrap_err();
4296        let LayoutError::AutoresViolation { caixa, issue } = err else {
4297            panic!("expected LayoutError::AutoresViolation, got {err:?}");
4298        };
4299        assert_eq!(caixa, "demo");
4300        assert!(
4301            issue.contains("pleme-io"),
4302            "issue must quote the offending author: {issue}",
4303        );
4304    }
4305
4306    #[test]
4307    fn autores_violation_fires_before_kind_coherence_mesh_slot() {
4308        // Cross-axis precedence pin: a Biblioteca with malformed
4309        // `:autores` *and* declared mesh slots (`:membros`) surfaces
4310        // the universal `:autores` diagnostic first, not the
4311        // kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4312        // `:autores` is universal (every kind owns the slot), so its
4313        // shape diagnostic is more fundamental than the partition-on-
4314        // kind diagnostic. Mirrors the peer
4315        // `etiquetas_violation_fires_before_kind_coherence_mesh_slot`
4316        // pin (360a499) on the `:etiquetas` axis vs the same kind-
4317        // coherence gates.
4318        let root = PathBuf::from("/tmp/x");
4319        let manifest = root.join("caixa.lisp");
4320        let default_lib = root.join("lib").join("demo.lisp");
4321        let layout =
4322            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4323        let mut c = caixa(CaixaKind::Biblioteca);
4324        c.autores = vec![String::new()];
4325        c.membros = vec![crate::aplicacao::Membro {
4326            caixa: "x".into(),
4327            versao: "^0.1".into(),
4328        }];
4329        let err = layout.verify(&c, &root).unwrap_err();
4330        assert!(
4331            matches!(err, LayoutError::AutoresViolation { .. }),
4332            "got {err:?}",
4333        );
4334    }
4335
4336    #[test]
4337    fn autores_violation_fires_after_etiquetas_violation() {
4338        // Cross-axis precedence pin (inside the Vec-shaped universal
4339        // metadata pair): a caixa with both a malformed `:etiquetas`
4340        // entry *and* a malformed `:autores` entry surfaces
4341        // `EtiquetasViolation` first — `:etiquetas` is the fourth
4342        // universal axis in the cascade and runs before `:autores`,
4343        // peer with the canonical identity-axis-first cascade the
4344        // peer gates establish. Mirrors the peer
4345        // `etiquetas_violation_fires_after_deps_violation` precedence
4346        // pin (360a499) on the dep-axis-before-tag-axis pair.
4347        let root = PathBuf::from("/tmp/x");
4348        let manifest = root.join("caixa.lisp");
4349        let default_lib = root.join("lib").join("demo.lisp");
4350        let layout =
4351            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4352        let mut c = caixa(CaixaKind::Biblioteca);
4353        c.etiquetas = vec![String::new()];
4354        c.autores = vec![String::new()];
4355        let err = layout.verify(&c, &root).unwrap_err();
4356        assert!(
4357            matches!(err, LayoutError::EtiquetasViolation { .. }),
4358            "got {err:?}",
4359        );
4360    }
4361
4362    #[test]
4363    fn autores_violation_accepts_canonical_template() {
4364        // Positive control sanity pin: the canonical `Caixa::template`
4365        // shape (`:autores ()` — empty list) passes the gate trivially.
4366        // Mirrors the peer `etiquetas_violation_accepts_canonical_template`
4367        // pin (360a499).
4368        let root = PathBuf::from("/tmp/x");
4369        let manifest = root.join("caixa.lisp");
4370        let default_lib = root.join("lib").join("demo.lisp");
4371        let layout =
4372            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4373        let c = caixa(CaixaKind::Biblioteca);
4374        layout.verify(&c, &root).expect("template must pass");
4375    }
4376
4377    #[test]
4378    fn autores_violation_on_non_chart_maintainer_shape() {
4379        // Canonical paste-from-multiline-doc footgun: the author
4380        // pasted a multi-line block of author records into one
4381        // `:autores` entry instead of splitting into one entry per
4382        // author. The shape gate fires past the empty + duplicate arms
4383        // via [`Caixa::validate_autores`]'s new
4384        // `is_chart_maintainer_name_shape` cascade, and the layout
4385        // envelope wraps [`ManifestError::AutorInvalid`]'s Display
4386        // through verbatim — the issue string names both the offending
4387        // slot and the offending value (debug-escaped). Peer with the
4388        // `descricao_violation_on_non_chart_shape` pin on the sibling
4389        // universal-axis `Option<String>` surface and the
4390        // `licenca_violation_on_non_spdx_shape` /
4391        // `edicao_violation_on_non_year_shape` peers — and the first
4392        // layout pin on the Vec<String> per-entry shape cascade.
4393        let root = PathBuf::from("/tmp/x");
4394        let manifest = root.join("caixa.lisp");
4395        let default_lib = root.join("lib").join("demo.lisp");
4396        let layout =
4397            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4398        let mut c = caixa(CaixaKind::Biblioteca);
4399        c.autores = vec!["alice\nbob".into()];
4400        let err = layout.verify(&c, &root).unwrap_err();
4401        let LayoutError::AutoresViolation { caixa, issue } = err else {
4402            panic!("expected LayoutError::AutoresViolation, got {err:?}");
4403        };
4404        assert_eq!(caixa, "demo");
4405        assert!(
4406            issue.contains(":autores"),
4407            "issue must name the offending slot: {issue}",
4408        );
4409        assert!(
4410            issue.contains("alice\\nbob"),
4411            "issue must quote the offending value (debug-escaped): {issue}",
4412        );
4413    }
4414
4415    // ── repositorio universal-axis gate wired into verify ────────────────
4416    //
4417    // Pins the layout-pipeline wire-up of [`Caixa::validate_repositorio`]:
4418    // the sixth universal-axis Caixa-level value-shape gate (peer of
4419    // `validate_nome` / `validate_versao` / `validate_deps` /
4420    // `validate_etiquetas` / `validate_autores` / `validate_code_paths`),
4421    // wired immediately after `validate_autores` so the universal
4422    // git-URL axis sits adjacent to the two Vec-shaped universal
4423    // metadata axes (`:etiquetas`, `:autores`) in the cascade. Until
4424    // this wire-up landed `:repositorio` had no shape gate at any
4425    // layer — the universal git-shaped homepage axis was the third
4426    // largest universal authoring surface on the typed Caixa with no
4427    // validate discipline, routing the same string through two
4428    // load-bearing substrate consumers (`caixa-helm`'s `Chart.yaml
4429    // home:` field and `caixa-flux`'s FluxCD `GitRepository.spec.url`)
4430    // via `Option::unwrap_or_else` fallbacks that only fire on `None` —
4431    // a `Some("")` silently passed every fallback and rendered as an
4432    // empty URL in both consumers, breaking at `helm template` /
4433    // FluxCD reconcile time far from the source `caixa.lisp`. The
4434    // gate closes the divergence and makes the two `git URL`-shaped
4435    // surfaces on the typed Caixa (`:repositorio` here, `:deps :fonte
4436    // :repo` peer routed through the same shared
4437    // `crate::render::is_git_repo_url` predicate) structurally
4438    // equivalent by construction.
4439
4440    #[test]
4441    fn repositorio_violation_on_empty_some() {
4442        // Canonical paste-from-blank-doc footgun on every kind. The
4443        // wrap envelope wraps [`ManifestError::RepositorioEmpty`]'s
4444        // Display through verbatim, so the issue string names the
4445        // offending `:repositorio` axis at the source — the author
4446        // can grep their caixa.lisp for `:repositorio ""` and fix the
4447        // empty value in one edit. Mirrors the peer
4448        // `autores_violation_on_empty_entry` shape (86c769b) on the
4449        // `:autores` axis.
4450        let root = PathBuf::from("/tmp/x");
4451        let manifest = root.join("caixa.lisp");
4452        let default_lib = root.join("lib").join("demo.lisp");
4453        let layout =
4454            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4455        let mut c = caixa(CaixaKind::Biblioteca);
4456        c.repositorio = Some(String::new());
4457        let err = layout.verify(&c, &root).unwrap_err();
4458        let LayoutError::RepositorioViolation { caixa, issue } = err else {
4459            panic!("expected LayoutError::RepositorioViolation, got {err:?}");
4460        };
4461        assert_eq!(caixa, "demo");
4462        assert!(
4463            issue.contains(":repositorio"),
4464            "issue must name the offending slot: {issue}",
4465        );
4466    }
4467
4468    #[test]
4469    fn repositorio_violation_on_malformed_shape() {
4470        // Canonical CLI-argument-injection footgun: a leading `-`
4471        // value (`-upload-pack=evil`) escapes the `git clone <repo>`
4472        // subprocess argument boundary at clone time. The shared
4473        // `is_git_repo_url` predicate — the same parser the peer
4474        // `:deps :fonte :repo` axis routes through via
4475        // `DepSource::validate` — refuses every leading-`-` shape at
4476        // validate time. The wrap envelope names the offending value
4477        // verbatim through the inner [`ManifestError::RepositorioInvalid`]'s
4478        // Display.
4479        let root = PathBuf::from("/tmp/x");
4480        let manifest = root.join("caixa.lisp");
4481        let default_lib = root.join("lib").join("demo.lisp");
4482        let layout =
4483            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4484        let mut c = caixa(CaixaKind::Biblioteca);
4485        c.repositorio = Some("-upload-pack=evil".into());
4486        let err = layout.verify(&c, &root).unwrap_err();
4487        let LayoutError::RepositorioViolation { caixa, issue } = err else {
4488            panic!("expected LayoutError::RepositorioViolation, got {err:?}");
4489        };
4490        assert_eq!(caixa, "demo");
4491        assert!(
4492            issue.contains("-upload-pack=evil"),
4493            "issue must quote the offending value: {issue}",
4494        );
4495    }
4496
4497    #[test]
4498    fn repositorio_violation_fires_before_kind_coherence_mesh_slot() {
4499        // Cross-axis precedence pin: a Biblioteca with malformed
4500        // `:repositorio` *and* declared mesh slots (`:membros`)
4501        // surfaces the universal `:repositorio` diagnostic first, not
4502        // the kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4503        // `:repositorio` is universal (every kind owns the slot), so
4504        // its shape diagnostic is more fundamental than the
4505        // partition-on-kind diagnostic. Mirrors the peer
4506        // `autores_violation_fires_before_kind_coherence_mesh_slot`
4507        // pin (86c769b) on the `:autores` axis vs the same
4508        // kind-coherence gates.
4509        let root = PathBuf::from("/tmp/x");
4510        let manifest = root.join("caixa.lisp");
4511        let default_lib = root.join("lib").join("demo.lisp");
4512        let layout =
4513            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4514        let mut c = caixa(CaixaKind::Biblioteca);
4515        c.repositorio = Some(String::new());
4516        c.membros = vec![crate::aplicacao::Membro {
4517            caixa: "x".into(),
4518            versao: "^0.1".into(),
4519        }];
4520        let err = layout.verify(&c, &root).unwrap_err();
4521        assert!(
4522            matches!(err, LayoutError::RepositorioViolation { .. }),
4523            "got {err:?}",
4524        );
4525    }
4526
4527    #[test]
4528    fn repositorio_violation_fires_after_autores_violation() {
4529        // Cross-axis precedence pin (inside the universal metadata
4530        // trio): a caixa with both a malformed `:autores` entry *and*
4531        // a malformed `:repositorio` value surfaces `AutoresViolation`
4532        // first — `:autores` is the fifth universal axis in the
4533        // cascade and runs before `:repositorio`, peer with the
4534        // canonical identity-axis-first cascade the peer gates
4535        // establish. Mirrors the peer
4536        // `autores_violation_fires_after_etiquetas_violation`
4537        // precedence pin (86c769b) on the tag-axis-before-author-axis
4538        // pair.
4539        let root = PathBuf::from("/tmp/x");
4540        let manifest = root.join("caixa.lisp");
4541        let default_lib = root.join("lib").join("demo.lisp");
4542        let layout =
4543            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4544        let mut c = caixa(CaixaKind::Biblioteca);
4545        c.autores = vec![String::new()];
4546        c.repositorio = Some(String::new());
4547        let err = layout.verify(&c, &root).unwrap_err();
4548        assert!(
4549            matches!(err, LayoutError::AutoresViolation { .. }),
4550            "got {err:?}",
4551        );
4552    }
4553
4554    #[test]
4555    fn repositorio_violation_accepts_canonical_template() {
4556        // Positive control sanity pin: the canonical `Caixa::template`
4557        // shape (omits `:repositorio` entirely → `None` on the typed
4558        // surface) passes the gate trivially — the gate is a no-op
4559        // when the author didn't author a value. Mirrors the peer
4560        // `autores_violation_accepts_canonical_template` pin (86c769b).
4561        let root = PathBuf::from("/tmp/x");
4562        let manifest = root.join("caixa.lisp");
4563        let default_lib = root.join("lib").join("demo.lisp");
4564        let layout =
4565            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4566        let c = caixa(CaixaKind::Biblioteca);
4567        layout.verify(&c, &root).expect("template must pass");
4568    }
4569
4570    #[test]
4571    fn repositorio_violation_accepts_canonical_github_shorthand() {
4572        // Positive control pin on the canonical pleme-io `:repositorio`
4573        // shape: the `github:org/repo` shorthand the README quickstart
4574        // and the `caixa-helm` / `caixa-mesh` / `caixa-flux` fixtures
4575        // all use passes the gate end-to-end. Closes the structural
4576        // equivalence between this surface and the peer `:deps :fonte
4577        // :repo` axis — both consume `crate::render::is_git_repo_url`
4578        // and both must agree on the same accepted shape set.
4579        let root = PathBuf::from("/tmp/x");
4580        let manifest = root.join("caixa.lisp");
4581        let default_lib = root.join("lib").join("demo.lisp");
4582        let layout =
4583            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4584        let mut c = caixa(CaixaKind::Biblioteca);
4585        c.repositorio = Some("github:pleme-io/hello-rio".into());
4586        layout.verify(&c, &root).expect("canonical shape must pass");
4587    }
4588
4589    // ── descricao universal-axis gate wired into verify ──────────────────
4590    //
4591    // Pins the layout-pipeline wire-up of [`Caixa::validate_descricao`]:
4592    // the seventh universal-axis Caixa-level value-shape gate (peer of
4593    // `validate_nome` / `validate_versao` / `validate_deps` /
4594    // `validate_etiquetas` / `validate_autores` / `validate_repositorio` /
4595    // `validate_code_paths`), wired immediately after `validate_repositorio`
4596    // so the universal free-form-prose axis sits adjacent to the
4597    // universal git-URL axis in the cascade. Until this wire-up landed
4598    // `:descricao` had no shape gate at any layer — the empty
4599    // `Some("")` silently passed both `caixa-helm` consumers'
4600    // `Option::unwrap_or_else(|| <fallback>)` (which only fire on
4601    // `None`) and rendered as `Chart.yaml description: ""` plus a
4602    // blank `README.md` header, breaking at `helm lint` time
4603    // (`WARNING [chart.metadata.description]: description is required`
4604    // on `apiVersion: v2` charts) far from the source `caixa.lisp`.
4605    // Closes the same `Some("")` skips-`unwrap_or_else` footgun the
4606    // peer `:repositorio` gate (577b0a9) closed, on the universal
4607    // free-form-prose summary axis.
4608
4609    #[test]
4610    fn descricao_violation_on_empty_some() {
4611        // Canonical paste-from-blank-doc footgun on every kind. The
4612        // wrap envelope wraps [`ManifestError::DescricaoEmpty`]'s
4613        // Display through verbatim, so the issue string names the
4614        // offending `:descricao` axis at the source — the author can
4615        // grep their caixa.lisp for `:descricao ""` and fix the empty
4616        // value in one edit. Mirrors the peer
4617        // `repositorio_violation_on_empty_some` shape (577b0a9) on
4618        // the sibling `Option<String>` `:repositorio` axis.
4619        let root = PathBuf::from("/tmp/x");
4620        let manifest = root.join("caixa.lisp");
4621        let default_lib = root.join("lib").join("demo.lisp");
4622        let layout =
4623            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4624        let mut c = caixa(CaixaKind::Biblioteca);
4625        c.descricao = Some(String::new());
4626        let err = layout.verify(&c, &root).unwrap_err();
4627        let LayoutError::DescricaoViolation { caixa, issue } = err else {
4628            panic!("expected LayoutError::DescricaoViolation, got {err:?}");
4629        };
4630        assert_eq!(caixa, "demo");
4631        assert!(
4632            issue.contains(":descricao"),
4633            "issue must name the offending slot: {issue}",
4634        );
4635    }
4636
4637    #[test]
4638    fn descricao_violation_fires_before_kind_coherence_mesh_slot() {
4639        // Cross-axis precedence pin: a Biblioteca with empty
4640        // `:descricao` *and* declared mesh slots (`:membros`)
4641        // surfaces the universal `:descricao` diagnostic first, not
4642        // the kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4643        // `:descricao` is universal (every kind owns the slot), so
4644        // its shape diagnostic is more fundamental than the
4645        // partition-on-kind diagnostic. Mirrors the peer
4646        // `repositorio_violation_fires_before_kind_coherence_mesh_slot`
4647        // pin (577b0a9) on the `:repositorio` axis vs the same
4648        // kind-coherence gates.
4649        let root = PathBuf::from("/tmp/x");
4650        let manifest = root.join("caixa.lisp");
4651        let default_lib = root.join("lib").join("demo.lisp");
4652        let layout =
4653            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4654        let mut c = caixa(CaixaKind::Biblioteca);
4655        c.descricao = Some(String::new());
4656        c.membros = vec![crate::aplicacao::Membro {
4657            caixa: "x".into(),
4658            versao: "^0.1".into(),
4659        }];
4660        let err = layout.verify(&c, &root).unwrap_err();
4661        assert!(
4662            matches!(err, LayoutError::DescricaoViolation { .. }),
4663            "got {err:?}",
4664        );
4665    }
4666
4667    #[test]
4668    fn descricao_violation_fires_after_repositorio_violation() {
4669        // Cross-axis precedence pin (inside the universal metadata
4670        // cascade): a caixa with both a malformed `:repositorio` *and*
4671        // an empty `:descricao` surfaces `RepositorioViolation`
4672        // first — `:repositorio` is the sixth universal axis in the
4673        // cascade and runs before `:descricao`, peer with the
4674        // canonical identity-axis-first cascade the peer gates
4675        // establish. Mirrors the peer
4676        // `repositorio_violation_fires_after_autores_violation`
4677        // precedence pin (577b0a9) on the autores-axis-before-
4678        // repositorio-axis pair.
4679        let root = PathBuf::from("/tmp/x");
4680        let manifest = root.join("caixa.lisp");
4681        let default_lib = root.join("lib").join("demo.lisp");
4682        let layout =
4683            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4684        let mut c = caixa(CaixaKind::Biblioteca);
4685        c.repositorio = Some(String::new());
4686        c.descricao = Some(String::new());
4687        let err = layout.verify(&c, &root).unwrap_err();
4688        assert!(
4689            matches!(err, LayoutError::RepositorioViolation { .. }),
4690            "got {err:?}",
4691        );
4692    }
4693
4694    #[test]
4695    fn descricao_violation_accepts_none() {
4696        // Positive control sanity pin: a caixa that omits
4697        // `:descricao` entirely (the canonical `Caixa::template` shape
4698        // carries `Some("FIXME — describe this caixa")`, but the
4699        // layout-test fixture defaults to `None`) passes the gate
4700        // trivially — the gate is a no-op when the author didn't
4701        // author a value. Mirrors the peer
4702        // `repositorio_violation_accepts_canonical_template` pin
4703        // (577b0a9).
4704        let root = PathBuf::from("/tmp/x");
4705        let manifest = root.join("caixa.lisp");
4706        let default_lib = root.join("lib").join("demo.lisp");
4707        let layout =
4708            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4709        let c = caixa(CaixaKind::Biblioteca);
4710        layout.verify(&c, &root).expect("None must pass");
4711    }
4712
4713    #[test]
4714    fn descricao_violation_accepts_canonical_summary() {
4715        // Positive control pin on the canonical pleme-io `:descricao`
4716        // shape: a short free-form prose summary the `caixa-helm` /
4717        // `caixa-flux` / `caixa-mesh` fixtures all carry passes the
4718        // gate end-to-end.
4719        let root = PathBuf::from("/tmp/x");
4720        let manifest = root.join("caixa.lisp");
4721        let default_lib = root.join("lib").join("demo.lisp");
4722        let layout =
4723            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4724        let mut c = caixa(CaixaKind::Biblioteca);
4725        c.descricao = Some("Canonical Rust→wasm32-wasip2 caixa Servico.".into());
4726        layout
4727            .verify(&c, &root)
4728            .expect("canonical summary must pass");
4729    }
4730
4731    #[test]
4732    fn descricao_violation_on_non_chart_shape() {
4733        // Shape-predicate wire-up pin: a malformed `:descricao` value
4734        // that's a non-empty `Some(s)` but carries a paste-from-
4735        // multiline-doc embedded newline surfaces the
4736        // `DescricaoViolation` envelope via the manifest-layer
4737        // `ManifestError::DescricaoInvalid` arm. Mirrors the peer
4738        // `descricao_violation_on_empty_some` shape on the empty arm
4739        // of the same axis and the peer
4740        // `licenca_violation_on_non_spdx_shape` shape on the sibling
4741        // `:licenca` axis. Until this gate landed a value like
4742        // `"Checkout\nflow."` (an embedded newline) or `"Checkout
4743        // flow. "` (a trailing whitespace) silently passed
4744        // `StandardLayout::verify` and landed in the rendered
4745        // Chart.yaml `description:` field as a YAML-illegal
4746        // multi-line scalar or a silently-trimmed whitespace
4747        // round-trip far from the source caixa.lisp.
4748        let root = PathBuf::from("/tmp/x");
4749        let manifest = root.join("caixa.lisp");
4750        let default_lib = root.join("lib").join("demo.lisp");
4751        let layout =
4752            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4753        let mut c = caixa(CaixaKind::Biblioteca);
4754        c.descricao = Some("Checkout\nflow.".into());
4755        let err = layout.verify(&c, &root).unwrap_err();
4756        let LayoutError::DescricaoViolation { caixa, issue } = err else {
4757            panic!("expected LayoutError::DescricaoViolation, got {err:?}");
4758        };
4759        assert_eq!(caixa, "demo");
4760        assert!(
4761            issue.contains(":descricao"),
4762            "issue must name the offending slot: {issue}",
4763        );
4764        // The wrapped `ManifestError::DescricaoInvalid` Display uses
4765        // `{descricao:?}` (Debug) so the embedded newline surfaces
4766        // debug-escaped as `\n` in the issue string.
4767        assert!(
4768            issue.contains("Checkout\\nflow."),
4769            "issue must quote the offending value (debug-escaped): {issue}",
4770        );
4771    }
4772
4773    // ── :licenca empty-Some shape wired into verify (universal axis) ──
4774    //
4775    // Until this wire-up landed `Caixa::validate_licenca` did not
4776    // exist — the universal SPDX-shaped license-expression axis had
4777    // no shape gate at any layer, so an empty `Some("")` silently
4778    // passed `Caixa::from_lisp` and `StandardLayout::verify` and
4779    // landed as a bare trailing period in the rendered
4780    // `lareira-<nome>` chart's `README.md` `## License` section via
4781    // the `caixa-helm` consumer's `caixa.licenca.clone().unwrap_or_else(||
4782    // "MIT".into())` (which only fires on `None`) at
4783    // `caixa-helm/src/lib.rs:361`. Closes the same `Some("")`
4784    // skips-`unwrap_or_else` footgun the peer `:repositorio`
4785    // (577b0a9) and `:descricao` (4e6db38) gates closed, on the
4786    // universal license-expression axis.
4787
4788    #[test]
4789    fn licenca_violation_on_empty_some() {
4790        // Canonical paste-from-blank-doc footgun on every kind. The
4791        // wrap envelope wraps [`ManifestError::LicencaEmpty`]'s
4792        // Display through verbatim, so the issue string names the
4793        // offending `:licenca` axis at the source — the author can
4794        // grep their caixa.lisp for `:licenca ""` and fix the empty
4795        // value in one edit. Mirrors the peer
4796        // `descricao_violation_on_empty_some` shape (4e6db38) on
4797        // the sibling `Option<String>` `:licenca` axis.
4798        let root = PathBuf::from("/tmp/x");
4799        let manifest = root.join("caixa.lisp");
4800        let default_lib = root.join("lib").join("demo.lisp");
4801        let layout =
4802            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4803        let mut c = caixa(CaixaKind::Biblioteca);
4804        c.licenca = Some(String::new());
4805        let err = layout.verify(&c, &root).unwrap_err();
4806        let LayoutError::LicencaViolation { caixa, issue } = err else {
4807            panic!("expected LayoutError::LicencaViolation, got {err:?}");
4808        };
4809        assert_eq!(caixa, "demo");
4810        assert!(
4811            issue.contains(":licenca"),
4812            "issue must name the offending slot: {issue}",
4813        );
4814    }
4815
4816    #[test]
4817    fn licenca_violation_fires_before_kind_coherence_mesh_slot() {
4818        // Cross-axis precedence pin: a Biblioteca with empty
4819        // `:licenca` *and* declared mesh slots (`:membros`)
4820        // surfaces the universal `:licenca` diagnostic first, not
4821        // the kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4822        // `:licenca` is universal (every kind owns the slot), so
4823        // its shape diagnostic is more fundamental than the
4824        // partition-on-kind diagnostic. Mirrors the peer
4825        // `descricao_violation_fires_before_kind_coherence_mesh_slot`
4826        // pin (4e6db38) on the `:descricao` axis vs the same
4827        // kind-coherence gates.
4828        let root = PathBuf::from("/tmp/x");
4829        let manifest = root.join("caixa.lisp");
4830        let default_lib = root.join("lib").join("demo.lisp");
4831        let layout =
4832            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4833        let mut c = caixa(CaixaKind::Biblioteca);
4834        c.licenca = Some(String::new());
4835        c.membros = vec![crate::aplicacao::Membro {
4836            caixa: "x".into(),
4837            versao: "^0.1".into(),
4838        }];
4839        let err = layout.verify(&c, &root).unwrap_err();
4840        assert!(
4841            matches!(err, LayoutError::LicencaViolation { .. }),
4842            "got {err:?}",
4843        );
4844    }
4845
4846    #[test]
4847    fn licenca_violation_fires_after_descricao_violation() {
4848        // Cross-axis precedence pin (inside the universal metadata
4849        // cascade): a caixa with both an empty `:descricao` *and*
4850        // an empty `:licenca` surfaces `DescricaoViolation`
4851        // first — `:descricao` is the seventh universal axis in the
4852        // cascade and runs before `:licenca`, peer with the
4853        // canonical identity-axis-first cascade the peer gates
4854        // establish. Mirrors the peer
4855        // `descricao_violation_fires_after_repositorio_violation`
4856        // precedence pin (4e6db38) on the repositorio-axis-before-
4857        // descricao-axis pair.
4858        let root = PathBuf::from("/tmp/x");
4859        let manifest = root.join("caixa.lisp");
4860        let default_lib = root.join("lib").join("demo.lisp");
4861        let layout =
4862            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4863        let mut c = caixa(CaixaKind::Biblioteca);
4864        c.descricao = Some(String::new());
4865        c.licenca = Some(String::new());
4866        let err = layout.verify(&c, &root).unwrap_err();
4867        assert!(
4868            matches!(err, LayoutError::DescricaoViolation { .. }),
4869            "got {err:?}",
4870        );
4871    }
4872
4873    #[test]
4874    fn licenca_violation_accepts_none() {
4875        // Positive control sanity pin: a caixa that omits `:licenca`
4876        // entirely (the layout-test fixture defaults to `None`)
4877        // passes the gate trivially — the gate is a no-op when the
4878        // author didn't author a value. Mirrors the peer
4879        // `descricao_violation_accepts_none` pin (4e6db38).
4880        let root = PathBuf::from("/tmp/x");
4881        let manifest = root.join("caixa.lisp");
4882        let default_lib = root.join("lib").join("demo.lisp");
4883        let layout =
4884            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4885        let c = caixa(CaixaKind::Biblioteca);
4886        layout.verify(&c, &root).expect("None must pass");
4887    }
4888
4889    #[test]
4890    fn licenca_violation_accepts_canonical_expression() {
4891        // Positive control pin on the canonical pleme-io `:licenca`
4892        // shape: a non-empty SPDX expression the `caixa-helm` /
4893        // `caixa-flux` / `caixa-mesh` fixtures all carry (`"MIT"`)
4894        // passes the gate end-to-end.
4895        let root = PathBuf::from("/tmp/x");
4896        let manifest = root.join("caixa.lisp");
4897        let default_lib = root.join("lib").join("demo.lisp");
4898        let layout =
4899            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4900        let mut c = caixa(CaixaKind::Biblioteca);
4901        c.licenca = Some("Apache-2.0 OR MIT".into());
4902        layout
4903            .verify(&c, &root)
4904            .expect("canonical SPDX expression must pass");
4905    }
4906
4907    #[test]
4908    fn licenca_violation_on_non_spdx_shape() {
4909        // Shape-predicate wire-up pin: a malformed `:licenca` value
4910        // that's a non-empty `Some(s)` but falls outside the SPDX
4911        // expression alphabet floor surfaces the `LicencaViolation`
4912        // envelope via the manifest-layer `ManifestError::LicencaInvalid`
4913        // arm. Mirrors the peer `licenca_violation_on_empty_some`
4914        // shape on the empty arm of the same axis and the peer
4915        // `edicao_violation_on_non_year_shape` shape on the sibling
4916        // `:edicao` axis. Until this gate landed a value like
4917        // `"Apache_2.0"` (an underscore-instead-of-hyphen typo) or
4918        // `"MIT, Apache-2.0"` (a comma-instead-of-`OR`-keyword
4919        // colloquial idiom) silently passed `StandardLayout::verify`
4920        // and landed in the rendered chart `README.md` `## License`
4921        // section + a future SPDX-aware Chart.yaml `license:`
4922        // emitter would refuse the value at `helm lint` time far
4923        // from the source caixa.lisp.
4924        let root = PathBuf::from("/tmp/x");
4925        let manifest = root.join("caixa.lisp");
4926        let default_lib = root.join("lib").join("demo.lisp");
4927        let layout =
4928            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4929        let mut c = caixa(CaixaKind::Biblioteca);
4930        c.licenca = Some("Apache_2.0".into());
4931        let err = layout.verify(&c, &root).unwrap_err();
4932        let LayoutError::LicencaViolation { caixa, issue } = err else {
4933            panic!("expected LayoutError::LicencaViolation, got {err:?}");
4934        };
4935        assert_eq!(caixa, "demo");
4936        assert!(
4937            issue.contains(":licenca"),
4938            "issue must name the offending slot: {issue}",
4939        );
4940        assert!(
4941            issue.contains("Apache_2.0"),
4942            "issue must quote the offending value: {issue}",
4943        );
4944    }
4945
4946    // ── :edicao empty-Some shape wired into verify (universal axis) ──
4947    //
4948    // Until this wire-up landed `Caixa::validate_edicao` did not
4949    // exist — the universal language-edition axis had no shape gate
4950    // at any layer, so an empty `Some("")` silently passed
4951    // `Caixa::from_lisp` and `StandardLayout::verify` and landed as a
4952    // bare `(:edicao "")` line in the rendered caixa.lisp, ready for
4953    // a future renderer-side consumer's `Option::unwrap_or_else`
4954    // (which only fires on `None`) to skip its fallback. Closes the
4955    // same `Some("")`-skips-`unwrap_or_else` footgun the peer
4956    // `:repositorio` (577b0a9), `:descricao` (4e6db38), and
4957    // `:licenca` (3d1e535) gates closed, on the universal language-
4958    // edition axis — the last un-gated universal-axis
4959    // `Option<String>` Caixa-level value-shape surface.
4960
4961    #[test]
4962    fn edicao_violation_on_empty_some() {
4963        // Canonical paste-from-blank-doc footgun on every kind. The
4964        // wrap envelope wraps [`ManifestError::EdicaoEmpty`]'s
4965        // Display through verbatim, so the issue string names the
4966        // offending `:edicao` axis at the source — the author can
4967        // grep their caixa.lisp for `:edicao ""` and fix the empty
4968        // value in one edit. Mirrors the peer
4969        // `licenca_violation_on_empty_some` shape (3d1e535) on the
4970        // sibling `Option<String>` `:edicao` axis.
4971        let root = PathBuf::from("/tmp/x");
4972        let manifest = root.join("caixa.lisp");
4973        let default_lib = root.join("lib").join("demo.lisp");
4974        let layout =
4975            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4976        let mut c = caixa(CaixaKind::Biblioteca);
4977        c.edicao = Some(String::new());
4978        let err = layout.verify(&c, &root).unwrap_err();
4979        let LayoutError::EdicaoViolation { caixa, issue } = err else {
4980            panic!("expected LayoutError::EdicaoViolation, got {err:?}");
4981        };
4982        assert_eq!(caixa, "demo");
4983        assert!(
4984            issue.contains(":edicao"),
4985            "issue must name the offending slot: {issue}",
4986        );
4987    }
4988
4989    #[test]
4990    fn edicao_violation_fires_before_kind_coherence_mesh_slot() {
4991        // Cross-axis precedence pin: a Biblioteca with empty
4992        // `:edicao` *and* declared mesh slots (`:membros`) surfaces
4993        // the universal `:edicao` diagnostic first, not the
4994        // kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4995        // `:edicao` is universal (every kind owns the slot), so
4996        // its shape diagnostic is more fundamental than the
4997        // partition-on-kind diagnostic. Mirrors the peer
4998        // `licenca_violation_fires_before_kind_coherence_mesh_slot`
4999        // pin (3d1e535) on the `:licenca` axis vs the same
5000        // kind-coherence gates.
5001        let root = PathBuf::from("/tmp/x");
5002        let manifest = root.join("caixa.lisp");
5003        let default_lib = root.join("lib").join("demo.lisp");
5004        let layout =
5005            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5006        let mut c = caixa(CaixaKind::Biblioteca);
5007        c.edicao = Some(String::new());
5008        c.membros = vec![crate::aplicacao::Membro {
5009            caixa: "x".into(),
5010            versao: "^0.1".into(),
5011        }];
5012        let err = layout.verify(&c, &root).unwrap_err();
5013        assert!(
5014            matches!(err, LayoutError::EdicaoViolation { .. }),
5015            "got {err:?}",
5016        );
5017    }
5018
5019    #[test]
5020    fn edicao_violation_fires_after_licenca_violation() {
5021        // Cross-axis precedence pin (inside the universal metadata
5022        // cascade): a caixa with both an empty `:licenca` *and* an
5023        // empty `:edicao` surfaces `LicencaViolation` first —
5024        // `:licenca` is the eighth universal axis in the cascade
5025        // and runs before `:edicao`, peer with the canonical
5026        // identity-axis-first cascade the peer gates establish.
5027        // Mirrors the peer
5028        // `licenca_violation_fires_after_descricao_violation`
5029        // precedence pin (3d1e535) on the descricao-axis-before-
5030        // licenca-axis pair.
5031        let root = PathBuf::from("/tmp/x");
5032        let manifest = root.join("caixa.lisp");
5033        let default_lib = root.join("lib").join("demo.lisp");
5034        let layout =
5035            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5036        let mut c = caixa(CaixaKind::Biblioteca);
5037        c.licenca = Some(String::new());
5038        c.edicao = Some(String::new());
5039        let err = layout.verify(&c, &root).unwrap_err();
5040        assert!(
5041            matches!(err, LayoutError::LicencaViolation { .. }),
5042            "got {err:?}",
5043        );
5044    }
5045
5046    #[test]
5047    fn edicao_violation_accepts_none() {
5048        // Positive control sanity pin: a caixa that omits `:edicao`
5049        // entirely (the layout-test fixture defaults to `None`)
5050        // passes the gate trivially — the gate is a no-op when the
5051        // author didn't author a value. Mirrors the peer
5052        // `licenca_violation_accepts_none` pin (3d1e535).
5053        let root = PathBuf::from("/tmp/x");
5054        let manifest = root.join("caixa.lisp");
5055        let default_lib = root.join("lib").join("demo.lisp");
5056        let layout =
5057            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5058        let c = caixa(CaixaKind::Biblioteca);
5059        layout.verify(&c, &root).expect("None must pass");
5060    }
5061
5062    #[test]
5063    fn edicao_violation_accepts_canonical_value() {
5064        // Positive control pin on the canonical pleme-io `:edicao`
5065        // shape: the `"2026"` edition every `caixa-helm` /
5066        // `caixa-flux` / `caixa-mesh` / `caixa-core/src/render.rs`
5067        // fixture carries by construction passes the gate end-to-end.
5068        let root = PathBuf::from("/tmp/x");
5069        let manifest = root.join("caixa.lisp");
5070        let default_lib = root.join("lib").join("demo.lisp");
5071        let layout =
5072            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5073        let mut c = caixa(CaixaKind::Biblioteca);
5074        c.edicao = Some("2026".into());
5075        layout
5076            .verify(&c, &root)
5077            .expect("canonical edition must pass");
5078    }
5079
5080    #[test]
5081    fn edicao_violation_on_non_year_shape() {
5082        // Shape-predicate wire-up pin: a malformed `:edicao` value
5083        // that's a non-empty `Some(s)` but not a 4-digit ASCII
5084        // decimal year surfaces the `EdicaoViolation` envelope via
5085        // the manifest-layer `ManifestError::EdicaoInvalid` arm.
5086        // Mirrors the peer `edicao_violation_on_empty_some` shape
5087        // on the empty arm of the same axis. Until this gate landed
5088        // a value like `"v2026"` (a familiar git-tag idiom that
5089        // doesn't apply to the year-shaped edition axis) silently
5090        // passed `StandardLayout::verify` and broke at the
5091        // substrate's build-time edition selector far from the
5092        // source caixa.lisp.
5093        let root = PathBuf::from("/tmp/x");
5094        let manifest = root.join("caixa.lisp");
5095        let default_lib = root.join("lib").join("demo.lisp");
5096        let layout =
5097            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5098        let mut c = caixa(CaixaKind::Biblioteca);
5099        c.edicao = Some("v2026".into());
5100        let err = layout.verify(&c, &root).unwrap_err();
5101        let LayoutError::EdicaoViolation { caixa, issue } = err else {
5102            panic!("expected LayoutError::EdicaoViolation, got {err:?}");
5103        };
5104        assert_eq!(caixa, "demo");
5105        assert!(
5106            issue.contains(":edicao"),
5107            "issue must name the offending slot: {issue}",
5108        );
5109        assert!(
5110            issue.contains("v2026"),
5111            "issue must quote the offending value: {issue}",
5112        );
5113    }
5114
5115    // ── Caixa-identity gates (`:nome`, `:versao`) wired into verify ────
5116    //
5117    // Until this wire-up landed `Caixa::validate_nome` and
5118    // `Caixa::validate_versao` lived as `pub fn` on `Caixa` with full
5119    // per-arm unit coverage in `manifest::tests`, but no production
5120    // path called them — `feira build` silently accepted malformed
5121    // `:nome` / `:versao` and the failure surfaced at `helm install` /
5122    // `kubectl apply` / `feira publish` / lacre-resolve / `:upgrade-from
5123    // :from` matching time, far from the source `caixa.lisp`. The
5124    // following pins fence the layout-pipeline wire-up: every layout
5125    // verify on a structurally-invalid Caixa identity axis surfaces
5126    // the per-axis `*Violation { caixa, issue }` envelope before any
5127    // kind-coherence, code-path, or downstream gate sees it.
5128
5129    #[test]
5130    fn nome_violation_on_uppercase() {
5131        let root = PathBuf::from("/tmp/x");
5132        let manifest = root.join("caixa.lisp");
5133        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5134        let mut c = caixa(CaixaKind::Biblioteca);
5135        c.nome = "MyApp".into();
5136        let err = layout.verify(&c, &root).unwrap_err();
5137        let LayoutError::NomeViolation { caixa, issue } = err else {
5138            panic!("expected LayoutError::NomeViolation, got {err:?}");
5139        };
5140        assert_eq!(caixa, "MyApp");
5141        assert!(
5142            issue.contains("MyApp"),
5143            "issue must quote the offending nome: {issue}",
5144        );
5145    }
5146
5147    #[test]
5148    fn nome_violation_on_underscore() {
5149        let root = PathBuf::from("/tmp/x");
5150        let manifest = root.join("caixa.lisp");
5151        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5152        let mut c = caixa(CaixaKind::Biblioteca);
5153        c.nome = "my_app".into();
5154        let err = layout.verify(&c, &root).unwrap_err();
5155        assert!(
5156            matches!(err, LayoutError::NomeViolation { ref caixa, .. } if caixa == "my_app"),
5157            "got {err:?}",
5158        );
5159    }
5160
5161    #[test]
5162    fn nome_violation_on_empty() {
5163        // Empty `:nome` surfaces NomeViolation wrapping the narrower
5164        // `ManifestError::NomeEmpty` arm — the empty-first cascade the
5165        // peer per-axis name gates already use.
5166        let root = PathBuf::from("/tmp/x");
5167        let manifest = root.join("caixa.lisp");
5168        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5169        let mut c = caixa(CaixaKind::Biblioteca);
5170        c.nome = String::new();
5171        let err = layout.verify(&c, &root).unwrap_err();
5172        let LayoutError::NomeViolation { caixa, issue } = err else {
5173            panic!("expected LayoutError::NomeViolation, got {err:?}");
5174        };
5175        assert!(caixa.is_empty());
5176        assert!(
5177            issue.contains(":nome is empty"),
5178            "issue must surface the empty-arm diagnostic: {issue}",
5179        );
5180    }
5181
5182    #[test]
5183    fn versao_violation_on_missing_patch() {
5184        // `"0.1"` — the canonical "I shortened it" footgun. Helm /
5185        // OCI / lacre-resolve / `:upgrade-from :from` all strict-parse
5186        // through `semver::Version::parse`, which refuses a two-part
5187        // shape.
5188        let root = PathBuf::from("/tmp/x");
5189        let manifest = root.join("caixa.lisp");
5190        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5191        let mut c = caixa(CaixaKind::Biblioteca);
5192        c.versao = "0.1".into();
5193        let err = layout.verify(&c, &root).unwrap_err();
5194        let LayoutError::VersaoViolation { caixa, issue } = err else {
5195            panic!("expected LayoutError::VersaoViolation, got {err:?}");
5196        };
5197        assert_eq!(caixa, "demo");
5198        assert!(
5199            issue.contains("0.1"),
5200            "issue must quote the offending versao: {issue}",
5201        );
5202    }
5203
5204    #[test]
5205    fn versao_violation_on_git_tag_shape() {
5206        // `"v0.1.0"` — the git-tag-shape-leaking-into-versao typo.
5207        let root = PathBuf::from("/tmp/x");
5208        let manifest = root.join("caixa.lisp");
5209        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5210        let mut c = caixa(CaixaKind::Biblioteca);
5211        c.versao = "v0.1.0".into();
5212        let err = layout.verify(&c, &root).unwrap_err();
5213        assert!(
5214            matches!(err, LayoutError::VersaoViolation { ref issue, .. }
5215                if issue.contains("v0.1.0")),
5216            "got {err:?}",
5217        );
5218    }
5219
5220    #[test]
5221    fn versao_violation_on_empty() {
5222        let root = PathBuf::from("/tmp/x");
5223        let manifest = root.join("caixa.lisp");
5224        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5225        let mut c = caixa(CaixaKind::Biblioteca);
5226        c.versao = String::new();
5227        let err = layout.verify(&c, &root).unwrap_err();
5228        let LayoutError::VersaoViolation { caixa, issue } = err else {
5229            panic!("expected LayoutError::VersaoViolation, got {err:?}");
5230        };
5231        assert_eq!(caixa, "demo");
5232        assert!(
5233            issue.contains(":versao is empty"),
5234            "issue must surface the empty-arm diagnostic: {issue}",
5235        );
5236    }
5237
5238    #[test]
5239    fn nome_violation_fires_before_versao_violation() {
5240        // Precedence pin: when both `:nome` and `:versao` are malformed,
5241        // `:nome` surfaces first — the canonical declaration-order
5242        // precedence the `ManifestError` family establishes, the same
5243        // grep-order the author follows when fixing in `caixa.lisp`.
5244        let root = PathBuf::from("/tmp/x");
5245        let manifest = root.join("caixa.lisp");
5246        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5247        let mut c = caixa(CaixaKind::Biblioteca);
5248        c.nome = "MyApp".into();
5249        c.versao = "0.1".into();
5250        let err = layout.verify(&c, &root).unwrap_err();
5251        assert!(
5252            matches!(err, LayoutError::NomeViolation { .. }),
5253            "got {err:?} — nome must fire before versao",
5254        );
5255    }
5256
5257    #[test]
5258    fn nome_violation_fires_before_kind_coherence() {
5259        // Precedence pin: a Biblioteca caixa with a malformed `:nome`
5260        // AND a declared mesh slot surfaces NomeViolation, not
5261        // MeshSlotsOnNonAplicacao — the identity-axis gate is more
5262        // fundamental than the kind-coherence gate (which carries
5263        // `caixa.nome` verbatim in its diagnostic, and so depends on the
5264        // name being structurally valid to render a useful message).
5265        let root = PathBuf::from("/tmp/x");
5266        let manifest = root.join("caixa.lisp");
5267        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5268        let mut c = caixa(CaixaKind::Biblioteca);
5269        c.nome = "MyApp".into();
5270        c.membros = vec![crate::aplicacao::Membro {
5271            caixa: "x".into(),
5272            versao: "^0.1".into(),
5273        }];
5274        let err = layout.verify(&c, &root).unwrap_err();
5275        assert!(
5276            matches!(err, LayoutError::NomeViolation { .. }),
5277            "got {err:?} — nome must fire before MeshSlotsOnNonAplicacao",
5278        );
5279    }
5280
5281    #[test]
5282    fn nome_violation_fires_before_owncode() {
5283        // Precedence pin: a Supervisor with a malformed `:nome` AND
5284        // declared `:bibliotecas` surfaces NomeViolation, not
5285        // SupervisorOwnsCode — same rationale as above.
5286        let root = PathBuf::from("/tmp/x");
5287        let manifest = root.join("caixa.lisp");
5288        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5289        let mut c = caixa(CaixaKind::Supervisor);
5290        c.nome = "MyApp".into();
5291        c.bibliotecas = vec!["lib/x.lisp".into()];
5292        let err = layout.verify(&c, &root).unwrap_err();
5293        assert!(
5294            matches!(err, LayoutError::NomeViolation { .. }),
5295            "got {err:?} — nome must fire before SupervisorOwnsCode",
5296        );
5297    }
5298
5299    #[test]
5300    fn versao_violation_fires_before_kind_coherence() {
5301        // Precedence pin: a Biblioteca with a valid `:nome` but a
5302        // malformed `:versao` AND a declared servico slot surfaces
5303        // VersaoViolation before ServicoSlotsOnNonServico.
5304        let root = PathBuf::from("/tmp/x");
5305        let manifest = root.join("caixa.lisp");
5306        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5307        let mut c = caixa(CaixaKind::Biblioteca);
5308        c.versao = "v0.1.0".into();
5309        c.limits = Some(crate::LimitsSpec {
5310            memory: Some(64 * 1024 * 1024),
5311            ..Default::default()
5312        });
5313        let err = layout.verify(&c, &root).unwrap_err();
5314        assert!(
5315            matches!(err, LayoutError::VersaoViolation { .. }),
5316            "got {err:?} — versao must fire before ServicoSlotsOnNonServico",
5317        );
5318    }
5319
5320    #[test]
5321    fn nome_violation_fires_before_missing_lib() {
5322        // Precedence pin: a Biblioteca with a malformed `:nome` and no
5323        // lib entry surfaces NomeViolation, not MissingLib — the
5324        // identity-axis gate is more fundamental than the layout's
5325        // `lib/<nome>.lisp` default-path check (which derives the
5326        // expected path from `:nome` itself, so would surface a
5327        // misleading "expected lib/MyApp.lisp" diagnostic against an
5328        // unrecoverable name).
5329        let root = PathBuf::from("/tmp/x");
5330        let manifest = root.join("caixa.lisp");
5331        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5332        let mut c = caixa(CaixaKind::Biblioteca);
5333        c.nome = "MyApp".into();
5334        let err = layout.verify(&c, &root).unwrap_err();
5335        assert!(
5336            matches!(err, LayoutError::NomeViolation { .. }),
5337            "got {err:?} — nome must fire before MissingLib",
5338        );
5339    }
5340
5341    #[test]
5342    fn nome_versao_violations_fire_after_missing_manifest() {
5343        // Precedence pin: `MissingManifest` still dominates — there's
5344        // no caixa to identity-check when the manifest is missing.
5345        let root = PathBuf::from("/tmp/x");
5346        let layout = StandardLayout::new().with_path_exists(|_| false);
5347        let mut c = caixa(CaixaKind::Biblioteca);
5348        c.nome = "MyApp".into();
5349        c.versao = "0.1".into();
5350        let err = layout.verify(&c, &root).unwrap_err();
5351        assert!(
5352            matches!(err, LayoutError::MissingManifest(_)),
5353            "got {err:?} — MissingManifest must dominate identity gates",
5354        );
5355    }
5356
5357    #[test]
5358    fn valid_nome_versao_passes_to_downstream_gates() {
5359        // Sanity pin: the canonical "demo" / "0.1.0" identity passes
5360        // both axes; downstream gates (MissingLib here) take over.
5361        let root = PathBuf::from("/tmp/x");
5362        let manifest = root.join("caixa.lisp");
5363        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5364        let err = layout
5365            .verify(&caixa(CaixaKind::Biblioteca), &root)
5366            .unwrap_err();
5367        assert!(
5368            matches!(err, LayoutError::MissingLib { .. }),
5369            "got {err:?} — valid identity must pass to MissingLib",
5370        );
5371    }
5372
5373    // ── :deps / :deps-dev shape gate (lifted to layout-level verify) ─────
5374    //
5375    // Until this wire-up landed `Caixa::validate_deps` lived as `pub fn`
5376    // on `Caixa` with full per-arm unit coverage in `manifest::tests` +
5377    // `dep::tests` but no production path called it — `feira build`
5378    // silently accepted a malformed `:deps` / `:deps-dev` entry and the
5379    // failure surfaced at lacre-resolve / `git clone` / `cargo metadata`
5380    // / `helm install` time on the *first* downstream consumer to
5381    // strict-parse the value, far from the source `caixa.lisp` and
5382    // without any field naming the offending `:deps` axis. The following
5383    // pins fence the layout-pipeline wire-up: every layout verify on a
5384    // structurally-invalid `:deps` value-shape surfaces the per-axis
5385    // `DepsViolation { caixa, issue }` envelope (peer of
5386    // `NomeViolation` / `VersaoViolation` / `CodePathViolation` /
5387    // `LimitsViolation` / `BehaviorViolation` / `UpgradeViolation` /
5388    // `SupervisorViolation` / `AplicacaoViolation`) before any kind-
5389    // coherence, code-path, or downstream gate sees it.
5390
5391    #[test]
5392    fn deps_violation_on_empty_dep_nome() {
5393        // Empty `:nome` on a `:deps` entry surfaces the narrower
5394        // `DepError::NomeEmpty` arm through the wrap envelope.
5395        use crate::Dep;
5396        let root = PathBuf::from("/tmp/x");
5397        let manifest = root.join("caixa.lisp");
5398        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5399        let mut c = caixa(CaixaKind::Biblioteca);
5400        c.deps = vec![Dep::simple("", "^0.1")];
5401        let err = layout.verify(&c, &root).unwrap_err();
5402        let LayoutError::DepsViolation { caixa, issue } = err else {
5403            panic!("expected LayoutError::DepsViolation, got {err:?}");
5404        };
5405        assert_eq!(caixa, "demo");
5406        assert!(
5407            issue.contains(":deps") && issue.contains(":nome"),
5408            "issue must name the offending slot + axis: {issue}",
5409        );
5410    }
5411
5412    #[test]
5413    fn deps_violation_on_uppercase_dep_nome() {
5414        // Uppercase `:nome` on a `:deps` entry surfaces
5415        // `DepError::NomeInvalid` (DNS-1123 violation) through the wrap.
5416        use crate::Dep;
5417        let root = PathBuf::from("/tmp/x");
5418        let manifest = root.join("caixa.lisp");
5419        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5420        let mut c = caixa(CaixaKind::Biblioteca);
5421        c.deps = vec![Dep::simple("Caixa-Teia", "^0.1")];
5422        let err = layout.verify(&c, &root).unwrap_err();
5423        let LayoutError::DepsViolation { caixa, issue } = err else {
5424            panic!("expected LayoutError::DepsViolation, got {err:?}");
5425        };
5426        assert_eq!(caixa, "demo");
5427        assert!(
5428            issue.contains("Caixa-Teia"),
5429            "issue must quote the offending dep nome verbatim: {issue}",
5430        );
5431    }
5432
5433    #[test]
5434    fn deps_violation_on_unparseable_dep_versao() {
5435        // Unparseable `:versao` requirement on a `:deps` entry surfaces
5436        // `DepError::VersaoInvalid` through the wrap — the canonical
5437        // "the semver::Error reached the resolver, far from the source"
5438        // footgun closed at author time.
5439        use crate::Dep;
5440        let root = PathBuf::from("/tmp/x");
5441        let manifest = root.join("caixa.lisp");
5442        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5443        let mut c = caixa(CaixaKind::Biblioteca);
5444        c.deps = vec![Dep::simple("caixa-teia", "not-a-req")];
5445        let err = layout.verify(&c, &root).unwrap_err();
5446        let LayoutError::DepsViolation { caixa, issue } = err else {
5447            panic!("expected LayoutError::DepsViolation, got {err:?}");
5448        };
5449        assert_eq!(caixa, "demo");
5450        assert!(
5451            issue.contains("caixa-teia") && issue.contains("not-a-req"),
5452            "issue must quote the dep nome + offending versao: {issue}",
5453        );
5454    }
5455
5456    #[test]
5457    fn deps_violation_on_duplicate_nome_in_deps() {
5458        // Within-list `:deps :nome` duplicate surfaces
5459        // `DepError::DuplicateNome { list: ":deps" }` through the wrap.
5460        use crate::Dep;
5461        let root = PathBuf::from("/tmp/x");
5462        let manifest = root.join("caixa.lisp");
5463        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5464        let mut c = caixa(CaixaKind::Biblioteca);
5465        c.deps = vec![
5466            Dep::simple("caixa-teia", "^0.1"),
5467            Dep::simple("caixa-teia", "^0.2"),
5468        ];
5469        let err = layout.verify(&c, &root).unwrap_err();
5470        let LayoutError::DepsViolation { caixa, issue } = err else {
5471            panic!("expected LayoutError::DepsViolation, got {err:?}");
5472        };
5473        assert_eq!(caixa, "demo");
5474        assert!(
5475            issue.contains("caixa-teia") && issue.contains(":deps"),
5476            "issue must quote the duplicated nome + list: {issue}",
5477        );
5478    }
5479
5480    #[test]
5481    fn deps_violation_on_duplicate_nome_in_deps_dev() {
5482        // Within-list `:deps-dev :nome` duplicate surfaces the same
5483        // diagnostic on the dev-only axis — neither list is a
5484        // second-class citizen of the typed surface.
5485        use crate::Dep;
5486        let root = PathBuf::from("/tmp/x");
5487        let manifest = root.join("caixa.lisp");
5488        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5489        let mut c = caixa(CaixaKind::Biblioteca);
5490        c.deps_dev = vec![
5491            Dep::simple("caixa-teia", "^0.1"),
5492            Dep::simple("caixa-teia", "^0.2"),
5493        ];
5494        let err = layout.verify(&c, &root).unwrap_err();
5495        let LayoutError::DepsViolation { caixa, issue } = err else {
5496            panic!("expected LayoutError::DepsViolation, got {err:?}");
5497        };
5498        assert_eq!(caixa, "demo");
5499        assert!(
5500            issue.contains(":deps-dev"),
5501            "issue must name the offending list: {issue}",
5502        );
5503    }
5504
5505    #[test]
5506    fn deps_violation_in_deps_fires_before_deps_dev() {
5507        // Precedence pin: when *both* `:deps` and `:deps-dev` carry a
5508        // malformed entry, the `:deps` walk fires first — the canonical
5509        // declaration-order precedence `Caixa::validate_deps` establishes
5510        // (the same author-grep ordering the typed-graph peers use on
5511        // every other Vec-shaped surface).
5512        use crate::Dep;
5513        let root = PathBuf::from("/tmp/x");
5514        let manifest = root.join("caixa.lisp");
5515        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5516        let mut c = caixa(CaixaKind::Biblioteca);
5517        c.deps = vec![Dep::simple("Bad-In-Deps", "^0.1")];
5518        c.deps_dev = vec![Dep::simple("Bad-In-Deps-Dev", "^0.1")];
5519        let err = layout.verify(&c, &root).unwrap_err();
5520        let LayoutError::DepsViolation { caixa: _, issue } = err else {
5521            panic!("expected LayoutError::DepsViolation, got {err:?}");
5522        };
5523        assert!(
5524            issue.contains("Bad-In-Deps") && !issue.contains("Bad-In-Deps-Dev"),
5525            "issue must name the :deps offender, not :deps-dev: {issue}",
5526        );
5527    }
5528
5529    #[test]
5530    fn deps_violation_fires_after_versao_violation() {
5531        // Precedence pin: when both the top-level `:versao` and a `:deps`
5532        // entry are malformed, the Caixa-identity gate fires first — the
5533        // canonical declaration order on `Caixa` (`:nome` → `:versao` →
5534        // ... → `:deps`) and the same identity-axis-dominates-content-
5535        // axis discipline the peer `validate_nome` / `validate_versao`
5536        // wire-up established (1f74a5f). A malformed `:versao` would
5537        // otherwise quote `caixa.nome` against a downstream-shaped
5538        // diagnostic.
5539        use crate::Dep;
5540        let root = PathBuf::from("/tmp/x");
5541        let manifest = root.join("caixa.lisp");
5542        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5543        let mut c = caixa(CaixaKind::Biblioteca);
5544        c.versao = "v0.1.0".into();
5545        c.deps = vec![Dep::simple("Bad-Dep", "^0.1")];
5546        let err = layout.verify(&c, &root).unwrap_err();
5547        assert!(
5548            matches!(err, LayoutError::VersaoViolation { .. }),
5549            "got {err:?} — versao must fire before DepsViolation",
5550        );
5551    }
5552
5553    #[test]
5554    fn deps_violation_fires_before_kind_coherence() {
5555        // Precedence pin: a Supervisor with a malformed `:deps` entry
5556        // AND declared `:bibliotecas` (the canonical SupervisorOwnsCode
5557        // shape) surfaces DepsViolation, not SupervisorOwnsCode — the
5558        // dep surface is universal across all kinds and its shape gate
5559        // is more fundamental than the kind-coherence partitions on
5560        // `:bibliotecas` / `:exe` / `:servicos`. The author can fix the
5561        // dep typo without first being told to move their `:bibliotecas`
5562        // off a Supervisor.
5563        use crate::Dep;
5564        let root = PathBuf::from("/tmp/x");
5565        let manifest = root.join("caixa.lisp");
5566        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5567        let mut c = caixa(CaixaKind::Supervisor);
5568        c.deps = vec![Dep::simple("Bad-Dep", "^0.1")];
5569        c.bibliotecas = vec!["lib/x.lisp".into()];
5570        let err = layout.verify(&c, &root).unwrap_err();
5571        assert!(
5572            matches!(err, LayoutError::DepsViolation { .. }),
5573            "got {err:?} — DepsViolation must fire before SupervisorOwnsCode",
5574        );
5575    }
5576
5577    #[test]
5578    fn deps_violation_fires_after_missing_manifest() {
5579        // Precedence pin: `MissingManifest` still dominates — there's no
5580        // caixa to deps-check when the manifest is missing.
5581        use crate::Dep;
5582        let root = PathBuf::from("/tmp/x");
5583        let layout = StandardLayout::new().with_path_exists(|_| false);
5584        let mut c = caixa(CaixaKind::Biblioteca);
5585        c.deps = vec![Dep::simple("Bad-Dep", "^0.1")];
5586        let err = layout.verify(&c, &root).unwrap_err();
5587        assert!(
5588            matches!(err, LayoutError::MissingManifest(_)),
5589            "got {err:?} — MissingManifest must dominate the deps gate",
5590        );
5591    }
5592
5593    #[test]
5594    fn deps_violation_on_self_dep_in_deps() {
5595        // Cross-slot self-edge: a caixa whose `:deps` lists its own
5596        // `:nome` is rejected at the layout wire-up, the diagnostic
5597        // surfaces through the `DepsViolation` envelope with both the
5598        // offending list tag (`":deps"`) and the parent's `:nome`
5599        // verbatim. Until this wire-up landed the self-dep silently
5600        // passed `feira build` and the resolver's lacre-pipeline
5601        // closure walk either rejected mid-traversal (infinite
5602        // recursion detected far from the source caixa.lisp) or, on
5603        // the unbounded path, recursed until it exhausted its stack.
5604        // Mirrors the supervision-tree
5605        // [`supervisor_violation_on_self_supervision`] and the
5606        // Aplicacao-membership self-edge wire-up tests on the peer
5607        // typed-name-graph axes.
5608        use crate::Dep;
5609        let root = PathBuf::from("/tmp/x");
5610        let manifest = root.join("caixa.lisp");
5611        let default_lib = root.join("lib").join("demo.lisp");
5612        let layout =
5613            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5614        let mut c = caixa(CaixaKind::Biblioteca);
5615        c.deps = vec![Dep::simple("demo", "^0.1")];
5616        let err = layout.verify(&c, &root).unwrap_err();
5617        let LayoutError::DepsViolation { caixa, issue } = err else {
5618            panic!("expected LayoutError::DepsViolation, got {err:?}");
5619        };
5620        assert_eq!(caixa, "demo");
5621        assert!(
5622            issue.contains(":deps") && issue.contains("demo"),
5623            "issue must name the offending list + parent :nome: {issue}",
5624        );
5625    }
5626
5627    #[test]
5628    fn deps_violation_on_self_dep_in_deps_dev() {
5629        // Same cross-slot self-edge gate on the `:deps-dev` axis —
5630        // neither dep list is a second-class citizen of the typed
5631        // surface. The diagnostic names `:deps-dev` so the author can
5632        // grep their caixa.lisp for the offending block directly.
5633        use crate::Dep;
5634        let root = PathBuf::from("/tmp/x");
5635        let manifest = root.join("caixa.lisp");
5636        let default_lib = root.join("lib").join("demo.lisp");
5637        let layout =
5638            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5639        let mut c = caixa(CaixaKind::Biblioteca);
5640        c.deps_dev = vec![Dep::simple("demo", "^0.1")];
5641        let err = layout.verify(&c, &root).unwrap_err();
5642        let LayoutError::DepsViolation { caixa, issue } = err else {
5643            panic!("expected LayoutError::DepsViolation, got {err:?}");
5644        };
5645        assert_eq!(caixa, "demo");
5646        assert!(
5647            issue.contains(":deps-dev"),
5648            "issue must name the offending list: {issue}",
5649        );
5650    }
5651
5652    #[test]
5653    fn self_dep_fires_after_per_entry_dep_shape() {
5654        // Precedence pin: the per-entry shape gates of
5655        // [`Caixa::validate_deps`] (DNS-1123 / SemVer / fonte / etc.)
5656        // fire first on a self-dep entry whose `:nome` is malformed.
5657        // Same ordering posture every peer cross-slot gate uses
5658        // (`validate_no_self_supervision` after `SupervisorSpec::validate`,
5659        // `validate_no_self_membership` after `AplicacaoSpec::validate`).
5660        // A malformed self-dep `:nome` surfaces the narrower
5661        // per-entry diagnostic (which already names the parser-side
5662        // reason) before the self-edge gate sees the entry.
5663        use crate::Dep;
5664        let root = PathBuf::from("/tmp/x");
5665        let manifest = root.join("caixa.lisp");
5666        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5667        let mut c = caixa(CaixaKind::Biblioteca);
5668        // The parent is "demo" (DNS-1123 valid); the dep is "DEMO"
5669        // (DNS-1123 invalid). The per-entry shape gate fires on the
5670        // upper-case nome, masking the self-edge gate (and that's the
5671        // canonical precedence — fix the dep shape first, then the
5672        // structural self-edge becomes the next live diagnostic).
5673        c.deps = vec![Dep::simple("DEMO", "^0.1")];
5674        let err = layout.verify(&c, &root).unwrap_err();
5675        let LayoutError::DepsViolation { caixa: _, issue } = err else {
5676            panic!("expected LayoutError::DepsViolation, got {err:?}");
5677        };
5678        assert!(
5679            issue.contains("DNS-1123"),
5680            "issue must be the per-entry shape diagnostic, not the self-edge gate: {issue}",
5681        );
5682    }
5683
5684    #[test]
5685    fn valid_deps_pass_to_downstream_gates() {
5686        // Positive control pin: the canonical authoring shape (one
5687        // `:deps` entry naming a DNS-1123 nome + Cargo-shaped requirement,
5688        // one `:deps-dev` entry on a distinct nome) passes the dep gate;
5689        // downstream gates (MissingLib here) take over. Drift here =
5690        // a future tighten that rejects any canonical shape surfaces as
5691        // a regression at this layout-level pin.
5692        use crate::Dep;
5693        let root = PathBuf::from("/tmp/x");
5694        let manifest = root.join("caixa.lisp");
5695        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5696        let mut c = caixa(CaixaKind::Biblioteca);
5697        c.deps = vec![Dep::simple("caixa-teia", "^0.1")];
5698        c.deps_dev = vec![Dep::simple("caixa-lint", "^0.2")];
5699        let err = layout.verify(&c, &root).unwrap_err();
5700        assert!(
5701            matches!(err, LayoutError::MissingLib { .. }),
5702            "got {err:?} — valid deps must pass to MissingLib",
5703        );
5704    }
5705
5706    #[test]
5707    fn code_path_gate_runs_after_foreign_code_slot_gate() {
5708        // Precedence pin: a Servico that declares `:exe` (foreign code
5709        // surface) surfaces ForeignCodeSlot, *not* a per-entry path
5710        // shape diagnostic, even when the `:exe` entry is itself
5711        // malformed. The kind-coherence gate is the load-bearing
5712        // diagnostic at this site — once the slot is moved off the
5713        // wrong kind, the per-entry shape gate becomes the next live
5714        // diagnostic.
5715        let root = PathBuf::from("/tmp/x");
5716        let manifest = root.join("caixa.lisp");
5717        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5718        let mut c = caixa(CaixaKind::Servico);
5719        c.servicos = vec!["servicos/ok.yaml".into()];
5720        c.exe = vec!["/etc/foreign".into()];
5721        let err = layout.verify(&c, &root).unwrap_err();
5722        assert!(
5723            matches!(err, LayoutError::ForeignCodeSlot { .. }),
5724            "expected ForeignCodeSlot (kind-coherence wins over per-entry shape), got {err:?}",
5725        );
5726    }
5727
5728    // ── M2 typed-substrate invariants ────────────────────────────────────
5729
5730    #[test]
5731    fn behavior_callback_path_must_exist() {
5732        use crate::BehaviorSpec;
5733        use std::path::PathBuf;
5734        let root = PathBuf::from("/tmp/x");
5735        let manifest = root.join("caixa.lisp");
5736        let mut c = caixa(CaixaKind::Servico);
5737        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5738        let svc = root.join("servicos/demo.computeunit.yaml");
5739        c.behavior = Some(BehaviorSpec {
5740            on_init: Some(PathBuf::from("lib/init.lisp")),
5741            ..Default::default()
5742        });
5743        let manifest_clone = manifest.clone();
5744        let svc_clone = svc.clone();
5745        let layout =
5746            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
5747        let err = layout.verify(&c, &root).unwrap_err();
5748        assert!(matches!(
5749            err,
5750            LayoutError::MissingEntry { kind, .. }
5751                if kind == crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK
5752        ));
5753
5754        // Now declare the path exists — passes.
5755        let init = root.join("lib/init.lisp");
5756        let layout =
5757            StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc || p == init);
5758        layout.verify(&c, &root).unwrap();
5759    }
5760
5761    #[test]
5762    fn behavior_absolute_callback_is_violation_not_missing() {
5763        // An absolute path silently subverts `root.join(p)` (Path::join
5764        // replaces the base when the right side is absolute). Before
5765        // BehaviorSpec::validate ran, an `:on-init "/etc/passwd"` would
5766        // surface as a confusing "missing behavior-callback /etc/passwd"
5767        // — or, worse, pass when /etc/passwd happens to exist. Now it's
5768        // a value-shape error naming the slot.
5769        use crate::BehaviorSpec;
5770        let root = PathBuf::from("/tmp/x");
5771        let manifest = root.join("caixa.lisp");
5772        let svc = root.join("servicos/demo.computeunit.yaml");
5773        let mut c = caixa(CaixaKind::Servico);
5774        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5775        c.behavior = Some(BehaviorSpec {
5776            on_init: Some(PathBuf::from("/etc/passwd")),
5777            ..Default::default()
5778        });
5779        // Path exists check would *succeed* on /etc/passwd (proving the
5780        // sandbox bypass) — value-shape pass must fire first.
5781        let layout = StandardLayout::new()
5782            .with_path_exists(move |p| p == manifest || p == svc || p == Path::new("/etc/passwd"));
5783        let err = layout.verify(&c, &root).unwrap_err();
5784        assert!(
5785            matches!(err, LayoutError::BehaviorViolation { ref caixa, .. } if caixa == "demo"),
5786            "got {err:?}",
5787        );
5788    }
5789
5790    #[test]
5791    fn behavior_empty_callback_is_violation() {
5792        use crate::BehaviorSpec;
5793        let root = PathBuf::from("/tmp/x");
5794        let manifest = root.join("caixa.lisp");
5795        let svc = root.join("servicos/demo.computeunit.yaml");
5796        let mut c = caixa(CaixaKind::Servico);
5797        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5798        c.behavior = Some(BehaviorSpec {
5799            on_call: Some(PathBuf::new()),
5800            ..Default::default()
5801        });
5802        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
5803        let err = layout.verify(&c, &root).unwrap_err();
5804        assert!(matches!(err, LayoutError::BehaviorViolation { .. }));
5805    }
5806
5807    #[test]
5808    fn upgrade_from_duplicate_surfaces_as_upgrade_violation() {
5809        // Wiring pin: the cross-entry duplicate-`:from` gate in
5810        // `validate_upgrade_from` lands on the same
5811        // `LayoutError::UpgradeViolation` axis the per-entry
5812        // `UpgradeFromEntry::validate` already does (26da2c7), so a
5813        // caixa.lisp with two `(:from "0.1.0" …)` blocks surfaces at
5814        // `feira build` time naming the offending caixa rather than
5815        // silently passing into the wasm-operator's non-deterministic
5816        // dispatch. Mirrors `behavior_empty_callback_is_violation` on
5817        // the peer M2 typed slot.
5818        use crate::{UpgradeFromEntry, UpgradeInstruction};
5819        let root = PathBuf::from("/tmp/x");
5820        let manifest = root.join("caixa.lisp");
5821        let svc = root.join("servicos/demo.computeunit.yaml");
5822        let mut c = caixa(CaixaKind::Servico);
5823        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5824        c.upgrade_from = vec![
5825            UpgradeFromEntry {
5826                from: "0.1.0".into(),
5827                instructions: vec![UpgradeInstruction::Restart],
5828            },
5829            UpgradeFromEntry {
5830                from: "0.1.0".into(),
5831                instructions: vec![UpgradeInstruction::Restart],
5832            },
5833        ];
5834        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
5835        let err = layout.verify(&c, &root).unwrap_err();
5836        let LayoutError::UpgradeViolation { caixa, issue } = err else {
5837            panic!("expected LayoutError::UpgradeViolation for duplicate `:from`, got {err:?}");
5838        };
5839        assert_eq!(caixa, "demo");
5840        assert!(
5841            issue.contains("0.1.0"),
5842            "UpgradeViolation issue must name the offending `:from` verbatim, got {issue:?}"
5843        );
5844    }
5845
5846    #[test]
5847    fn upgrade_from_downgrade_surfaces_as_upgrade_violation() {
5848        // Wiring pin: the cross-slot precedence gate in
5849        // `validate_upgrade_from_against_versao` lands on the same
5850        // `LayoutError::UpgradeViolation` axis the per-entry and
5851        // cross-entry gates already do (26da2c7, 7c6aef2), so a
5852        // caixa.lisp whose `:upgrade-from :from` is greater than the
5853        // caixa's own `:versao` surfaces at `feira build` time
5854        // naming the offending caixa rather than silently passing
5855        // into the wasm-operator's `:from`-match dispatch where the
5856        // entry would sit dormant forever. Mirrors
5857        // `upgrade_from_duplicate_surfaces_as_upgrade_violation` on
5858        // the peer cross-entry gate.
5859        use crate::{UpgradeFromEntry, UpgradeInstruction};
5860        let root = PathBuf::from("/tmp/x");
5861        let manifest = root.join("caixa.lisp");
5862        let svc = root.join("servicos/demo.computeunit.yaml");
5863        let mut c = caixa(CaixaKind::Servico);
5864        c.versao = "0.1.5".into();
5865        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5866        c.upgrade_from = vec![UpgradeFromEntry {
5867            from: "0.2.0".into(),
5868            instructions: vec![UpgradeInstruction::Restart],
5869        }];
5870        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
5871        let err = layout.verify(&c, &root).unwrap_err();
5872        let LayoutError::UpgradeViolation { caixa, issue } = err else {
5873            panic!(
5874                "expected LayoutError::UpgradeViolation for downgrade-shaped `:from`, got {err:?}"
5875            );
5876        };
5877        assert_eq!(caixa, "demo");
5878        assert!(
5879            issue.contains("0.2.0") && issue.contains("0.1.5"),
5880            "UpgradeViolation issue must name both `:from` and `:versao` verbatim, got {issue:?}"
5881        );
5882    }
5883
5884    #[test]
5885    fn upgrade_from_equal_to_versao_surfaces_as_upgrade_violation() {
5886        // Self-upgrade no-op arm: `:from "0.1.0"` while
5887        // `:versao "0.1.0"` declares "upgrade from myself to
5888        // myself", which the operator's dispatch either skips
5889        // silently or trivially "succeeds" with no observable
5890        // transition. Surfaces at validate time naming both values
5891        // so the author can fix in one edit.
5892        use crate::{UpgradeFromEntry, UpgradeInstruction};
5893        let root = PathBuf::from("/tmp/x");
5894        let manifest = root.join("caixa.lisp");
5895        let svc = root.join("servicos/demo.computeunit.yaml");
5896        let mut c = caixa(CaixaKind::Servico);
5897        c.versao = "0.1.0".into();
5898        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5899        c.upgrade_from = vec![UpgradeFromEntry {
5900            from: "0.1.0".into(),
5901            instructions: vec![UpgradeInstruction::Restart],
5902        }];
5903        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
5904        let err = layout.verify(&c, &root).unwrap_err();
5905        let LayoutError::UpgradeViolation { caixa, issue } = err else {
5906            panic!(
5907                "expected LayoutError::UpgradeViolation for self-upgrade `:from == :versao`, got \
5908                 {err:?}"
5909            );
5910        };
5911        assert_eq!(caixa, "demo");
5912        assert!(
5913            issue.contains("0.1.0"),
5914            "UpgradeViolation issue must name the equal `:from`/`:versao` verbatim, got {issue:?}"
5915        );
5916    }
5917
5918    #[test]
5919    fn upgrade_from_strict_upgrade_passes_layout() {
5920        // Positive control for the precedence gate at the
5921        // LayoutInvariants level: a valid `:from < :versao` chain
5922        // (`0.1.0 → 0.2.0`) must not regress into a false-positive
5923        // `UpgradeViolation`. Mirrors `behavior_callback_path_must_exist`'s
5924        // positive-control arm.
5925        use crate::{UpgradeFromEntry, UpgradeInstruction};
5926        let root = PathBuf::from("/tmp/x");
5927        let manifest = root.join("caixa.lisp");
5928        let svc = root.join("servicos/demo.computeunit.yaml");
5929        let mut c = caixa(CaixaKind::Servico);
5930        c.versao = "0.2.0".into();
5931        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5932        c.upgrade_from = vec![UpgradeFromEntry {
5933            from: "0.1.0".into(),
5934            instructions: vec![UpgradeInstruction::Restart],
5935        }];
5936        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
5937        layout.verify(&c, &root).unwrap();
5938    }
5939
5940    #[test]
5941    fn upgrade_script_path_must_exist() {
5942        use crate::{BehaviorSpec, UpgradeFromEntry, UpgradeInstruction};
5943        use std::path::PathBuf;
5944        let root = PathBuf::from("/tmp/x");
5945        let manifest = root.join("caixa.lisp");
5946        let svc = root.join("servicos/demo.computeunit.yaml");
5947        let on_state_change = root.join("lib/migrations.lisp");
5948        let mut c = caixa(CaixaKind::Servico);
5949        // `:versao` past the entry's `:from` so the cross-slot
5950        // precedence gate (`FromNotBeforeVersao`) lets this case
5951        // through to the path-existence pass under test.
5952        c.versao = "0.2.0".into();
5953        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5954        // `:on-state-change` declared so the cross-slot composition
5955        // gate (`validate_upgrade_from_against_behavior`) lets the
5956        // `:state-change` entry through to the path-existence pass
5957        // under test. Without the callback the missing-callback gate
5958        // would surface first and the path-existence pass wouldn't be
5959        // exercised.
5960        c.behavior = Some(BehaviorSpec {
5961            on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
5962            ..Default::default()
5963        });
5964        // A `:load-module` precedes the `:state-change` so the entry
5965        // satisfies the within-entry state-change-ordering gate
5966        // (`StateChangeWithoutPriorLoad`) and the path-existence pass
5967        // under test is the gate actually exercised. `:load-module`
5968        // carries no on-disk path, so it adds no existence requirement.
5969        c.upgrade_from = vec![UpgradeFromEntry {
5970            from: "0.1.0".into(),
5971            instructions: vec![
5972                UpgradeInstruction::LoadModule {
5973                    module: "demo".into(),
5974                },
5975                UpgradeInstruction::StateChange {
5976                    script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
5977                },
5978            ],
5979        }];
5980        let manifest_clone = manifest.clone();
5981        let svc_clone = svc.clone();
5982        let on_state_change_clone = on_state_change.clone();
5983        let layout = StandardLayout::new().with_path_exists(move |p| {
5984            p == manifest_clone || p == svc_clone || p == on_state_change_clone
5985        });
5986        let err = layout.verify(&c, &root).unwrap_err();
5987        assert!(matches!(
5988            err,
5989            LayoutError::MissingEntry { kind, .. }
5990                if kind == crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT
5991        ));
5992    }
5993
5994    #[test]
5995    fn layout_missing_entry_kind_m2_consts_pin_canonical_kebab_case_labels() {
5996        // Byte-identity pin: the two per-M2-slot leaf-kind labels the
5997        // [`LayoutError::MissingEntry`] `kind: &'static str`
5998        // discriminator surfaces under (the M2 `:behavior` per-callback
5999        // on-disk-leaf axis, the M2 `:upgrade-from :instructions`
6000        // per-`:state-change` script-path on-disk-leaf axis) route
6001        // through the lifted [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`]
6002        // / [`crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT`]
6003        // consts, so a future rebrand that reaches the const but not
6004        // the production emit / test probe (or vice versa) surfaces
6005        // here at build time rather than at runtime as a downstream
6006        // [`LayoutError::MissingEntry`] `kind: <stale-label>`
6007        // diagnostic mismatch far from the rename's commit. Mirror of
6008        // the peer
6009        // [`crate::aplicacao::tests::contrato_author_key_consts_pin_canonical_kebab_case_labels`]
6010        // (f50c875) and
6011        // [`crate::upgrade::tests::upgrade_instruction_kind_consts_pin_canonical_kebab_case_tags`]
6012        // (56120ef) byte-identity pins on the sibling M3 `:contratos`
6013        // per-entry endpoint-label + M2 `:upgrade-from :instructions`
6014        // per-variant kind-tag axes.
6015        assert_eq!(
6016            crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
6017            "behavior-callback"
6018        );
6019        assert_eq!(
6020            crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
6021            "upgrade-script"
6022        );
6023    }
6024
6025    #[test]
6026    fn layout_missing_entry_kind_m0_consts_pin_canonical_code_slot_labels() {
6027        // Byte-identity pin: the three per-M0-code-slot leaf-kind
6028        // labels the [`LayoutError::MissingEntry`] `kind: &'static
6029        // str` discriminator surfaces under (the `:bibliotecas`
6030        // per-entry axis, the `:exe` per-entry axis, the `:servicos`
6031        // per-entry axis) route through the lifted
6032        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`] /
6033        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] /
6034        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] consts,
6035        // so a future rebrand that reaches the const but not the
6036        // production emit (or vice versa) surfaces here at build time
6037        // rather than at runtime as a downstream
6038        // [`LayoutError::MissingEntry`] `kind: <stale-label>`
6039        // diagnostic mismatch far from the rename's commit. Mirror of
6040        // the peer M2-tier pin
6041        // [`layout_missing_entry_kind_m2_consts_pin_canonical_kebab_case_labels`]
6042        // (95c9c4c) on the sibling `:behavior` / `:upgrade-from`
6043        // per-slot leaf-kind axes.
6044        assert_eq!(
6045            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
6046            "biblioteca"
6047        );
6048        assert_eq!(crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE, "exe");
6049        assert_eq!(crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO, "servico");
6050    }
6051
6052    #[test]
6053    fn layout_missing_entry_kind_m0_consts_align_with_caixa_kind_as_str() {
6054        // Cross-axis byte-identity pin: the two `:kind`-namesake M0
6055        // leaf-kind labels ([`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`]
6056        // = `"biblioteca"`,
6057        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] =
6058        // `"servico"`) must equal [`crate::CaixaKind::Biblioteca`] /
6059        // [`crate::CaixaKind::Servico`]'s
6060        // [`crate::CaixaKind::as_str`] outputs verbatim — the
6061        // substrate's canonical human-readable-kind axis and the
6062        // layout diagnostic's per-slot leaf-kind axis share one
6063        // vocabulary for these two arms by design (both label the
6064        // caixa's code-producing shape by its Portuguese-native
6065        // idiom), so drift between the two lands as a build-time
6066        // pattern-arm miss here rather than as a runtime diagnostic
6067        // that reads inconsistently across `feira build`'s
6068        // per-invocation output.
6069        //
6070        // The third M0 arm ([`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`]
6071        // = `"exe"`) is deliberately *distinct* from
6072        // [`crate::CaixaKind::Binario`]'s [`crate::CaixaKind::as_str`]
6073        // output (`"binario"`) — the `:exe` code slot names the
6074        // per-directory leaf-kind at the `exe/` subtree, whereas
6075        // [`crate::CaixaKind::Binario`] names the caixa's own runtime
6076        // kind. Two axes, two labels — the inequality assertion here
6077        // pins the split so a future accidental collapse of the two
6078        // onto one scalar (a rebrand that reroutes either axis to
6079        // match the other) trips at build time.
6080        assert_eq!(
6081            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
6082            CaixaKind::Biblioteca.as_str()
6083        );
6084        assert_eq!(
6085            crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
6086            CaixaKind::Servico.as_str()
6087        );
6088        assert_ne!(
6089            crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
6090            CaixaKind::Binario.as_str(),
6091            "`exe` leaf-kind label names the per-directory code-slot \
6092             axis; `binario` names the caixa-kind axis — the two must \
6093             not silently collapse onto one scalar"
6094        );
6095    }
6096
6097    #[test]
6098    fn layout_missing_entry_kind_consts_are_pairwise_distinct() {
6099        // Distinctness pin: the five [`LayoutError::MissingEntry`]
6100        // `kind: &'static str` accept-set members
6101        // ([`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`] /
6102        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] /
6103        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] on the
6104        // M0 code-slot arms plus
6105        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`]
6106        // / [`crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT`]
6107        // on the M2 slot arms) must be pairwise distinct — an
6108        // accidental copy-paste flip that reroutes one label's byte-
6109        // string to also match another silently collapses two
6110        // per-slot diagnostics onto one, so an operator running
6111        // `feira build` reads `kind: "biblioteca"` for what should
6112        // have surfaced as a `:behavior :on-init` script-not-found
6113        // diagnostic (or vice versa). This pin catches any such
6114        // flip at build time. Mirror of the peer
6115        // [`crate::render::tests::m2_limits_key_consts_are_pairwise_distinct`]
6116        // / peer distinctness pins on other closed-set typed axes.
6117        let entries: &[(&str, &str)] = &[
6118            (
6119                "LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA",
6120                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
6121            ),
6122            (
6123                "LAYOUT_MISSING_ENTRY_KIND_EXE",
6124                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
6125            ),
6126            (
6127                "LAYOUT_MISSING_ENTRY_KIND_SERVICO",
6128                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
6129            ),
6130            (
6131                "LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK",
6132                crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
6133            ),
6134            (
6135                "LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT",
6136                crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
6137            ),
6138        ];
6139        for (i, (name_a, value_a)) in entries.iter().enumerate() {
6140            for (name_b, value_b) in entries.iter().skip(i + 1) {
6141                assert_ne!(
6142                    value_a, value_b,
6143                    "LAYOUT_MISSING_ENTRY_KIND_* consts must be \
6144                     pairwise-distinct byte-strings — {name_a} and \
6145                     {name_b} both resolve to {value_a:?}"
6146                );
6147            }
6148        }
6149    }
6150
6151    #[test]
6152    fn layout_dir_consts_pin_canonical_directory_names() {
6153        // Scalar-value pin for the three [`crate::render::LAYOUT_DIR_*`]
6154        // consts naming the CSE-invariant per-[`CaixaKind`]
6155        // on-disk-directory-name axes the substrate's layout invariants
6156        // pin (`lib/` for [`CaixaKind::Biblioteca`], `exe/` for
6157        // [`CaixaKind::Binario`], `servicos/` for [`CaixaKind::Servico`]).
6158        // A future rebrand of any of the three on-disk directory landing
6159        // conventions must reach this pin — the const-edit lands on one
6160        // arm, the assertion here re-pins the new byte-string, and every
6161        // downstream consumer (the caixa-feira `init` / `fmt` / `lint` /
6162        // `tofu` scaffolders, the [`crate::LayoutInvariants::verify`]
6163        // sandbox reconstruction, the future
6164        // `feira app deploy`-cluster scaffolder) picks up the new
6165        // directory name at build time. Mirror of the peer
6166        // [`layout_missing_entry_kind_m0_consts_pin_canonical_code_slot_labels`]
6167        // (fe2a898) on the sibling
6168        // [`crate::LayoutError::MissingEntry`] `kind:` discriminator
6169        // axis this on-disk-directory axis composes with.
6170        assert_eq!(crate::render::LAYOUT_DIR_LIB, "lib");
6171        assert_eq!(crate::render::LAYOUT_DIR_EXE, "exe");
6172        assert_eq!(crate::render::LAYOUT_DIR_SERVICOS, "servicos");
6173    }
6174
6175    #[test]
6176    fn layout_dir_consts_are_pairwise_distinct() {
6177        // Distinctness pin: the three per-[`CaixaKind`]
6178        // on-disk-directory-name arms must resolve to pairwise-distinct
6179        // byte-strings — a future accidental copy-paste flip that
6180        // reroutes any one of the three onto another's value silently
6181        // collapses two per-kind on-disk sandboxes onto one, so
6182        // [`crate::LayoutInvariants::verify`] would gate a
6183        // [`CaixaKind::Binario`] caixa's `:exe` entries against the
6184        // wrong sub-tree (or a `:kind Servico` caixa's `:servicos`
6185        // entries against `lib/` and pass every entry `feira build`
6186        // should have rejected as [`crate::LayoutError::ServicoOutsideDir`]).
6187        // Mirror of the peer
6188        // [`layout_missing_entry_kind_consts_are_pairwise_distinct`]
6189        // (fe2a898) on the sibling leaf-kind label accept-set.
6190        let entries: &[(&str, &str)] = &[
6191            ("LAYOUT_DIR_LIB", crate::render::LAYOUT_DIR_LIB),
6192            ("LAYOUT_DIR_EXE", crate::render::LAYOUT_DIR_EXE),
6193            ("LAYOUT_DIR_SERVICOS", crate::render::LAYOUT_DIR_SERVICOS),
6194        ];
6195        for (i, (name_a, value_a)) in entries.iter().enumerate() {
6196            for (name_b, value_b) in entries.iter().skip(i + 1) {
6197                assert_ne!(
6198                    value_a, value_b,
6199                    "LAYOUT_DIR_* consts must be pairwise-distinct \
6200                     byte-strings — {name_a} and {name_b} both resolve \
6201                     to {value_a:?}"
6202                );
6203            }
6204        }
6205    }
6206
6207    #[test]
6208    fn layout_dir_exe_matches_layout_missing_entry_kind_exe() {
6209        // Cross-axis byte-identity pin: [`crate::render::LAYOUT_DIR_EXE`]
6210        // (the on-disk-directory-name arm) equals
6211        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] (the
6212        // [`LayoutError::MissingEntry`] `kind:` leaf-kind categorization
6213        // arm) verbatim — the M0 `:kind Binario` on-disk-directory axis
6214        // and the [`crate::LayoutError::MissingEntry`] `kind:` leaf-kind
6215        // discriminator name the same three-byte sub-tree (`exe/`), a
6216        // coincidence [`crate::LayoutInvariants::verify`] itself relies
6217        // on: it joins `root` with [`crate::render::LAYOUT_DIR_EXE`] to
6218        // reconstruct `exe_dir` and emits [`crate::LayoutError::MissingEntry
6219        // { kind: LAYOUT_MISSING_ENTRY_KIND_EXE, path: <under exe_dir> }`]
6220        // for every non-resolving entry. Making the coincidence
6221        // load-bearing means a future rebrand touching either axis
6222        // without the other (a per-consumer disambiguation collapsing
6223        // the leaf-kind label onto `"binary"` while the directory stays
6224        // `"exe"`, or vice versa) trips at caixa-core build time rather
6225        // than surfacing at runtime as a mismatched
6226        // [`crate::LayoutInvariants::verify`] diagnostic whose `kind:`
6227        // reads one label while the `path:` sits under a differently-named
6228        // sub-tree.
6229        assert_eq!(
6230            crate::render::LAYOUT_DIR_EXE,
6231            crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
6232            "LAYOUT_DIR_EXE must equal LAYOUT_MISSING_ENTRY_KIND_EXE — \
6233             both name the M0 `:kind Binario` sub-tree by the same \
6234             three-byte scalar"
6235        );
6236    }
6237
6238    #[test]
6239    fn layout_dir_bib_is_distinct_from_layout_missing_entry_kind_bib() {
6240        // Cross-axis distinctness pin: [`crate::render::LAYOUT_DIR_LIB`]
6241        // (`"lib"`, the Cargo-style abbreviated on-disk directory name)
6242        // is *deliberately* distinct from
6243        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`]
6244        // (`"biblioteca"`, the full-form Portuguese-native leaf-kind
6245        // label) — the substrate splits the on-disk convention terse
6246        // (`lib/`) from the diagnostic vocabulary full (`biblioteca`),
6247        // matching Cargo's `src/lib.rs` abbreviation of the `library`
6248        // crate-type discriminator. A future accidental collapse of the
6249        // two axes onto one scalar (a rebrand aligning either arm with
6250        // the other for schema-clarity, an English-uniformity pass that
6251        // renames `LAYOUT_DIR_LIB` to `LAYOUT_DIR_BIBLIOTECA` or the
6252        // diagnostic label to `"lib"`) would silently reroute either
6253        // consumer onto the other's byte-string. This pin catches the
6254        // collapse at build time. Peer of the sibling
6255        // [`layout_missing_entry_kind_m0_consts_align_with_caixa_kind_as_str`]
6256        // (fe2a898) that pins the analogous *equality* between the M0
6257        // `:kind Biblioteca` diagnostic-label arm and
6258        // [`crate::CaixaKind::Biblioteca`]'s [`crate::CaixaKind::as_str`]
6259        // output — the two pins jointly encode the "which of the three
6260        // Biblioteca-related scalars are load-bearing-equal, which are
6261        // load-bearing-distinct" invariant across the substrate's
6262        // per-kind vocabulary.
6263        assert_ne!(
6264            crate::render::LAYOUT_DIR_LIB,
6265            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
6266            "LAYOUT_DIR_LIB (`\"lib\"`) and LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA \
6267             (`\"biblioteca\"`) name two distinct axes — the on-disk \
6268             directory convention (Cargo-style abbreviated) and the \
6269             layout-diagnostic leaf-kind label (full-form Portuguese) — \
6270             and must not silently collapse onto one scalar"
6271        );
6272    }
6273
6274    #[test]
6275    fn layout_dir_servicos_is_distinct_from_layout_missing_entry_kind_servico() {
6276        // Cross-axis distinctness pin: [`crate::render::LAYOUT_DIR_SERVICOS`]
6277        // (`"servicos"`, the Portuguese-*plural* on-disk directory
6278        // name) is *deliberately* distinct from
6279        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`]
6280        // (`"servico"`, the singular leaf-kind label) — the on-disk
6281        // sub-tree houses one-or-more ComputeUnit YAML descriptors per
6282        // caixa (hence the plural), the diagnostic label names the
6283        // caixa's own kind (singular). A future accidental collapse
6284        // onto one scalar (a per-consumer disambiguation aligning the
6285        // two, a hypothetical English-uniformity pass renaming
6286        // `"servicos"` → `"services"` while retaining `"servico"` on
6287        // the diagnostic arm — or vice versa) would silently reroute
6288        // either consumer onto the other's byte-string. Peer of the
6289        // sibling [`layout_dir_bib_is_distinct_from_layout_missing_entry_kind_bib`]
6290        // pin on the M0 `:kind Biblioteca` split axis; two of the three
6291        // per-kind on-disk / leaf-kind splits carry a distinctness
6292        // pin here, the third ([`crate::render::LAYOUT_DIR_EXE`] vs
6293        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`]) carries an
6294        // equality pin under
6295        // [`layout_dir_exe_matches_layout_missing_entry_kind_exe`].
6296        assert_ne!(
6297            crate::render::LAYOUT_DIR_SERVICOS,
6298            crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
6299            "LAYOUT_DIR_SERVICOS (`\"servicos\"`, plural on-disk sub-tree) \
6300             and LAYOUT_MISSING_ENTRY_KIND_SERVICO (`\"servico\"`, singular \
6301             leaf-kind label) name two distinct axes and must not silently \
6302             collapse onto one scalar"
6303        );
6304    }
6305
6306    #[test]
6307    fn layout_invariants_reconstruct_sandbox_roots_through_lifted_layout_dir_consts() {
6308        // Production-through-const pin: [`LayoutInvariants::verify`]
6309        // routes its three per-kind sandbox-root joins
6310        // (`root.join(LAYOUT_DIR_LIB)` for the `:kind Biblioteca`
6311        // default `lib/<nome>.lisp` reconstruction, `root.join(LAYOUT_DIR_EXE)`
6312        // for the [`LayoutError::ExeOutsideDir`] gate,
6313        // `root.join(LAYOUT_DIR_SERVICOS)` for the
6314        // [`LayoutError::ServicoOutsideDir`] gate) through the three
6315        // lifted consts, not through inline `"lib"` / `"exe"` /
6316        // `"servicos"` `&str` literals. This test drives the
6317        // [`LayoutError::ExeOutsideDir`] arm through a `:kind Binario`
6318        // caixa whose declared `:exe` entry deliberately escapes
6319        // `root.join(LAYOUT_DIR_EXE)` (a sibling `bin/tool` path) —
6320        // if the production emit reads the wrong const (or reverts to
6321        // an inline literal that drifts from the const) the diagnostic
6322        // arm surfaces the wrong variant, catching the drift at build
6323        // time rather than as a per-invocation runtime mismatch.
6324        //
6325        // Mirror of the peer production-through-const pin
6326        // [`crate::dep::tests::validate_no_self_dep_deps_field_routes_through_dep_author_key`]
6327        // (4da6fba) on the sibling M0 `:deps` `list:` diagnostic axis.
6328        use std::path::PathBuf;
6329        let root = PathBuf::from("/tmp/x");
6330        let manifest = root.join("caixa.lisp");
6331        let bin_entry_outside = root.join("bin/tool");
6332        let mut c = caixa(CaixaKind::Binario);
6333        c.exe = vec!["bin/tool".into()];
6334        let manifest_clone = manifest.clone();
6335        let outside_clone = bin_entry_outside.clone();
6336        let layout = StandardLayout::new()
6337            .with_path_exists(move |p| p == manifest_clone || p == outside_clone);
6338        let err = layout.verify(&c, &root).unwrap_err();
6339        match err {
6340            LayoutError::ExeOutsideDir(path) => {
6341                assert_eq!(
6342                    path, bin_entry_outside,
6343                    "ExeOutsideDir must carry the resolved `:exe` entry that \
6344                     escapes `root.join(LAYOUT_DIR_EXE)`"
6345                );
6346                // Byte-identity check: the escape must be against the
6347                // lifted `LAYOUT_DIR_EXE` sub-tree, not a stale inline
6348                // literal — a future const-edit that drifts from `"exe"`
6349                // reroutes `exe_dir` off the sandbox `bin/tool` escapes
6350                // from, and this pattern-arm miss re-surfaces here.
6351                assert!(
6352                    !path.starts_with(root.join(crate::render::LAYOUT_DIR_EXE)),
6353                    "resolved `:exe` entry {path:?} must escape the \
6354                     `root.join(LAYOUT_DIR_EXE)` sub-tree the production \
6355                     emit uses to gate the [`LayoutError::ExeOutsideDir`] arm"
6356                );
6357            }
6358            other => panic!("expected ExeOutsideDir, got {other:?}"),
6359        }
6360    }
6361
6362    #[test]
6363    fn upgrade_state_change_without_behavior_callback_surfaces_as_upgrade_violation() {
6364        // Wiring pin for the cross-slot composition gate
6365        // (`validate_upgrade_from_against_behavior`): a caixa whose
6366        // `:upgrade-from` declares a `(:state-change "lib/m.lisp")`
6367        // instruction but does not declare `:behavior :on-state-change`
6368        // surfaces at `feira build` time as a `LayoutError::UpgradeViolation`
6369        // naming the offending caixa + the entry's `:from` + the
6370        // offending script — not at hot-upgrade dispatch when the
6371        // operator reaches for the missing callback. Mirrors
6372        // `upgrade_from_downgrade_surfaces_as_upgrade_violation` on the
6373        // peer `:from` ↔ `:versao` cross-slot precedence gate.
6374        use crate::{UpgradeFromEntry, UpgradeInstruction};
6375        use std::path::PathBuf;
6376        let root = PathBuf::from("/tmp/x");
6377        let manifest = root.join("caixa.lisp");
6378        let svc = root.join("servicos/demo.computeunit.yaml");
6379        let mut c = caixa(CaixaKind::Servico);
6380        c.versao = "0.2.0".into();
6381        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6382        // `:behavior` is None (the canonical "I added the upgrade path
6383        // but never declared :behavior" footgun the gate closes); a
6384        // peer arm covers the BehaviorSpec-Some-but-on-state-change-
6385        // None shape in `upgrade::tests::behavior_gate_rejects_state_
6386        // change_when_on_state_change_is_none`.
6387        c.upgrade_from = vec![UpgradeFromEntry {
6388            from: "0.1.0".into(),
6389            instructions: vec![
6390                UpgradeInstruction::LoadModule {
6391                    module: "demo".into(),
6392                },
6393                UpgradeInstruction::StateChange {
6394                    script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
6395                },
6396            ],
6397        }];
6398        let manifest_clone = manifest.clone();
6399        let svc_clone = svc.clone();
6400        let layout =
6401            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
6402        let err = layout.verify(&c, &root).unwrap_err();
6403        match err {
6404            LayoutError::UpgradeViolation { caixa, issue } => {
6405                assert_eq!(caixa, "demo", "diagnostic must name the offending caixa");
6406                assert!(
6407                    issue.contains(crate::render::M2_BEHAVIOR_AUTHOR_KEY_ON_STATE_CHANGE),
6408                    "diagnostic must name the missing callback slot for self-locating fix, \
6409                     got {issue:?}"
6410                );
6411                assert!(
6412                    issue.contains("0.1.0"),
6413                    "diagnostic must name the offending entry's :from, got {issue:?}"
6414                );
6415                assert!(
6416                    issue.contains("v01-to-v02.lisp"),
6417                    "diagnostic must name the offending :script for self-locating fix, \
6418                     got {issue:?}"
6419                );
6420            }
6421            other => panic!("expected UpgradeViolation, got {other:?}"),
6422        }
6423    }
6424
6425    #[test]
6426    fn supervisor_must_have_children() {
6427        use crate::RestartStrategy;
6428        let root = PathBuf::from("/tmp/x");
6429        let manifest = root.join("caixa.lisp");
6430        let manifest_clone = manifest.clone();
6431        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6432        let mut c = caixa(CaixaKind::Supervisor);
6433        c.estrategia = Some(RestartStrategy::OneForOne);
6434        c.max_restarts = Some(5);
6435        // No children → should fail
6436        let err = layout.verify(&c, &root).unwrap_err();
6437        assert!(matches!(err, LayoutError::SupervisorViolation { .. }));
6438    }
6439
6440    #[test]
6441    fn supervisor_self_referential_child_is_violation() {
6442        // A Supervisor whose `:children` names its own `:nome` is a
6443        // one-node supervision cycle. The cross-slot gate fires at
6444        // verify time, surfacing as a SupervisorViolation that names the
6445        // offending supervisor — not at the cluster apply far from
6446        // source. The `caixa()` helper's `:nome` is "demo".
6447        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6448        let root = PathBuf::from("/tmp/x");
6449        let manifest = root.join("caixa.lisp");
6450        let manifest_clone = manifest.clone();
6451        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6452        let mut c = caixa(CaixaKind::Supervisor);
6453        c.estrategia = Some(RestartStrategy::OneForOne);
6454        c.max_restarts = Some(5);
6455        c.children = vec![
6456            ChildSpec {
6457                caixa: "worker".into(),
6458                versao: "^0.1".into(),
6459                restart: RestartPolicy::Permanent,
6460            },
6461            ChildSpec {
6462                caixa: "demo".into(),
6463                versao: "^0.1".into(),
6464                restart: RestartPolicy::Permanent,
6465            },
6466        ];
6467        let err = layout.verify(&c, &root).unwrap_err();
6468        let LayoutError::SupervisorViolation { caixa, issue } = err else {
6469            panic!("expected SupervisorViolation for self-referential child, got {err:?}");
6470        };
6471        assert_eq!(caixa, "demo");
6472        assert!(
6473            issue.contains("demo") && issue.contains("itself"),
6474            "issue must name the self-supervising caixa, got {issue:?}"
6475        );
6476    }
6477
6478    #[test]
6479    fn supervisor_distinct_children_pass_self_supervision_gate() {
6480        // Positive control: a Supervisor whose children are all distinct
6481        // from its own `:nome` verifies cleanly.
6482        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6483        let root = PathBuf::from("/tmp/x");
6484        let manifest = root.join("caixa.lisp");
6485        let manifest_clone = manifest.clone();
6486        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6487        let mut c = caixa(CaixaKind::Supervisor);
6488        c.estrategia = Some(RestartStrategy::OneForOne);
6489        c.max_restarts = Some(5);
6490        c.children = vec![ChildSpec {
6491            caixa: "worker".into(),
6492            versao: "^0.1".into(),
6493            restart: RestartPolicy::Permanent,
6494        }];
6495        layout.verify(&c, &root).unwrap();
6496    }
6497
6498    #[test]
6499    fn cross_slot_self_edge_gates_route_parent_nome_through_lifted_accessor() {
6500        // Composition pin: every cross-slot self-edge gate fired from
6501        // `LayoutInvariants::verify` — the supervision-tree arm's
6502        // `crate::supervisor::validate_no_self_supervision` call, the
6503        // Aplicacao arm's `crate::aplicacao::validate_no_self_membership`
6504        // call, and the dep-graph arm's
6505        // `crate::dep::validate_no_self_dep` call — must key its
6506        // `parent_nome` arg off the typed [`Caixa::nome`] accessor, not
6507        // the raw `&caixa.nome` `&String`-borrow of the underlying
6508        // field.
6509        //
6510        // Structurally: a rename of the storage field or a hypothetical
6511        // accessor rebrand (a per-cluster alias table pinned through a
6512        // future `:placement`-scoped slot, the M4 CR materializer's
6513        // per-CR namespace-qualified rewrite, a `:nome-suffix` overlay
6514        // the MESH-COMPOSITION §III.2 roadmap acknowledges) would land
6515        // through the accessor by construction; a raw-borrow bypass
6516        // would silently disagree with every peer consumer that already
6517        // routes through `caixa.nome()` (the caixa-mesh 980c059,
6518        // caixa-helm 22461ef, caixa-flux 162e2e2, caixa-crd 61d3429,
6519        // caixa-feira ef83332 raw-borrow converges), reintroducing the
6520        // drift surface the sibling converges closed. Each arm fires
6521        // its per-kind `LayoutError` variant (`SupervisorViolation` /
6522        // `AplicacaoViolation` / `DepsViolation`) whose `caixa` field
6523        // carries the offending parent name verbatim through
6524        // `caixa.nome().clone()`; asserting the field equals
6525        // `caixa.nome()` on the mutated fixture pins the accessor-
6526        // routed parent-nome projection at every call site — a future
6527        // silent detour that had the gate observe a stale / aliased
6528        // name at the arg boundary would surface here as a
6529        // `caixa != "demo"` inequality.
6530        //
6531        // Peer of the sibling per-caixa-crate `nome`-arg raw-borrow
6532        // convergence pin discipline (54bf2f3 / 22461ef / 162e2e2 on the
6533        // renderer crates; ef83332 on the CLI) — extends the "one typed
6534        // dispatch per `:nome` consumer" discipline onto the substrate's
6535        // own [`LayoutInvariants::verify`] cross-slot self-edge gate
6536        // wire-up on all three typed-name-graph kinds.
6537        use crate::{
6538            ChildSpec, Dep, Membro, Placement, PlacementStrategy, RestartPolicy, RestartStrategy,
6539        };
6540        let root = PathBuf::from("/tmp/x");
6541        let manifest = root.join("caixa.lisp");
6542        let manifest_clone = manifest.clone();
6543        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6544
6545        // Supervisor arm — the `caixa()` helper's `:nome` is "demo",
6546        // and the accessor's return `caixa.nome()` must equal the
6547        // parent-nome that the self-supervision gate observes.
6548        let mut sup = caixa(CaixaKind::Supervisor);
6549        sup.estrategia = Some(RestartStrategy::OneForOne);
6550        sup.max_restarts = Some(5);
6551        sup.children = vec![ChildSpec {
6552            caixa: "demo".into(),
6553            versao: "^0.1".into(),
6554            restart: RestartPolicy::Permanent,
6555        }];
6556        let parent_nome_via_accessor = sup.nome();
6557        assert_eq!(
6558            parent_nome_via_accessor, "demo",
6559            "the caixa() fixture helper's `:nome` must be \"demo\" — \
6560             the accessor's return is the pin's ground truth for the \
6561             cross-slot gate's parent-nome arg",
6562        );
6563        let err = layout.verify(&sup, &root).unwrap_err();
6564        let LayoutError::SupervisorViolation { caixa: c_nome, .. } = err else {
6565            panic!("expected SupervisorViolation for self-referential child, got {err:?}");
6566        };
6567        assert_eq!(
6568            c_nome, parent_nome_via_accessor,
6569            "the SupervisorViolation's `caixa` field must equal \
6570             `sup.nome()` — the cross-slot self-supervision gate's \
6571             `parent_nome` arg must route through the lifted \
6572             [`Caixa::nome`] accessor, not the raw `&caixa.nome` \
6573             `&String`-borrow of the underlying field",
6574        );
6575
6576        // Aplicacao arm — same discipline on the peer typed-name-graph
6577        // kind. Constructed alongside the supervisor arm so any future
6578        // accessor drift lands on both arms in the same pin.
6579        let mut app = caixa(CaixaKind::Aplicacao);
6580        app.placement = Some(Placement {
6581            estrategia: PlacementStrategy::Replicated,
6582            clusters: vec!["rio".into()],
6583            affinity: None,
6584            shard_key: None,
6585        });
6586        app.membros = vec![Membro {
6587            caixa: "demo".into(),
6588            versao: "^0.1".into(),
6589        }];
6590        let parent_nome_via_accessor = app.nome();
6591        assert_eq!(
6592            parent_nome_via_accessor, "demo",
6593            "the caixa() fixture helper's `:nome` must be \"demo\" on \
6594             the Aplicacao arm too — same accessor-ground-truth as the \
6595             sibling supervisor arm above",
6596        );
6597        let err = layout.verify(&app, &root).unwrap_err();
6598        let LayoutError::AplicacaoViolation { caixa: c_nome, .. } = err else {
6599            panic!("expected AplicacaoViolation for self-referential membro, got {err:?}");
6600        };
6601        assert_eq!(
6602            c_nome, parent_nome_via_accessor,
6603            "the AplicacaoViolation's `caixa` field must equal \
6604             `app.nome()` — the cross-slot self-membership gate's \
6605             `parent_nome` arg must route through the lifted \
6606             [`Caixa::nome`] accessor, not the raw `&caixa.nome` \
6607             `&String`-borrow of the underlying field",
6608        );
6609
6610        // Dep-graph arm — third typed-name-graph kind on the
6611        // `parent_nome` arg boundary. Same discipline as the peer
6612        // supervision-tree and Aplicacao-membership arms above.
6613        // Constructed alongside so any future accessor drift lands on
6614        // all three arms in the same pin. Needs a distinct layout
6615        // fixture from the supervisor / aplicacao arms above because
6616        // the `Biblioteca` kind's code-path existence gate demands the
6617        // canonical `lib/<nome>.lisp` path also `exists`, so the shim
6618        // covers both `caixa.lisp` and `lib/demo.lisp`.
6619        let default_lib = root.join("lib").join("demo.lisp");
6620        let manifest_dep = manifest.clone();
6621        let default_lib_clone = default_lib.clone();
6622        let layout_dep = StandardLayout::new()
6623            .with_path_exists(move |p| p == manifest_dep || p == default_lib_clone);
6624        let mut lib = caixa(CaixaKind::Biblioteca);
6625        lib.deps = vec![Dep::simple("demo", "^0.1")];
6626        let parent_nome_via_accessor = lib.nome();
6627        assert_eq!(
6628            parent_nome_via_accessor, "demo",
6629            "the caixa() fixture helper's `:nome` must be \"demo\" on \
6630             the Biblioteca arm too — same accessor-ground-truth as the \
6631             sibling supervisor + Aplicacao arms above",
6632        );
6633        let err = layout_dep.verify(&lib, &root).unwrap_err();
6634        let LayoutError::DepsViolation { caixa: c_nome, .. } = err else {
6635            panic!("expected DepsViolation for self-referential :deps entry, got {err:?}");
6636        };
6637        assert_eq!(
6638            c_nome, parent_nome_via_accessor,
6639            "the DepsViolation's `caixa` field must equal \
6640             `lib.nome()` — the cross-slot self-dep gate's `parent_nome` \
6641             arg must route through the lifted [`Caixa::nome`] accessor, \
6642             not the raw `&caixa.nome` `&String`-borrow of the underlying \
6643             field",
6644        );
6645    }
6646
6647    #[test]
6648    fn upgrade_against_versao_gate_routes_current_versao_through_lifted_accessor() {
6649        // Composition pin: the cross-slot `:upgrade-from :from` ↔
6650        // `:versao` precedence gate fired from
6651        // `LayoutInvariants::verify` — the
6652        // `crate::upgrade::validate_upgrade_from_against_versao` call —
6653        // must key its `versao` arg off the typed [`Caixa::versao`]
6654        // accessor, not the raw `&caixa.versao` `&String`-borrow of
6655        // the underlying field.
6656        //
6657        // Same "arg-boundary reads through the lifted accessor"
6658        // discipline as the sibling
6659        // [`cross_slot_self_edge_gates_route_parent_nome_through_lifted_accessor`]
6660        // pin above on the `:nome`-arg axis of the three typed-name-
6661        // graph self-edge gates — extended here onto the `:versao`-arg
6662        // axis of the substrate's remaining `LayoutInvariants::verify`
6663        // cross-slot arg-carrying call site. Structurally byte-equal
6664        // today (the accessor is `pub fn versao(&self) -> &str { &self.versao }`,
6665        // so both paths coerce to the same `&str`); the pin catches a
6666        // future silent detour (an accessor rebrand that no longer
6667        // shipped the raw slot verbatim — a per-`:edicao` overlay,
6668        // a promotion of `:versao` to a `CaixaVersion` newtype with a
6669        // canonicalizing accessor, an M4 CR-materializer-side pinning
6670        // through a resolver-annotated `:versao-resolved` slot) that
6671        // would silently split the substrate's own precedence gate
6672        // from every peer consumer already routing `:versao` reads
6673        // through the lifted accessor.
6674        //
6675        // The gate fires an `UpgradeViolation { caixa, issue }` when a
6676        // `:upgrade-from` entry's `:from` is not strictly less than
6677        // the top-level `:versao` — the `issue` string names both the
6678        // offending prior version and the current version verbatim,
6679        // so asserting the substring `caixa.versao()` appears in the
6680        // fired diagnostic pins the accessor-routed current-versao
6681        // projection at the arg boundary. A raw-borrow bypass would
6682        // still surface the same bytes today, but the presence of
6683        // this pin makes any future divergence between the accessor's
6684        // return and the raw slot's contents a build-time failure at
6685        // this call site.
6686        use crate::{UpgradeFromEntry, UpgradeInstruction};
6687        let root = PathBuf::from("/tmp/x");
6688        let manifest = root.join("caixa.lisp");
6689        let servico_path = root.join("servicos").join("demo.computeunit.yaml");
6690        let manifest_clone = manifest.clone();
6691        let servico_clone = servico_path.clone();
6692        let layout = StandardLayout::new()
6693            .with_path_exists(move |p| p == manifest_clone || p == servico_clone);
6694        let mut svc = caixa(CaixaKind::Servico);
6695        svc.versao = "0.1.0".into();
6696        svc.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6697        // `:from` >= current `:versao` — trips the precedence gate the
6698        // `validate_upgrade_from_against_versao` cross-slot call
6699        // enforces. `:load-module` carries no on-disk path so the
6700        // path-existence gate downstream stays inert; the precedence
6701        // gate is what fires. No `:on-state-change` needed because the
6702        // instruction list carries no `:state-change` entry, so the
6703        // sibling `validate_upgrade_from_against_behavior` gate is
6704        // inert too.
6705        svc.upgrade_from = vec![UpgradeFromEntry {
6706            from: "0.2.0".into(),
6707            instructions: vec![UpgradeInstruction::LoadModule {
6708                module: "demo".into(),
6709            }],
6710        }];
6711        let current_versao_via_accessor = svc.versao().to_string();
6712        assert_eq!(
6713            current_versao_via_accessor, "0.1.0",
6714            "the mutated fixture's `:versao` must be observable through \
6715             the accessor before layout verification fires — a drift on \
6716             `Caixa::versao` would surface here as a `!= \"0.1.0\"` \
6717             inequality",
6718        );
6719        let err = layout.verify(&svc, &root).unwrap_err();
6720        let LayoutError::UpgradeViolation {
6721            caixa: c_nome,
6722            issue,
6723        } = err
6724        else {
6725            panic!("expected UpgradeViolation for :from >= :versao, got {err:?}");
6726        };
6727        assert_eq!(c_nome, svc.nome(), "wrap envelope names the caixa");
6728        assert!(
6729            issue.contains(&current_versao_via_accessor),
6730            "the UpgradeViolation's `issue` must quote the current \
6731             `:versao` byte-string verbatim — the cross-slot precedence \
6732             gate's `versao` arg must route through the lifted \
6733             [`Caixa::versao`] accessor, not the raw `&caixa.versao` \
6734             `&String`-borrow of the underlying field. issue: {issue}",
6735        );
6736    }
6737
6738    #[test]
6739    fn layout_violation_envelopes_carry_caixa_nome_through_lifted_accessor() {
6740        // Wrap-envelope drift-detection pin: every per-axis
6741        // `LayoutError::*Violation { caixa, issue }` envelope fired
6742        // from `LayoutInvariants::verify` must key its offending-caixa
6743        // field off the typed [`Caixa::nome`] accessor's
6744        // `.to_string()` extension, not the raw
6745        // `caixa.nome.clone()` `String::clone()` of the underlying
6746        // field. Structurally byte-equal today (each accessor is
6747        // `pub fn nome(&self) -> &str { &self.nome }`, so
6748        // `caixa.nome().to_string()` and `caixa.nome.clone()` produce
6749        // the same bytes); the pin catches a future silent detour
6750        // (an accessor rebrand that no longer shipped the raw slot
6751        // verbatim — a per-cluster alias table pinned through a
6752        // future `:placement`-scoped slot, the M4 CR materializer's
6753        // per-CR namespace-qualified rewrite, a `:nome-suffix`
6754        // overlay the MESH-COMPOSITION §III.2 roadmap acknowledges)
6755        // that would silently split the substrate's own layout
6756        // invariant verifier's diagnostic surface from every peer
6757        // caixa-crate consumer that already routes `:nome` reads
6758        // through the lifted accessor (the caixa-mesh 980c059,
6759        // caixa-helm 22461ef, caixa-flux 162e2e2, caixa-crd 61d3429,
6760        // caixa-feira ef83332 raw-borrow converges).
6761        //
6762        // Exercises a representative variant on each of the three
6763        // wrap-envelope arm shapes the substrate's per-axis fan-out
6764        // carries: (1) `LayoutError::NomeViolation` (the leading arm
6765        // in the `verify` order — the `:nome` axis's DNS-1123 shape
6766        // gate fires immediately after the manifest-existence gate),
6767        // (2) `LayoutError::BinarioWithoutExe` (a tuple-variant on
6768        // the kind-coherence family — different envelope shape than
6769        // the struct-variant `*Violation { caixa, issue }` family
6770        // but the same converge target on the `caixa.nome().to_string()`
6771        // arg), and (3) `LayoutError::ServicoWithoutServicos` (the
6772        // sibling tuple-variant on the same kind-coherence family).
6773        // Together they cover the two `LayoutError` envelope shapes
6774        // (struct-variant + tuple-variant) the layout invariants file
6775        // emits on `:nome`-carrying arms.
6776        //
6777        // Peer of the sibling
6778        // [`cross_slot_self_edge_gates_route_parent_nome_through_lifted_accessor`]
6779        // pin above — extends the "wrap-envelope `caixa:` field
6780        // reads through the lifted accessor" discipline from the
6781        // cross-slot self-edge gates' `parent_nome` arg boundary
6782        // onto the per-axis `LayoutError::*Violation` envelope's
6783        // `caixa:` field boundary.
6784
6785        let root = PathBuf::from("/tmp/x");
6786        let manifest = root.join("caixa.lisp");
6787        let manifest_clone = manifest.clone();
6788        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6789
6790        // (1) `NomeViolation` on the struct-variant envelope: force a
6791        // DNS-1123-invalid `:nome` (uppercase byte — `is_dns_1123_label`
6792        // rejects) and assert the fired envelope's `caixa:` field
6793        // byte-equals `c.nome().to_string()`.
6794        let mut c = caixa(CaixaKind::Biblioteca);
6795        c.nome = "BAD_NAME".into();
6796        c.bibliotecas = vec!["lib/demo.lisp".into()];
6797        let expected_nome_via_accessor = c.nome().to_string();
6798        assert_eq!(
6799            expected_nome_via_accessor, "BAD_NAME",
6800            "the mutated fixture's `:nome` must be observable through \
6801             the accessor before layout verification fires — a drift \
6802             on `Caixa::nome` would surface here as a `!= \"BAD_NAME\"` \
6803             inequality",
6804        );
6805        let err = layout.verify(&c, &root).unwrap_err();
6806        let LayoutError::NomeViolation { caixa: c_nome, .. } = err else {
6807            panic!("expected NomeViolation for DNS-1123-invalid :nome, got {err:?}");
6808        };
6809        assert_eq!(
6810            c_nome, expected_nome_via_accessor,
6811            "the NomeViolation's `caixa` field must equal \
6812             `c.nome().to_string()` — the wrap envelope's per-axis \
6813             projection must route through the lifted [`Caixa::nome`] \
6814             accessor's `.to_string()` extension, not the raw \
6815             `caixa.nome.clone()` `String::clone()` of the underlying \
6816             field",
6817        );
6818
6819        // (2) `BinarioWithoutExe` on the tuple-variant envelope: a
6820        // Binario-kind caixa with an empty `:exe` list fires the
6821        // kind-coherence gate whose payload is a bare `String`, so the
6822        // pattern is `LayoutError::BinarioWithoutExe(String)` rather
6823        // than the struct-variant `{ caixa, issue }` family. The
6824        // converge target is the same — `caixa.nome().to_string()` — but
6825        // the envelope shape is different, so the pin exercises both.
6826        let mut c = caixa(CaixaKind::Binario);
6827        // `:exe` empty is the trigger — the fixture helper defaults
6828        // it to `vec![]`, so no mutation is needed.
6829        c.nome = "binario-demo".into();
6830        let expected_nome_via_accessor = c.nome().to_string();
6831        assert_eq!(
6832            expected_nome_via_accessor, "binario-demo",
6833            "the mutated fixture's `:nome` must be observable through \
6834             the accessor before layout verification fires",
6835        );
6836        let err = layout.verify(&c, &root).unwrap_err();
6837        let LayoutError::BinarioWithoutExe(c_nome) = err else {
6838            panic!("expected BinarioWithoutExe for empty :exe list on Binario kind, got {err:?}");
6839        };
6840        assert_eq!(
6841            c_nome, expected_nome_via_accessor,
6842            "the BinarioWithoutExe's payload must equal \
6843             `c.nome().to_string()` — the tuple-variant envelope's \
6844             per-axis projection must route through the lifted \
6845             [`Caixa::nome`] accessor's `.to_string()` extension, not \
6846             the raw `caixa.nome.clone()` `String::clone()` of the \
6847             underlying field",
6848        );
6849
6850        // (3) `ServicoWithoutServicos` on the sibling tuple-variant
6851        // envelope: same discipline on the peer kind-coherence
6852        // partition arm. Constructed alongside the Binario arm so any
6853        // future accessor drift lands on both arms in the same pin.
6854        let mut c = caixa(CaixaKind::Servico);
6855        // `:servicos` empty is the trigger — the fixture helper
6856        // defaults it to `vec![]`, so no mutation is needed.
6857        c.nome = "servico-demo".into();
6858        let expected_nome_via_accessor = c.nome().to_string();
6859        assert_eq!(
6860            expected_nome_via_accessor, "servico-demo",
6861            "the mutated fixture's `:nome` must be observable through \
6862             the accessor before layout verification fires",
6863        );
6864        let err = layout.verify(&c, &root).unwrap_err();
6865        let LayoutError::ServicoWithoutServicos(c_nome) = err else {
6866            panic!(
6867                "expected ServicoWithoutServicos for empty :servicos list on Servico kind, \
6868                 got {err:?}"
6869            );
6870        };
6871        assert_eq!(
6872            c_nome, expected_nome_via_accessor,
6873            "the ServicoWithoutServicos's payload must equal \
6874             `c.nome().to_string()` — same converge discipline as the \
6875             sibling `BinarioWithoutExe` tuple-variant arm above",
6876        );
6877    }
6878
6879    #[test]
6880    fn supervisor_must_not_have_bibliotecas() {
6881        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6882        let root = PathBuf::from("/tmp/x");
6883        let manifest = root.join("caixa.lisp");
6884        let manifest_clone = manifest.clone();
6885        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6886        let mut c = caixa(CaixaKind::Supervisor);
6887        c.estrategia = Some(RestartStrategy::OneForOne);
6888        c.max_restarts = Some(5);
6889        c.bibliotecas = vec!["lib/code.lisp".into()];
6890        c.children = vec![ChildSpec {
6891            caixa: "worker".into(),
6892            versao: "^0.1".into(),
6893            restart: RestartPolicy::Permanent,
6894        }];
6895        let err = layout.verify(&c, &root).unwrap_err();
6896        assert!(matches!(err, LayoutError::SupervisorOwnsCode(_)));
6897    }
6898
6899    // ── Caixa::validate_restart_window wired into Supervisor verify ─────
6900    //
6901    // Until this wire-up landed `Caixa::validate_restart_window` lived as
6902    // `pub fn` on `Caixa` with full per-arm unit coverage in
6903    // `manifest::tests` (`validate_restart_window_rejects_*` — fractional,
6904    // decimal-shaped integer, half-unit minute, leading sign, unknown
6905    // unit, garbage, empty-after-trim) but no production path called it;
6906    // `feira build` silently accepted malformed `:restart-window` and
6907    // `Caixa::supervisor_view` soft-swallowed the parse failure as
6908    // `restart_window: None` (the canonical "no reset" sentinel), turning
6909    // every authoring footgun into a never-reset supervisor far from the
6910    // source caixa.lisp. The following pins fence the layout-pipeline
6911    // wire-up: every layout verify on a structurally-invalid `:restart-
6912    // window` axis surfaces the per-axis `RestartWindowViolation { caixa,
6913    // issue }` envelope before the typed `SupervisorSpec::validate` gate
6914    // sees the laundered `None`.
6915
6916    fn supervisor_with_window(window: Option<&str>) -> Caixa {
6917        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6918        let mut c = caixa(CaixaKind::Supervisor);
6919        c.estrategia = Some(RestartStrategy::OneForOne);
6920        c.max_restarts = Some(5);
6921        c.restart_window = window.map(str::to_string);
6922        c.children = vec![ChildSpec {
6923            caixa: "worker".into(),
6924            versao: "^0.1".into(),
6925            restart: RestartPolicy::Permanent,
6926        }];
6927        c
6928    }
6929
6930    #[test]
6931    fn restart_window_violation_on_fractional_seconds() {
6932        // `"1.5s"` is the canonical fractional-seconds drift footgun the
6933        // shared integer-magnitude codec (1c55a2a) rejects: round-trips
6934        // through `render` as `"1500ms"` on first serialize, breaking
6935        // THEORY.md §V.2.7 render-determinism. Before this wire-up
6936        // `supervisor_view` soft-swallowed the parse error as
6937        // `restart_window: None`, masking the drift as a never-reset
6938        // supervisor.
6939        let root = PathBuf::from("/tmp/x");
6940        let manifest = root.join("caixa.lisp");
6941        let manifest_clone = manifest.clone();
6942        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6943        let c = supervisor_with_window(Some("1.5s"));
6944        let err = layout.verify(&c, &root).unwrap_err();
6945        let LayoutError::RestartWindowViolation { caixa, issue } = err else {
6946            panic!("expected LayoutError::RestartWindowViolation, got {err:?}");
6947        };
6948        assert_eq!(caixa, "demo");
6949        assert!(
6950            issue.contains("1.5s"),
6951            "issue must quote the offending raw value: {issue}",
6952        );
6953    }
6954
6955    #[test]
6956    fn restart_window_violation_on_decimal_shaped_integer() {
6957        // `"1.0s"` — decimal-shaped integer the codec also rejects (a
6958        // canonical authoring form is `"1s"`). Sibling of the fractional
6959        // case; same codec arm.
6960        let root = PathBuf::from("/tmp/x");
6961        let manifest = root.join("caixa.lisp");
6962        let manifest_clone = manifest.clone();
6963        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6964        let c = supervisor_with_window(Some("1.0s"));
6965        let err = layout.verify(&c, &root).unwrap_err();
6966        assert!(
6967            matches!(
6968                err,
6969                LayoutError::RestartWindowViolation { ref caixa, ref issue }
6970                    if caixa == "demo" && issue.contains("1.0s")
6971            ),
6972            "got {err:?}",
6973        );
6974    }
6975
6976    #[test]
6977    fn restart_window_violation_on_leading_sign() {
6978        // `"+30s"` / `"-30s"` — leading-sign drift the codec rejects.
6979        // Canonical form is `"30s"`. Pin both signs separately because
6980        // a future relaxation might accept one but not the other.
6981        for raw in ["+30s", "-30s"] {
6982            let root = PathBuf::from("/tmp/x");
6983            let manifest = root.join("caixa.lisp");
6984            let manifest_clone = manifest.clone();
6985            let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6986            let c = supervisor_with_window(Some(raw));
6987            let err = layout.verify(&c, &root).unwrap_err();
6988            assert!(
6989                matches!(
6990                    err,
6991                    LayoutError::RestartWindowViolation { ref caixa, ref issue }
6992                        if caixa == "demo" && issue.contains(raw)
6993                ),
6994                "leading-sign {raw:?} got {err:?}",
6995            );
6996        }
6997    }
6998
6999    #[test]
7000    fn restart_window_violation_on_unknown_unit() {
7001        // `"30x"` — unknown duration unit. The codec admits only
7002        // `ms`/`s`/`m`/`h`.
7003        let root = PathBuf::from("/tmp/x");
7004        let manifest = root.join("caixa.lisp");
7005        let manifest_clone = manifest.clone();
7006        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7007        let c = supervisor_with_window(Some("30x"));
7008        let err = layout.verify(&c, &root).unwrap_err();
7009        assert!(
7010            matches!(
7011                err,
7012                LayoutError::RestartWindowViolation { ref caixa, .. } if caixa == "demo"
7013            ),
7014            "got {err:?}",
7015        );
7016    }
7017
7018    #[test]
7019    fn restart_window_violation_on_garbage() {
7020        // `"abc"` — pure garbage. The codec's parse fails before the
7021        // unit dispatch; the wrap envelope still surfaces the
7022        // self-locating diagnostic at the source.
7023        let root = PathBuf::from("/tmp/x");
7024        let manifest = root.join("caixa.lisp");
7025        let manifest_clone = manifest.clone();
7026        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7027        let c = supervisor_with_window(Some("abc"));
7028        let err = layout.verify(&c, &root).unwrap_err();
7029        assert!(
7030            matches!(err, LayoutError::RestartWindowViolation { ref caixa, .. } if caixa == "demo"),
7031            "got {err:?}",
7032        );
7033    }
7034
7035    #[test]
7036    fn restart_window_violation_on_empty_string() {
7037        // `""` — empty after trim. The shared codec's digit-only gate
7038        // refuses an empty magnitude. Distinguished here from the
7039        // `None` ("omit the slot") canonical authoring shape: an empty
7040        // string is an authored-but-empty slot, never the author's
7041        // intent.
7042        let root = PathBuf::from("/tmp/x");
7043        let manifest = root.join("caixa.lisp");
7044        let manifest_clone = manifest.clone();
7045        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7046        let c = supervisor_with_window(Some(""));
7047        let err = layout.verify(&c, &root).unwrap_err();
7048        assert!(
7049            matches!(err, LayoutError::RestartWindowViolation { ref caixa, .. } if caixa == "demo"),
7050            "got {err:?}",
7051        );
7052    }
7053
7054    #[test]
7055    fn verify_accepts_supervisor_without_restart_window() {
7056        // `None` is the canonical "omit the slot to express no reset"
7057        // shape — never reaches the codec, validates cleanly.
7058        let root = PathBuf::from("/tmp/x");
7059        let manifest = root.join("caixa.lisp");
7060        let manifest_clone = manifest.clone();
7061        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7062        let c = supervisor_with_window(None);
7063        layout.verify(&c, &root).unwrap();
7064    }
7065
7066    #[test]
7067    fn verify_accepts_supervisor_with_canonical_restart_window() {
7068        // Every canonical form the shared codec round-trips losslessly
7069        // must pass — `"500ms"`, `"30s"`, `"60s"`, `"1m"`, `"2m"`,
7070        // `"1h"`. Pin every form so a future tightening of the codec's
7071        // accepted set surfaces here as a test failure.
7072        for form in ["500ms", "30s", "60s", "1m", "2m", "1h"] {
7073            let root = PathBuf::from("/tmp/x");
7074            let manifest = root.join("caixa.lisp");
7075            let manifest_clone = manifest.clone();
7076            let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7077            let c = supervisor_with_window(Some(form));
7078            layout
7079                .verify(&c, &root)
7080                .unwrap_or_else(|e| panic!("canonical {form:?} must validate, got {e:?}"));
7081        }
7082    }
7083
7084    #[test]
7085    fn restart_window_violation_fires_before_supervisor_view_validate() {
7086        // Diagnostic-precedence pin: a Supervisor with a malformed
7087        // `:restart-window` AND a typed-shape defect on the typed view
7088        // (zero `:max-restarts`, which `SupervisorSpec::validate`'s
7089        // `ZeroMaxRestarts` arm rejects) surfaces the raw-string
7090        // diagnostic first — the narrower self-locating gate wins. Until
7091        // this wire-up landed `supervisor_view` would silently launder
7092        // the malformed `:restart-window` to `None` and then the typed
7093        // view's `ZeroMaxRestarts` gate would surface, masking the
7094        // raw-string footgun.
7095        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
7096        let root = PathBuf::from("/tmp/x");
7097        let manifest = root.join("caixa.lisp");
7098        let manifest_clone = manifest.clone();
7099        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7100        let mut c = caixa(CaixaKind::Supervisor);
7101        c.estrategia = Some(RestartStrategy::OneForOne);
7102        c.max_restarts = Some(0);
7103        c.restart_window = Some("1.5s".into());
7104        c.children = vec![ChildSpec {
7105            caixa: "worker".into(),
7106            versao: "^0.1".into(),
7107            restart: RestartPolicy::Permanent,
7108        }];
7109        let err = layout.verify(&c, &root).unwrap_err();
7110        assert!(
7111            matches!(err, LayoutError::RestartWindowViolation { .. }),
7112            "got {err:?} — RestartWindowViolation must fire before SupervisorViolation",
7113        );
7114    }
7115
7116    #[test]
7117    fn supervisor_slots_on_non_supervisor_fires_before_restart_window_violation() {
7118        // Order pin: a non-Supervisor caixa with a malformed
7119        // `:restart-window` surfaces `SupervisorSlotsOnNonSupervisor`
7120        // (the kind-coherence gate at the top of verify) before the
7121        // raw-string parse gate inside the Supervisor branch — because
7122        // `:restart-window` is foreign to non-Supervisor kinds, the
7123        // kind-coherence diagnostic is the load-bearing one. Mirrors
7124        // the existing `nome_violation_on_*` ordering tests that fence
7125        // the precedence between universal and kind-specific gates.
7126        let root = PathBuf::from("/tmp/x");
7127        let manifest = root.join("caixa.lisp");
7128        let default_lib = root.join("lib").join("demo.lisp");
7129        let layout =
7130            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
7131        let mut c = caixa(CaixaKind::Biblioteca);
7132        c.restart_window = Some("1.5s".into());
7133        let err = layout.verify(&c, &root).unwrap_err();
7134        assert!(
7135            matches!(err, LayoutError::SupervisorSlotsOnNonSupervisor { .. }),
7136            "got {err:?} — kind-coherence must fire before RestartWindowViolation",
7137        );
7138    }
7139
7140    #[test]
7141    fn restart_window_violation_diagnostic_carries_offending_value() {
7142        // Diagnostic-shape pin: the wrap envelope's `issue` carries the
7143        // codec's parser-shaped reason verbatim (which names the
7144        // offending raw value), so the author can grep their caixa.lisp
7145        // for `:restart-window "<value>"` and fix in one edit. Mirrors
7146        // `nome_violation_*_carries_offending_*` shape pins.
7147        let root = PathBuf::from("/tmp/x");
7148        let manifest = root.join("caixa.lisp");
7149        let manifest_clone = manifest.clone();
7150        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7151        let c = supervisor_with_window(Some("0.5m"));
7152        let err = layout.verify(&c, &root).unwrap_err();
7153        let LayoutError::RestartWindowViolation { caixa, issue } = err else {
7154            panic!("expected LayoutError::RestartWindowViolation, got {err:?}");
7155        };
7156        assert_eq!(caixa, "demo");
7157        assert!(
7158            issue.contains("0.5m"),
7159            "issue must quote the offending raw value verbatim: {issue}",
7160        );
7161        assert!(
7162            !issue.is_empty(),
7163            "issue must carry the codec's parser-shaped reason",
7164        );
7165    }
7166
7167    // ── Aplicacao layout tests ──────────────────────────────────────────
7168
7169    #[test]
7170    fn aplicacao_must_have_membros() {
7171        use crate::{Membro, Placement, PlacementStrategy};
7172        let root = PathBuf::from("/tmp/x");
7173        let manifest = root.join("caixa.lisp");
7174        let manifest_clone = manifest.clone();
7175        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7176        let mut c = caixa(CaixaKind::Aplicacao);
7177        c.placement = Some(Placement {
7178            estrategia: PlacementStrategy::Replicated,
7179            clusters: vec!["rio".into()],
7180            affinity: None,
7181            shard_key: None,
7182        });
7183        // No membros → fails
7184        let err = layout.verify(&c, &root).unwrap_err();
7185        assert!(matches!(err, LayoutError::AplicacaoViolation { .. }));
7186
7187        // With membros → passes
7188        c.membros = vec![Membro {
7189            caixa: "service-a".into(),
7190            versao: "^0.1".into(),
7191        }];
7192        layout.verify(&c, &root).unwrap();
7193    }
7194
7195    #[test]
7196    fn aplicacao_self_referential_membro_is_violation() {
7197        // An Aplicacao whose `:membros` names its own `:nome` is a
7198        // one-node lacre-closure recursion. The cross-slot gate fires
7199        // at verify time, surfacing as an AplicacaoViolation that
7200        // names the offending aplicacao — not at lacre-resolve time
7201        // far from source. The `caixa()` helper's `:nome` is "demo".
7202        // Peer of `supervisor_self_referential_child_is_violation`
7203        // on the supervision-tree axis.
7204        use crate::{Membro, Placement, PlacementStrategy};
7205        let root = PathBuf::from("/tmp/x");
7206        let manifest = root.join("caixa.lisp");
7207        let manifest_clone = manifest.clone();
7208        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7209        let mut c = caixa(CaixaKind::Aplicacao);
7210        c.placement = Some(Placement {
7211            estrategia: PlacementStrategy::Replicated,
7212            clusters: vec!["rio".into()],
7213            affinity: None,
7214            shard_key: None,
7215        });
7216        c.membros = vec![
7217            Membro {
7218                caixa: "service-a".into(),
7219                versao: "^0.1".into(),
7220            },
7221            Membro {
7222                caixa: "demo".into(),
7223                versao: "^0.1".into(),
7224            },
7225        ];
7226        let err = layout.verify(&c, &root).unwrap_err();
7227        let LayoutError::AplicacaoViolation { caixa, issue } = err else {
7228            panic!("expected AplicacaoViolation for self-referential membro, got {err:?}");
7229        };
7230        assert_eq!(caixa, "demo");
7231        assert!(
7232            issue.contains("demo") && issue.contains("lists itself"),
7233            "issue must name the self-membering aplicacao, got {issue:?}"
7234        );
7235    }
7236
7237    #[test]
7238    fn aplicacao_distinct_membros_pass_self_membership_gate() {
7239        // Positive control: an Aplicacao whose membros are all distinct
7240        // from its own `:nome` verifies cleanly.
7241        use crate::{Membro, Placement, PlacementStrategy};
7242        let root = PathBuf::from("/tmp/x");
7243        let manifest = root.join("caixa.lisp");
7244        let manifest_clone = manifest.clone();
7245        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7246        let mut c = caixa(CaixaKind::Aplicacao);
7247        c.placement = Some(Placement {
7248            estrategia: PlacementStrategy::Replicated,
7249            clusters: vec!["rio".into()],
7250            affinity: None,
7251            shard_key: None,
7252        });
7253        c.membros = vec![
7254            Membro {
7255                caixa: "service-a".into(),
7256                versao: "^0.1".into(),
7257            },
7258            Membro {
7259                caixa: "service-b".into(),
7260                versao: "^0.1".into(),
7261            },
7262        ];
7263        layout.verify(&c, &root).unwrap();
7264    }
7265
7266    #[test]
7267    fn aplicacao_self_membership_fires_after_view_validate() {
7268        // Diagnostic-precedence pin: a self-referential membro alongside
7269        // a duplicate-:caixa shape surfaces the more-fundamental
7270        // `MembroDuplicate` (from `view.validate()`) first; only when the
7271        // per-membros shape diagnostics pass does the cross-slot
7272        // self-membership gate fire. Mirrors the ordering pin
7273        // `supervisor_self_referential_child_is_violation` carries on
7274        // the peer supervision-tree axis (`view.validate()` runs first,
7275        // then the cross-slot gate). Without this ordering a future
7276        // refactor that swaps the two calls would silently mask the
7277        // narrower per-membro defect.
7278        use crate::{Membro, Placement, PlacementStrategy};
7279        let root = PathBuf::from("/tmp/x");
7280        let manifest = root.join("caixa.lisp");
7281        let manifest_clone = manifest.clone();
7282        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7283        let mut c = caixa(CaixaKind::Aplicacao);
7284        c.placement = Some(Placement {
7285            estrategia: PlacementStrategy::Replicated,
7286            clusters: vec!["rio".into()],
7287            affinity: None,
7288            shard_key: None,
7289        });
7290        c.membros = vec![
7291            Membro {
7292                caixa: "service-a".into(),
7293                versao: "^0.1".into(),
7294            },
7295            Membro {
7296                caixa: "service-a".into(),
7297                versao: "^0.2".into(),
7298            },
7299            Membro {
7300                caixa: "demo".into(),
7301                versao: "^0.1".into(),
7302            },
7303        ];
7304        let err = layout.verify(&c, &root).unwrap_err();
7305        let LayoutError::AplicacaoViolation { issue, .. } = err else {
7306            panic!("expected AplicacaoViolation, got {err:?}");
7307        };
7308        // The per-membros duplicate diagnostic (from view.validate())
7309        // surfaces ahead of the cross-slot self-membership gate, so the
7310        // `service-a` duplicate is named — not the `demo` self-reference.
7311        assert!(
7312            issue.contains("service-a") && issue.contains("more than once"),
7313            "duplicate-:caixa diagnostic must surface before self-membership gate, \
7314             got {issue:?}"
7315        );
7316    }
7317
7318    #[test]
7319    fn mesh_slots_on_servico_rejected() {
7320        // The canonical real-world footgun: an author adds :entrada to a
7321        // :kind Servico expecting it to expose ingress. aplicacao_view
7322        // returns None for Servico, so the slot is the manifest's
7323        // "ignored otherwise" — never validated, never rendered. The
7324        // kind-coherence gate rejects it at build time (before the
7325        // :servicos existence loop), naming the offending slot + kind.
7326        use crate::Entrada;
7327        let root = PathBuf::from("/tmp/x");
7328        let manifest = root.join("caixa.lisp");
7329        let manifest_clone = manifest.clone();
7330        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7331        let mut c = caixa(CaixaKind::Servico);
7332        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7333        c.entrada = Some(Entrada {
7334            host: "demo.example.com".into(),
7335            para: "demo".into(),
7336            paths: vec![],
7337            port: 8080,
7338        });
7339        let err = layout.verify(&c, &root).unwrap_err();
7340        match err {
7341            LayoutError::MeshSlotsOnNonAplicacao { caixa, kind, slots } => {
7342                assert_eq!(caixa, "demo");
7343                assert_eq!(kind, CaixaKind::Servico);
7344                assert_eq!(slots, crate::render::M3_AUTHOR_KEY_ENTRADA);
7345            }
7346            other => panic!("expected MeshSlotsOnNonAplicacao, got {other:?}"),
7347        }
7348    }
7349
7350    #[test]
7351    fn mesh_slots_on_non_aplicacao_lists_slots_in_canonical_order() {
7352        // All five mesh slots declared on a Biblioteca → the diagnostic
7353        // enumerates them in canonical declaration order, deterministic
7354        // across runs. The gate fires on declared-ness only (the values
7355        // need not be a *valid* AplicacaoSpec — aplicacao_view is never
7356        // called for a non-Aplicacao kind).
7357        use crate::{Entrada, Membro, MeshPolicy, Placement, PlacementStrategy, WitContract};
7358        let root = PathBuf::from("/tmp/x");
7359        let manifest = root.join("caixa.lisp");
7360        let manifest_clone = manifest.clone();
7361        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7362        let mut c = caixa(CaixaKind::Biblioteca);
7363        c.membros = vec![Membro {
7364            caixa: "a".into(),
7365            versao: "^0.1".into(),
7366        }];
7367        c.contratos = vec![WitContract {
7368            de: "a".into(),
7369            para: "a".into(),
7370            wit: "wasi:http/proxy".into(),
7371            endpoint: Some("/x".into()),
7372            subject: None,
7373            slot: None,
7374        }];
7375        c.politicas = Some(MeshPolicy::default());
7376        c.placement = Some(Placement {
7377            estrategia: PlacementStrategy::Replicated,
7378            clusters: vec!["rio".into()],
7379            affinity: None,
7380            shard_key: None,
7381        });
7382        c.entrada = Some(Entrada {
7383            host: "x.example.com".into(),
7384            para: "a".into(),
7385            paths: vec![],
7386            port: 8080,
7387        });
7388        let err = layout.verify(&c, &root).unwrap_err();
7389        match err {
7390            LayoutError::MeshSlotsOnNonAplicacao { slots, .. } => {
7391                assert_eq!(
7392                    slots,
7393                    format!(
7394                        "{} {} {} {} {}",
7395                        crate::render::M3_AUTHOR_KEY_MEMBROS,
7396                        crate::render::M3_AUTHOR_KEY_CONTRATOS,
7397                        crate::render::M3_AUTHOR_KEY_POLITICAS,
7398                        crate::render::M3_AUTHOR_KEY_PLACEMENT,
7399                        crate::render::M3_AUTHOR_KEY_ENTRADA,
7400                    )
7401                );
7402            }
7403            other => panic!("expected MeshSlotsOnNonAplicacao, got {other:?}"),
7404        }
7405    }
7406
7407    #[test]
7408    fn servico_without_mesh_slots_still_verifies() {
7409        // Pass-after control: a well-formed Servico carrying no mesh
7410        // slots must remain accepted — the gate keys off declared-ness,
7411        // so it must not over-fire on the common case.
7412        let root = PathBuf::from("/tmp/x");
7413        let servico = root.join("servicos/demo.computeunit.yaml");
7414        let manifest = root.join("caixa.lisp");
7415        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == servico);
7416        let mut c = caixa(CaixaKind::Servico);
7417        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7418        layout.verify(&c, &root).unwrap();
7419    }
7420
7421    #[test]
7422    fn supervisor_slots_on_servico_rejected() {
7423        // Mirror of `mesh_slots_on_servico_rejected` on the
7424        // supervisor-tree slot set: an author adds `:children` to a
7425        // `:kind Servico` expecting it to spawn workers. supervisor_view
7426        // returns None for Servico, so the slot is the manifest's
7427        // "ignored otherwise" — never validated, never reconciled. The
7428        // kind-coherence gate rejects it at build time (before the
7429        // :servicos existence loop), naming the offending slot + kind.
7430        use crate::{ChildSpec, RestartPolicy};
7431        let root = PathBuf::from("/tmp/x");
7432        let manifest = root.join("caixa.lisp");
7433        let manifest_clone = manifest.clone();
7434        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7435        let mut c = caixa(CaixaKind::Servico);
7436        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7437        c.children = vec![ChildSpec {
7438            caixa: "worker".into(),
7439            versao: "^0.1".into(),
7440            restart: RestartPolicy::Permanent,
7441        }];
7442        let err = layout.verify(&c, &root).unwrap_err();
7443        match err {
7444            LayoutError::SupervisorSlotsOnNonSupervisor { caixa, kind, slots } => {
7445                assert_eq!(caixa, "demo");
7446                assert_eq!(kind, CaixaKind::Servico);
7447                assert_eq!(slots, crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN);
7448            }
7449            other => panic!("expected SupervisorSlotsOnNonSupervisor, got {other:?}"),
7450        }
7451    }
7452
7453    #[test]
7454    fn supervisor_slots_on_non_supervisor_lists_slots_in_canonical_order() {
7455        // All four supervisor slots declared on a Biblioteca → the
7456        // diagnostic enumerates them in canonical declaration order
7457        // (`:estrategia` → `:max-restarts` → `:restart-window` →
7458        // `:children`), deterministic across runs. The gate fires on
7459        // declared-ness only (the values need not be a *valid*
7460        // SupervisorSpec — supervisor_view is never called for a
7461        // non-Supervisor kind). Mirror of
7462        // `mesh_slots_on_non_aplicacao_lists_slots_in_canonical_order`.
7463        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
7464        let root = PathBuf::from("/tmp/x");
7465        let manifest = root.join("caixa.lisp");
7466        let manifest_clone = manifest.clone();
7467        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7468        let mut c = caixa(CaixaKind::Biblioteca);
7469        c.estrategia = Some(RestartStrategy::OneForOne);
7470        c.max_restarts = Some(5);
7471        c.restart_window = Some("60s".into());
7472        c.children = vec![ChildSpec {
7473            caixa: "worker".into(),
7474            versao: "^0.1".into(),
7475            restart: RestartPolicy::Permanent,
7476        }];
7477        let err = layout.verify(&c, &root).unwrap_err();
7478        match err {
7479            LayoutError::SupervisorSlotsOnNonSupervisor { slots, .. } => {
7480                assert_eq!(slots, ":estrategia :max-restarts :restart-window :children");
7481            }
7482            other => panic!("expected SupervisorSlotsOnNonSupervisor, got {other:?}"),
7483        }
7484    }
7485
7486    #[test]
7487    fn aplicacao_with_supervisor_slots_rejected() {
7488        // Cross-kind pin: an Aplicacao (the other no-code orchestrator
7489        // kind) that declares a supervisor slot is rejected by the
7490        // supervisor-slot gate, just as a Supervisor declaring a mesh
7491        // slot is rejected by the mesh-slot gate — the two kind ↔ slot
7492        // coherence gates are symmetric and mutually exclusive. The
7493        // gate fires before the Aplicacao typed-graph validation, so
7494        // the diagnostic names the foreign supervisor slot rather than
7495        // a downstream AplicacaoViolation.
7496        use crate::{Membro, Placement, PlacementStrategy, RestartStrategy};
7497        let root = PathBuf::from("/tmp/x");
7498        let manifest = root.join("caixa.lisp");
7499        let manifest_clone = manifest.clone();
7500        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7501        let mut c = caixa(CaixaKind::Aplicacao);
7502        c.membros = vec![Membro {
7503            caixa: "service-a".into(),
7504            versao: "^0.1".into(),
7505        }];
7506        c.placement = Some(Placement {
7507            estrategia: PlacementStrategy::Replicated,
7508            clusters: vec!["rio".into()],
7509            affinity: None,
7510            shard_key: None,
7511        });
7512        c.estrategia = Some(RestartStrategy::OneForAll);
7513        let err = layout.verify(&c, &root).unwrap_err();
7514        match err {
7515            LayoutError::SupervisorSlotsOnNonSupervisor { caixa, kind, slots } => {
7516                assert_eq!(caixa, "demo");
7517                assert_eq!(kind, CaixaKind::Aplicacao);
7518                assert_eq!(slots, crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA);
7519            }
7520            other => panic!("expected SupervisorSlotsOnNonSupervisor, got {other:?}"),
7521        }
7522    }
7523
7524    #[test]
7525    fn servico_without_supervisor_slots_still_verifies() {
7526        // Pass-after control: a well-formed Servico carrying no
7527        // supervisor slots must remain accepted — the gate keys off
7528        // declared-ness, so it must not over-fire on the common case.
7529        let root = PathBuf::from("/tmp/x");
7530        let servico = root.join("servicos/demo.computeunit.yaml");
7531        let manifest = root.join("caixa.lisp");
7532        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == servico);
7533        let mut c = caixa(CaixaKind::Servico);
7534        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7535        layout.verify(&c, &root).unwrap();
7536    }
7537
7538    #[test]
7539    fn servico_slots_on_biblioteca_rejected() {
7540        // Mirror of `mesh_slots_on_servico_rejected` /
7541        // `supervisor_slots_on_servico_rejected` on the M2
7542        // Servico-runtime slot set: an author adds `:limits` to a
7543        // `:kind Biblioteca` expecting per-process sandboxing. The
7544        // caixa-helm / caixa-flux renderers gate on `require_kind(_,
7545        // Servico)`, so the slot is the manifest's "ignored otherwise" —
7546        // never rendered into any artifact. The kind-coherence gate
7547        // rejects it at build time (before the M2 validate blocks),
7548        // naming the offending slot + kind.
7549        use crate::LimitsSpec;
7550        let root = PathBuf::from("/tmp/x");
7551        let manifest = root.join("caixa.lisp");
7552        let lib = root.join("lib").join("demo.lisp");
7553        let manifest_clone = manifest.clone();
7554        let layout =
7555            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == lib);
7556        let mut c = caixa(CaixaKind::Biblioteca);
7557        c.limits = Some(LimitsSpec {
7558            fuel: Some(1_000_000),
7559            ..Default::default()
7560        });
7561        let err = layout.verify(&c, &root).unwrap_err();
7562        match err {
7563            LayoutError::ServicoSlotsOnNonServico { caixa, kind, slots } => {
7564                assert_eq!(caixa, "demo");
7565                assert_eq!(kind, CaixaKind::Biblioteca);
7566                assert_eq!(slots, crate::render::M2_AUTHOR_KEY_LIMITS);
7567            }
7568            other => panic!("expected ServicoSlotsOnNonServico, got {other:?}"),
7569        }
7570    }
7571
7572    #[test]
7573    fn servico_slots_on_non_servico_lists_slots_in_canonical_order() {
7574        // All three M2 slots declared on a Biblioteca → the diagnostic
7575        // enumerates them in canonical declaration order (`:limits` →
7576        // `:behavior` → `:upgrade-from`), deterministic across runs. The
7577        // gate fires on declared-ness only (the values need not pass the
7578        // M2 validate blocks — those run only after the kind-coherence
7579        // gate, and never for a non-Servico declared-slot caixa). Mirror
7580        // of the mesh/supervisor `*_lists_slots_in_canonical_order` pins.
7581        use crate::{BehaviorSpec, LimitsSpec, UpgradeFromEntry, UpgradeInstruction};
7582        let root = PathBuf::from("/tmp/x");
7583        let manifest = root.join("caixa.lisp");
7584        let manifest_clone = manifest.clone();
7585        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7586        let mut c = caixa(CaixaKind::Biblioteca);
7587        c.limits = Some(LimitsSpec {
7588            fuel: Some(1_000_000),
7589            ..Default::default()
7590        });
7591        c.behavior = Some(BehaviorSpec {
7592            on_init: Some(PathBuf::from("lib/init.lisp")),
7593            ..Default::default()
7594        });
7595        c.upgrade_from = vec![UpgradeFromEntry {
7596            from: "0.1.0".into(),
7597            instructions: vec![UpgradeInstruction::Restart],
7598        }];
7599        let err = layout.verify(&c, &root).unwrap_err();
7600        match err {
7601            LayoutError::ServicoSlotsOnNonServico { slots, .. } => {
7602                assert_eq!(
7603                    slots,
7604                    format!(
7605                        "{} {} {}",
7606                        crate::render::M2_AUTHOR_KEY_LIMITS,
7607                        crate::render::M2_AUTHOR_KEY_BEHAVIOR,
7608                        crate::render::M2_AUTHOR_KEY_UPGRADE_FROM,
7609                    )
7610                );
7611            }
7612            other => panic!("expected ServicoSlotsOnNonServico, got {other:?}"),
7613        }
7614    }
7615
7616    #[test]
7617    fn aplicacao_with_servico_slots_rejected() {
7618        // Cross-kind pin (mirror of `aplicacao_with_supervisor_slots_rejected`):
7619        // an Aplicacao that declares an M2 Servico-runtime slot is
7620        // rejected by the Servico-slot gate, just as a Supervisor
7621        // declaring a mesh slot is rejected by the mesh-slot gate — the
7622        // three kind ↔ slot coherence gates are symmetric and mutually
7623        // exclusive. The gate fires before the Aplicacao typed-graph
7624        // validation, so the diagnostic names the foreign M2 slot rather
7625        // than a downstream AplicacaoViolation about missing :membros.
7626        use crate::{Membro, Placement, PlacementStrategy, UpgradeFromEntry, UpgradeInstruction};
7627        let root = PathBuf::from("/tmp/x");
7628        let manifest = root.join("caixa.lisp");
7629        let manifest_clone = manifest.clone();
7630        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7631        let mut c = caixa(CaixaKind::Aplicacao);
7632        c.membros = vec![Membro {
7633            caixa: "service-a".into(),
7634            versao: "^0.1".into(),
7635        }];
7636        c.placement = Some(Placement {
7637            estrategia: PlacementStrategy::Replicated,
7638            clusters: vec!["rio".into()],
7639            affinity: None,
7640            shard_key: None,
7641        });
7642        c.upgrade_from = vec![UpgradeFromEntry {
7643            from: "0.1.0".into(),
7644            instructions: vec![UpgradeInstruction::Restart],
7645        }];
7646        let err = layout.verify(&c, &root).unwrap_err();
7647        match err {
7648            LayoutError::ServicoSlotsOnNonServico { caixa, kind, slots } => {
7649                assert_eq!(caixa, "demo");
7650                assert_eq!(kind, CaixaKind::Aplicacao);
7651                assert_eq!(slots, crate::render::M2_AUTHOR_KEY_UPGRADE_FROM);
7652            }
7653            other => panic!("expected ServicoSlotsOnNonServico, got {other:?}"),
7654        }
7655    }
7656
7657    #[test]
7658    fn servico_with_servico_slots_still_verifies() {
7659        // Pass-after control: a well-formed Servico carrying all three M2
7660        // slots must remain accepted — the gate is guarded by `kind !=
7661        // Servico`, so it must not over-fire on the kind these slots
7662        // exist for. Mirror of `servico_without_{mesh,supervisor}_slots_
7663        // still_verifies` on the legitimate-declaration axis.
7664        use crate::{BehaviorSpec, LimitsSpec, UpgradeFromEntry, UpgradeInstruction};
7665        let root = PathBuf::from("/tmp/x");
7666        let manifest = root.join("caixa.lisp");
7667        let svc = root.join("servicos/demo.computeunit.yaml");
7668        let init = root.join("lib/init.lisp");
7669        let layout =
7670            StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc || p == init);
7671        let mut c = caixa(CaixaKind::Servico);
7672        c.versao = "0.2.0".into();
7673        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7674        c.limits = Some(LimitsSpec {
7675            fuel: Some(1_000_000),
7676            ..Default::default()
7677        });
7678        c.behavior = Some(BehaviorSpec {
7679            on_init: Some(PathBuf::from("lib/init.lisp")),
7680            ..Default::default()
7681        });
7682        c.upgrade_from = vec![UpgradeFromEntry {
7683            from: "0.1.0".into(),
7684            instructions: vec![UpgradeInstruction::Restart],
7685        }];
7686        layout.verify(&c, &root).unwrap();
7687    }
7688
7689    #[test]
7690    fn aplicacao_must_not_have_bibliotecas() {
7691        use crate::{Membro, Placement, PlacementStrategy};
7692        let root = PathBuf::from("/tmp/x");
7693        let manifest = root.join("caixa.lisp");
7694        let manifest_clone = manifest.clone();
7695        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7696        let mut c = caixa(CaixaKind::Aplicacao);
7697        c.bibliotecas = vec!["lib/code.lisp".into()];
7698        c.membros = vec![Membro {
7699            caixa: "x".into(),
7700            versao: "^0.1".into(),
7701        }];
7702        c.placement = Some(Placement {
7703            estrategia: PlacementStrategy::Replicated,
7704            clusters: vec!["rio".into()],
7705            affinity: None,
7706            shard_key: None,
7707        });
7708        let err = layout.verify(&c, &root).unwrap_err();
7709        assert!(matches!(err, LayoutError::AplicacaoOwnsCode(_)));
7710    }
7711
7712    #[test]
7713    fn acao_must_not_have_bibliotecas() {
7714        // Mirror of `supervisor_must_not_have_bibliotecas` /
7715        // `aplicacao_must_not_have_bibliotecas` on the third no-code
7716        // kind. `has_code` fires before the `:ci`-presence gates below
7717        // it, so this must surface `AcaoOwnsCode` even though the
7718        // caixa also lacks a `:ci` slot (which would otherwise surface
7719        // as `MissingCi`) — the more-fundamental "this kind runs no
7720        // code at all" diagnostic wins.
7721        let root = PathBuf::from("/tmp/x");
7722        let manifest = root.join("caixa.lisp");
7723        let manifest_clone = manifest.clone();
7724        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7725        let mut c = caixa(CaixaKind::Acao);
7726        c.bibliotecas = vec!["lib/code.lisp".into()];
7727        let err = layout.verify(&c, &root).unwrap_err();
7728        assert!(matches!(err, LayoutError::AcaoOwnsCode(_)));
7729    }
7730
7731    #[test]
7732    fn acao_without_ci_errors() {
7733        // Mirror of `binario_without_exe_errors` on the fifth required-
7734        // slot axis.
7735        let root = PathBuf::from("/tmp/x");
7736        let manifest = root.join("caixa.lisp");
7737        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
7738        let err = layout.verify(&caixa(CaixaKind::Acao), &root).unwrap_err();
7739        assert!(matches!(err, LayoutError::MissingCi(_)));
7740    }
7741
7742    #[test]
7743    fn acao_with_ci_passes() {
7744        let root = PathBuf::from("/tmp/x");
7745        let manifest = root.join("caixa.lisp");
7746        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
7747        let mut c = caixa(CaixaKind::Acao);
7748        c.ci = Some(canteiro_types::CiRun {
7749            workspace: "pleme-io".into(),
7750            repo: "caixa".into(),
7751            nodes: vec![],
7752        });
7753        layout
7754            .verify(&c, &root)
7755            .expect("an Acao caixa with a declared :ci slot passes layout verify");
7756    }
7757
7758    #[test]
7759    fn acao_with_cyclic_ci_rejected_at_layout() {
7760        // Layout-side wire-up pin on the compound
7761        // [`crate::Caixa::validate_acao_shape`] gate: a `:kind Acao`
7762        // caixa declaring a structurally illegal `:ci` (here — a
7763        // minimal two-node cycle `a → b → a`, one of the three
7764        // `canteiro_types::DecomposeError` arms
7765        // [`crate::decompose_ci`] refuses) surfaces
7766        // [`LayoutError::AcaoViolation`] at `feira build` time rather
7767        // than passing the layout gate silently and deferring the
7768        // diagnostic to [`caixa_actions::validate`] at renderer time.
7769        //
7770        // Pre-lift the layout pipeline only checked `:ci` *presence*
7771        // via [`LayoutError::MissingCi`]; the decompose gate lived
7772        // only wired open-coded at
7773        // [`caixa_actions::validate`] via the substrate-canonical
7774        // [`crate::require_acao_view`] compound helper. This wire-up
7775        // pin locks the new layout-side compound-shape gate in place
7776        // — a future regression that dropped the `if
7777        // caixa.kind().is_acao() { validate_acao_shape() }` block or
7778        // relaxed the diagnostic surface trips here at caixa-core
7779        // build time. Sibling in shape to the peer
7780        // [`aplicacao_must_not_have_bibliotecas`] /
7781        // [`supervisor_must_not_have_bibliotecas`] /
7782        // [`acao_must_not_have_bibliotecas`] layout wire-up pins on
7783        // the sibling per-kind shape gates.
7784        let root = PathBuf::from("/tmp/x");
7785        let manifest = root.join("caixa.lisp");
7786        let manifest_clone = manifest.clone();
7787        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7788        let mut c = caixa(CaixaKind::Acao);
7789        c.ci = Some(canteiro_types::CiRun {
7790            workspace: "pleme-io".into(),
7791            repo: "caixa".into(),
7792            nodes: vec![
7793                canteiro_types::CiNode::new(
7794                    "a",
7795                    canteiro_types::EnvClass::None,
7796                    canteiro_types::ActionRef {
7797                        name: "a".into(),
7798                        command: "true".into(),
7799                        args: vec![],
7800                    },
7801                    vec!["b".into()],
7802                ),
7803                canteiro_types::CiNode::new(
7804                    "b",
7805                    canteiro_types::EnvClass::None,
7806                    canteiro_types::ActionRef {
7807                        name: "b".into(),
7808                        command: "true".into(),
7809                        args: vec![],
7810                    },
7811                    vec!["a".into()],
7812                ),
7813            ],
7814        });
7815        let err = layout.verify(&c, &root).unwrap_err();
7816        match err {
7817            LayoutError::AcaoViolation { caixa, issue } => {
7818                assert_eq!(caixa, "demo");
7819                assert!(
7820                    issue.contains("decompose"),
7821                    "AcaoViolation issue must name the decompose axis (got: {issue:?})",
7822                );
7823                assert!(
7824                    issue.contains("demo"),
7825                    "AcaoViolation issue must name the offending caixa nome via the folded \
7826                     CiDecomposeFailure Display (got: {issue:?})",
7827                );
7828            }
7829            other => panic!("expected AcaoViolation on a cyclic :ci, got {other:?}"),
7830        }
7831    }
7832
7833    #[test]
7834    fn ci_on_non_acao_errors() {
7835        // Mirror of `mesh_slots_on_non_aplicacao_lists_slots_in_canonical_order`
7836        // on the Acao-only `:ci` axis — declaring `:ci` on any other
7837        // kind is the same "silently ignored" footgun the sibling
7838        // mesh-/supervisor-/servico-slot gates already close.
7839        let root = PathBuf::from("/tmp/x");
7840        let manifest = root.join("caixa.lisp");
7841        let manifest_clone = manifest.clone();
7842        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7843        let mut c = caixa(CaixaKind::Biblioteca);
7844        c.ci = Some(canteiro_types::CiRun {
7845            workspace: "pleme-io".into(),
7846            repo: "caixa".into(),
7847            nodes: vec![],
7848        });
7849        let err = layout.verify(&c, &root).unwrap_err();
7850        match err {
7851            LayoutError::CiOnNonAcao { caixa, kind } => {
7852                assert_eq!(caixa, "demo");
7853                assert_eq!(kind, CaixaKind::Biblioteca);
7854            }
7855            other => panic!("expected CiOnNonAcao, got {other:?}"),
7856        }
7857    }
7858
7859    // ── ForeignCodeSlot — kind ↔ code-surface coherence ────────────────
7860
7861    #[test]
7862    fn biblioteca_with_exe_rejected() {
7863        // Fail-before-pass-after pin: a `:kind Biblioteca` declaring
7864        // `:exe` is the "I added a CLI to my library" footgun — the nix
7865        // flake renderer for Binario gates on `require_kind(_, Binario)`,
7866        // so on a Biblioteca the `:exe` path is silently dropped past
7867        // the layout's path-existence check (no executable target is
7868        // ever generated). The diagnostic names the offending kind +
7869        // slot verbatim so the author can grep their caixa.lisp for
7870        // `:exe` and fix in one edit (drop the slot or change
7871        // `:kind Biblioteca` → `:kind Binario`).
7872        let root = PathBuf::from("/tmp/x");
7873        let manifest = root.join("caixa.lisp");
7874        let lib = root.join("lib").join("demo.lisp");
7875        let exe_path = root.join("exe").join("tool");
7876        let layout = StandardLayout::new()
7877            .with_path_exists(move |p| p == manifest || p == lib || p == exe_path);
7878        let mut c = caixa(CaixaKind::Biblioteca);
7879        c.exe = vec!["exe/tool".into()];
7880        let err = layout.verify(&c, &root).unwrap_err();
7881        match err {
7882            LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
7883                assert_eq!(caixa, "demo");
7884                assert_eq!(kind, CaixaKind::Biblioteca);
7885                assert_eq!(slots, ":exe");
7886            }
7887            other => panic!("expected ForeignCodeSlot, got {other:?}"),
7888        }
7889    }
7890
7891    #[test]
7892    fn biblioteca_with_servicos_rejected() {
7893        // Symmetric to `biblioteca_with_exe_rejected` on the
7894        // `:servicos` axis: a `:kind Biblioteca` declaring a Servico
7895        // computeunit silently passed validate and the daemon's
7896        // ComputeUnit / lareira chart never materialized (caixa-helm /
7897        // caixa-flux gate emission on `require_kind(_, Servico)`).
7898        let root = PathBuf::from("/tmp/x");
7899        let manifest = root.join("caixa.lisp");
7900        let lib = root.join("lib").join("demo.lisp");
7901        let svc = root.join("servicos").join("demo.computeunit.yaml");
7902        let layout =
7903            StandardLayout::new().with_path_exists(move |p| p == manifest || p == lib || p == svc);
7904        let mut c = caixa(CaixaKind::Biblioteca);
7905        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7906        let err = layout.verify(&c, &root).unwrap_err();
7907        match err {
7908            LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
7909                assert_eq!(caixa, "demo");
7910                assert_eq!(kind, CaixaKind::Biblioteca);
7911                assert_eq!(slots, ":servicos");
7912            }
7913            other => panic!("expected ForeignCodeSlot, got {other:?}"),
7914        }
7915    }
7916
7917    #[test]
7918    fn biblioteca_with_exe_and_servicos_lists_slots_in_canonical_order() {
7919        // Both foreign code slots declared on a Biblioteca → the
7920        // diagnostic enumerates them in canonical declaration order
7921        // (`:exe` → `:servicos`), deterministic across runs. Mirrors the
7922        // mesh/supervisor/servico-slot `*_lists_slots_in_canonical_order`
7923        // pins on the peer kind ↔ slot algebra axes; drift in the
7924        // [`Caixa::declared_foreign_code_slots`] iteration order surfaces
7925        // here.
7926        let root = PathBuf::from("/tmp/x");
7927        let manifest = root.join("caixa.lisp");
7928        let manifest_clone = manifest.clone();
7929        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7930        let mut c = caixa(CaixaKind::Biblioteca);
7931        c.exe = vec!["exe/tool".into()];
7932        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7933        let err = layout.verify(&c, &root).unwrap_err();
7934        match err {
7935            LayoutError::ForeignCodeSlot { slots, .. } => {
7936                assert_eq!(slots, ":exe :servicos");
7937            }
7938            other => panic!("expected ForeignCodeSlot, got {other:?}"),
7939        }
7940    }
7941
7942    #[test]
7943    fn binario_with_servicos_rejected() {
7944        // The peer footgun on the Binario kind: declaring a Servico
7945        // computeunit on a `:kind Binario` caixa. The caixa-helm /
7946        // caixa-flux renderers gate on `require_kind(_, Servico)`, so
7947        // the `:servicos` slot vanishes past the layout's path-
7948        // existence check — no ComputeUnit, no Helm chart. `:exe` stays
7949        // valid (Binario's native code surface), so the kind-coherence
7950        // diagnostic targets only `:servicos`.
7951        let root = PathBuf::from("/tmp/x");
7952        let manifest = root.join("caixa.lisp");
7953        let exe_path = root.join("exe").join("tool");
7954        let svc = root.join("servicos").join("demo.computeunit.yaml");
7955        let layout = StandardLayout::new()
7956            .with_path_exists(move |p| p == manifest || p == exe_path || p == svc);
7957        let mut c = caixa(CaixaKind::Binario);
7958        c.exe = vec!["exe/tool".into()];
7959        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7960        let err = layout.verify(&c, &root).unwrap_err();
7961        match err {
7962            LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
7963                assert_eq!(caixa, "demo");
7964                assert_eq!(kind, CaixaKind::Binario);
7965                assert_eq!(slots, ":servicos");
7966            }
7967            other => panic!("expected ForeignCodeSlot, got {other:?}"),
7968        }
7969    }
7970
7971    #[test]
7972    fn servico_with_exe_rejected() {
7973        // Symmetric to `binario_with_servicos_rejected` on the other
7974        // code-running peer: a `:kind Servico` declaring an `:exe` is
7975        // the "I added a host-side CLI to my wasm component" footgun —
7976        // the nix flake's Binario target gates on `require_kind(_,
7977        // Binario)`, so the `:exe` path vanishes past the layout's
7978        // path-existence check.
7979        let root = PathBuf::from("/tmp/x");
7980        let manifest = root.join("caixa.lisp");
7981        let svc = root.join("servicos").join("demo.computeunit.yaml");
7982        let exe_path = root.join("exe").join("tool");
7983        let layout = StandardLayout::new()
7984            .with_path_exists(move |p| p == manifest || p == svc || p == exe_path);
7985        let mut c = caixa(CaixaKind::Servico);
7986        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7987        c.exe = vec!["exe/tool".into()];
7988        let err = layout.verify(&c, &root).unwrap_err();
7989        match err {
7990            LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
7991                assert_eq!(caixa, "demo");
7992                assert_eq!(kind, CaixaKind::Servico);
7993                assert_eq!(slots, ":exe");
7994            }
7995            other => panic!("expected ForeignCodeSlot, got {other:?}"),
7996        }
7997    }
7998
7999    #[test]
8000    fn binario_without_servicos_still_verifies() {
8001        // Pass-after control: a well-formed Binario carrying only its
8002        // native `:exe` surface must remain accepted — the gate keys off
8003        // declared-ness of the *foreign* slots, so it must not over-fire
8004        // on the legitimate same-kind case. Mirror of
8005        // `servico_with_servico_slots_still_verifies` on the peer axis.
8006        let root = PathBuf::from("/tmp/x");
8007        let manifest = root.join("caixa.lisp");
8008        let exe_path = root.join("exe").join("tool");
8009        let layout =
8010            StandardLayout::new().with_path_exists(move |p| p == manifest || p == exe_path);
8011        let mut c = caixa(CaixaKind::Binario);
8012        c.exe = vec!["exe/tool".into()];
8013        layout.verify(&c, &root).unwrap();
8014    }
8015
8016    #[test]
8017    fn biblioteca_with_only_bibliotecas_still_verifies() {
8018        // Pass-after control: a well-formed Biblioteca carrying only
8019        // its native `:bibliotecas` surface (or the default
8020        // `lib/<nome>.lisp`) must remain accepted. The gate keys off
8021        // declared-ness of `:exe` + `:servicos` only — `:bibliotecas`
8022        // is deliberately excluded from the foreign-set on every
8023        // code-running kind (`declared_foreign_code_slots` doc), so a
8024        // Biblioteca with the canonical lib surface alone passes.
8025        let root = PathBuf::from("/tmp/x");
8026        let manifest = root.join("caixa.lisp");
8027        let lib = root.join("lib").join("demo.lisp");
8028        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == lib);
8029        layout
8030            .verify(&caixa(CaixaKind::Biblioteca), &root)
8031            .expect("Biblioteca with default lib must verify");
8032    }
8033
8034    #[test]
8035    fn binario_with_bibliotecas_helper_still_verifies() {
8036        // Pass-after control on the deliberate `:bibliotecas`-as-helper
8037        // shape: a `:kind Binario` may legitimately bundle a `lib/`
8038        // helper its nix flake build consumes (the same shape a
8039        // `:kind Servico` may bundle for its wasm-component source).
8040        // The foreign-code-slot gate must NOT fire on `:bibliotecas` for
8041        // either code-running kind; pinned here so a future tightening
8042        // that adds `:bibliotecas` to the foreign set on Binario /
8043        // Servico surfaces as a test failure rather than as a silent
8044        // over-reach.
8045        let root = PathBuf::from("/tmp/x");
8046        let manifest = root.join("caixa.lisp");
8047        let exe_path = root.join("exe").join("tool");
8048        let lib = root.join("lib").join("helper.lisp");
8049        let layout = StandardLayout::new()
8050            .with_path_exists(move |p| p == manifest || p == exe_path || p == lib);
8051        let mut c = caixa(CaixaKind::Binario);
8052        c.exe = vec!["exe/tool".into()];
8053        c.bibliotecas = vec!["lib/helper.lisp".into()];
8054        layout.verify(&c, &root).unwrap();
8055    }
8056
8057    #[test]
8058    fn supervisor_with_exe_still_surfaces_owns_code() {
8059        // Diagnostic-precedence pin: a `:kind Supervisor` declaring
8060        // `:exe` is *both* "Supervisor with code" and "foreign code
8061        // slot". The more-fundamental `SupervisorOwnsCode` must win
8062        // (Supervisor doesn't run code at all — the foreign-slot
8063        // diagnostic would mislead the author toward changing `:kind`
8064        // when the underlying defect is that supervisors orchestrate
8065        // children, not code). Guards the call order in `verify`
8066        // against silent reordering.
8067        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
8068        let root = PathBuf::from("/tmp/x");
8069        let manifest = root.join("caixa.lisp");
8070        let manifest_clone = manifest.clone();
8071        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8072        let mut c = caixa(CaixaKind::Supervisor);
8073        c.estrategia = Some(RestartStrategy::OneForOne);
8074        c.max_restarts = Some(5);
8075        c.exe = vec!["exe/tool".into()];
8076        c.children = vec![ChildSpec {
8077            caixa: "worker".into(),
8078            versao: "^0.1".into(),
8079            restart: RestartPolicy::Permanent,
8080        }];
8081        let err = layout.verify(&c, &root).unwrap_err();
8082        assert!(
8083            matches!(err, LayoutError::SupervisorOwnsCode(_)),
8084            "Supervisor-with-:exe must surface as SupervisorOwnsCode (the more-fundamental \
8085             no-code-at-all diagnostic), got {err:?}"
8086        );
8087    }
8088
8089    #[test]
8090    fn declared_foreign_code_slots_returns_canonical_order() {
8091        // Unit-level pin for the lifted method: the canonical iteration
8092        // order is `:exe` → `:servicos`, independent of which subset is
8093        // populated. Empty input + each single-slot subset + the full
8094        // pair are all checked so a future axis added to the method
8095        // (a hypothetical fifth code-surface slot) is one extension
8096        // point + one assertion update here, not a coordinated rewrite
8097        // across the layout-test sites that reach for the canonical
8098        // order.
8099        let mut c = caixa(CaixaKind::Biblioteca);
8100        assert!(c.declared_foreign_code_slots().is_empty());
8101        c.exe = vec!["exe/tool".into()];
8102        assert_eq!(c.declared_foreign_code_slots(), vec![":exe"]);
8103        c.exe.clear();
8104        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8105        assert_eq!(c.declared_foreign_code_slots(), vec![":servicos"]);
8106        c.exe = vec!["exe/tool".into()];
8107        assert_eq!(c.declared_foreign_code_slots(), vec![":exe", ":servicos"]);
8108    }
8109
8110    #[test]
8111    fn aplicacao_with_unknown_contrato_member_fails() {
8112        use crate::{Membro, Placement, PlacementStrategy, WitContract};
8113        let root = PathBuf::from("/tmp/x");
8114        let manifest = root.join("caixa.lisp");
8115        let manifest_clone = manifest.clone();
8116        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8117        let mut c = caixa(CaixaKind::Aplicacao);
8118        c.membros = vec![Membro {
8119            caixa: "service-a".into(),
8120            versao: "^0.1".into(),
8121        }];
8122        c.contratos = vec![WitContract {
8123            de: "service-a".into(),
8124            para: "phantom".into(),
8125            wit: "wasi:http/proxy".into(),
8126            endpoint: Some("/x".into()),
8127            subject: None,
8128            slot: None,
8129        }];
8130        c.placement = Some(Placement {
8131            estrategia: PlacementStrategy::Replicated,
8132            clusters: vec!["rio".into()],
8133            affinity: None,
8134            shard_key: None,
8135        });
8136        let err = layout.verify(&c, &root).unwrap_err();
8137        assert!(matches!(err, LayoutError::AplicacaoViolation { .. }));
8138    }
8139
8140    #[test]
8141    fn limits_zero_axis_surfaces_as_layout_violation() {
8142        use crate::LimitsSpec;
8143        let root = PathBuf::from("/tmp/x");
8144        let manifest = root.join("caixa.lisp");
8145        let svc = root.join("servicos/demo.computeunit.yaml");
8146        let mut c = caixa(CaixaKind::Servico);
8147        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8148        c.limits = Some(LimitsSpec {
8149            fuel: Some(0),
8150            ..Default::default()
8151        });
8152        let manifest_clone = manifest.clone();
8153        let svc_clone = svc.clone();
8154        let layout =
8155            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8156        let err = layout.verify(&c, &root).unwrap_err();
8157        let LayoutError::LimitsViolation { caixa, issue } = err else {
8158            panic!("expected LimitsViolation, got {err:?}");
8159        };
8160        assert_eq!(caixa, "demo");
8161        assert!(issue.contains(":fuel"), "issue must name the axis: {issue}");
8162    }
8163
8164    #[test]
8165    fn limits_well_formed_passes_layout() {
8166        use crate::LimitsSpec;
8167        use std::time::Duration;
8168        let root = PathBuf::from("/tmp/x");
8169        let manifest = root.join("caixa.lisp");
8170        let svc = root.join("servicos/demo.computeunit.yaml");
8171        let mut c = caixa(CaixaKind::Servico);
8172        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8173        c.limits = Some(LimitsSpec {
8174            memory: Some(64 * 1024 * 1024),
8175            fuel: Some(1_000_000),
8176            wall_clock: Some(Duration::from_secs(30)),
8177            cpu: Some(500),
8178        });
8179        let manifest_clone = manifest.clone();
8180        let svc_clone = svc.clone();
8181        let layout =
8182            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8183        layout.verify(&c, &root).unwrap();
8184    }
8185
8186    #[test]
8187    fn supervisor_with_valid_children_passes() {
8188        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
8189        let root = PathBuf::from("/tmp/x");
8190        let manifest = root.join("caixa.lisp");
8191        let manifest_clone = manifest.clone();
8192        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8193        let mut c = caixa(CaixaKind::Supervisor);
8194        c.estrategia = Some(RestartStrategy::OneForOne);
8195        c.max_restarts = Some(5);
8196        c.children = vec![
8197            ChildSpec {
8198                caixa: "worker".into(),
8199                versao: "^0.1".into(),
8200                restart: RestartPolicy::Permanent,
8201            },
8202            ChildSpec {
8203                caixa: "cache".into(),
8204                versao: "^0.1".into(),
8205                restart: RestartPolicy::Transient,
8206            },
8207        ];
8208        layout.verify(&c, &root).unwrap();
8209    }
8210
8211    // ── :upgrade-from entry validation pipes through layout ─────────────
8212
8213    #[test]
8214    fn upgrade_invalid_module_surfaces_as_layout_violation() {
8215        // End-to-end pin that
8216        // [`crate::UpgradeFromEntry::validate`] runs *inside*
8217        // `LayoutInvariants::verify` and surfaces value-shape
8218        // violations through the new `UpgradeViolation` arm
8219        // (parallel to `BehaviorViolation`, `LimitsViolation`,
8220        // `SupervisorViolation`, `AplicacaoViolation`). Until this
8221        // wiring landed the entry validator was unreachable from any
8222        // build-pipeline caller — an `:upgrade-from
8223        // ((:from "0.1.0" :instructions ((:load-module "Hello")))` (uppercase
8224        // module name the K8s apiserver would reject on the per-
8225        // ComputeUnit `metadata.name` axis) silently passed
8226        // `feira lint` / `feira build` and surfaced only at wasm-engine
8227        // hot-upgrade time as a per-backend "module not found" /
8228        // `code:load_module/1` `badarg` runtime error, far from the
8229        // source caixa.lisp. Pinning the wiring here so a future
8230        // refactor that drops the `entry.validate()` call surfaces as
8231        // a build-pipeline regression at this test, not as a runtime
8232        // surprise per consumer.
8233        use crate::{UpgradeFromEntry, UpgradeInstruction};
8234        use std::path::PathBuf;
8235        let root = PathBuf::from("/tmp/x");
8236        let manifest = root.join("caixa.lisp");
8237        let svc = root.join("servicos/demo.computeunit.yaml");
8238        let mut c = caixa(CaixaKind::Servico);
8239        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8240        c.upgrade_from = vec![UpgradeFromEntry {
8241            from: "0.1.0".into(),
8242            instructions: vec![UpgradeInstruction::LoadModule {
8243                module: "Hello".into(), // uppercase — not DNS-1123
8244            }],
8245        }];
8246        let manifest_clone = manifest.clone();
8247        let svc_clone = svc.clone();
8248        let layout =
8249            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8250        let err = layout.verify(&c, &root).unwrap_err();
8251        let LayoutError::UpgradeViolation { caixa, issue } = err else {
8252            panic!("expected UpgradeViolation, got {err:?}");
8253        };
8254        assert_eq!(caixa, "demo");
8255        assert!(
8256            issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_LOAD_MODULE),
8257            "issue must name the lisp-form of the offending instruction: {issue}"
8258        );
8259        assert!(
8260            issue.contains("Hello"),
8261            "issue must name the offending :module verbatim: {issue}"
8262        );
8263    }
8264
8265    #[test]
8266    fn upgrade_empty_module_surfaces_as_layout_violation() {
8267        // Companion to the DNS-1123 footgun above on the narrower
8268        // empty arm. Every Module-bearing variant's empty value
8269        // reaches the layout pipeline through the kind-tagged
8270        // `ModuleEmpty` diagnostic naming its lisp-form.
8271        use crate::{UpgradeFromEntry, UpgradeInstruction};
8272        use std::path::PathBuf;
8273        let root = PathBuf::from("/tmp/x");
8274        let manifest = root.join("caixa.lisp");
8275        let svc = root.join("servicos/demo.computeunit.yaml");
8276        let mut c = caixa(CaixaKind::Servico);
8277        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8278        c.upgrade_from = vec![UpgradeFromEntry {
8279            from: "0.1.0".into(),
8280            instructions: vec![UpgradeInstruction::SoftPurge {
8281                module: String::new(),
8282            }],
8283        }];
8284        let manifest_clone = manifest.clone();
8285        let svc_clone = svc.clone();
8286        let layout =
8287            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8288        let err = layout.verify(&c, &root).unwrap_err();
8289        let LayoutError::UpgradeViolation { caixa, issue } = err else {
8290            panic!("expected UpgradeViolation, got {err:?}");
8291        };
8292        assert_eq!(caixa, "demo");
8293        assert!(
8294            issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_SOFT_PURGE),
8295            "issue must name the lisp-form of the empty instruction: {issue}"
8296        );
8297    }
8298
8299    #[test]
8300    fn upgrade_invalid_state_change_script_surfaces_as_layout_violation() {
8301        // Pins that the b0c8389 script value-shape gates
8302        // (AbsoluteScript / ParentEscapeScript) — previously
8303        // unreachable from any build-pipeline caller — now fire
8304        // through the same `UpgradeViolation` arm before the path-
8305        // existence pass would otherwise emit the less-helpful
8306        // "missing upgrade-script" (or, worse, *succeed* against
8307        // /etc/passwd, proving the sandbox bypass — same defect
8308        // the b0c8389 BehaviorSpec wiring closed on the peer M2
8309        // slot).
8310        use crate::{UpgradeFromEntry, UpgradeInstruction};
8311        use std::path::PathBuf;
8312        let root = PathBuf::from("/tmp/x");
8313        let manifest = root.join("caixa.lisp");
8314        let svc = root.join("servicos/demo.computeunit.yaml");
8315        let etc_passwd = PathBuf::from("/etc/passwd");
8316        let mut c = caixa(CaixaKind::Servico);
8317        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8318        c.upgrade_from = vec![UpgradeFromEntry {
8319            from: "0.1.0".into(),
8320            instructions: vec![UpgradeInstruction::StateChange {
8321                script: PathBuf::from("/etc/passwd"),
8322            }],
8323        }];
8324        let manifest_clone = manifest.clone();
8325        let svc_clone = svc.clone();
8326        let etc_passwd_clone = etc_passwd.clone();
8327        // Critically: /etc/passwd "exists" in our mock — without the
8328        // value-shape pre-check, the existence loop would *succeed*
8329        // and the path-traversal exit from the project sandbox would
8330        // pass `feira build` silently.
8331        let layout = StandardLayout::new().with_path_exists(move |p| {
8332            p == manifest_clone || p == svc_clone || p == etc_passwd_clone
8333        });
8334        let err = layout.verify(&c, &root).unwrap_err();
8335        let LayoutError::UpgradeViolation { caixa, issue } = err else {
8336            panic!("expected UpgradeViolation, got {err:?}");
8337        };
8338        assert_eq!(caixa, "demo");
8339        assert!(
8340            issue.contains("absolute") || issue.contains("Absolute"),
8341            "issue must name the violation kind (absolute): {issue}"
8342        );
8343    }
8344
8345    #[test]
8346    fn upgrade_well_formed_passes_layout() {
8347        // Positive control — every documented authoring shape
8348        // (`:load-module`, `:state-change` with a relative path,
8349        // `:soft-purge`, `:purge`, sole `:restart`) passes the wired
8350        // gate. The typed sequence (`:load-module` → `:state-change`
8351        // → `:soft-purge` → `:purge`) lives in one entry; the sole
8352        // `:restart` fallback lives in a *separate* entry on a
8353        // different `:from` (the within-entry restart-exclusivity
8354        // gate added in this commit rejects mixing the fallback with
8355        // the typed sequence — per the UpgradeInstruction::Restart
8356        // doc, `:restart` is terminal and any other instructions in
8357        // the same entry are dead code). Drift here = a future
8358        // tighten that rejects any canonical shape surfaces as a
8359        // regression at this layout-level pin, not piecemeal across
8360        // per-renderer call sites.
8361        //
8362        // `:soft-purge` and `:purge` target *distinct* old-version
8363        // modules (`hello-rio-old` and `hello-rio-oldest`) so the
8364        // within-entry cleanup-singularity gate
8365        // (`UpgradeError::DuplicateCleanup`) passes — that gate
8366        // rejects more than one cleanup per module per entry (one
8367        // semantic per old version; mixing drain + discard on one
8368        // module is the soft-then-hard fallback footgun the author
8369        // shouldn't write because the operator handles cleanup
8370        // failure escalation itself). The two distinct names cover
8371        // the legitimate "drain a recent old, hard-discard an
8372        // older-still" shape — both authoring forms remain load-
8373        // bearing in this positive-control enumeration.
8374        use crate::{BehaviorSpec, UpgradeFromEntry, UpgradeInstruction};
8375        use std::path::PathBuf;
8376        let root = PathBuf::from("/tmp/x");
8377        let manifest = root.join("caixa.lisp");
8378        let svc = root.join("servicos/demo.computeunit.yaml");
8379        let migration = root.join("lib/migrations/v01-to-v02.lisp");
8380        let on_state_change = root.join("lib/migrations.lisp");
8381        let mut c = caixa(CaixaKind::Servico);
8382        // `:versao` past both entries' `:from` so the cross-slot
8383        // precedence gate (`FromNotBeforeVersao`) lets this canonical
8384        // authoring shape through to the positive-control assertion.
8385        c.versao = "0.2.0".into();
8386        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8387        // `:on-state-change` declared alongside the `(:state-change …)`
8388        // instruction below — the cross-slot composition gate
8389        // (`validate_upgrade_from_against_behavior`) rejects a
8390        // `:state-change` without the callback, so the canonical
8391        // authoring shape this positive control pins now includes the
8392        // runtime delivery hook (the `gen_server:code_change/3` analog
8393        // that the per-version script is invoked through during hot
8394        // upgrade per the upgrade.rs module doc "Composes with"
8395        // promise).
8396        c.behavior = Some(BehaviorSpec {
8397            on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
8398            ..Default::default()
8399        });
8400        c.upgrade_from = vec![
8401            UpgradeFromEntry {
8402                from: "0.1.0".into(),
8403                instructions: vec![
8404                    UpgradeInstruction::LoadModule {
8405                        module: "hello-rio".into(),
8406                    },
8407                    UpgradeInstruction::StateChange {
8408                        script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
8409                    },
8410                    UpgradeInstruction::SoftPurge {
8411                        module: "hello-rio-old".into(),
8412                    },
8413                    UpgradeInstruction::Purge {
8414                        module: "hello-rio-oldest".into(),
8415                    },
8416                ],
8417            },
8418            UpgradeFromEntry {
8419                from: "0.0.9".into(),
8420                instructions: vec![UpgradeInstruction::Restart],
8421            },
8422        ];
8423        let manifest_clone = manifest.clone();
8424        let svc_clone = svc.clone();
8425        let migration_clone = migration.clone();
8426        let on_state_change_clone = on_state_change.clone();
8427        let layout = StandardLayout::new().with_path_exists(move |p| {
8428            p == manifest_clone
8429                || p == svc_clone
8430                || p == migration_clone
8431                || p == on_state_change_clone
8432        });
8433        layout.verify(&c, &root).unwrap();
8434    }
8435
8436    #[test]
8437    fn upgrade_from_restart_mixed_surfaces_as_upgrade_violation() {
8438        // Wiring pin: the within-entry `(:restart)`-exclusivity gate
8439        // (`UpgradeFromEntry::validate_restart_exclusive`) lands on
8440        // the same `LayoutError::UpgradeViolation` axis the per-entry
8441        // shape gate (26da2c7), the cross-entry duplicate-`:from`
8442        // gate (7c6aef2), and the cross-slot `:from < :versao`
8443        // precedence gate (de7ab1a) already do. A caixa.lisp whose
8444        // `:upgrade-from` entry mixes `(:restart)` with a typed
8445        // instruction surfaces at `feira build` time naming the
8446        // offending caixa + the entry's `:from` rather than silently
8447        // passing into the wasm-operator with semantically dead code
8448        // in the operator's dispatch table. Mirrors
8449        // `upgrade_from_duplicate_surfaces_as_upgrade_violation` on
8450        // the peer cross-entry gate.
8451        use crate::{UpgradeFromEntry, UpgradeInstruction};
8452        let root = PathBuf::from("/tmp/x");
8453        let manifest = root.join("caixa.lisp");
8454        let svc = root.join("servicos/demo.computeunit.yaml");
8455        let mut c = caixa(CaixaKind::Servico);
8456        c.versao = "0.2.0".into();
8457        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8458        c.upgrade_from = vec![UpgradeFromEntry {
8459            from: "0.1.0".into(),
8460            instructions: vec![
8461                UpgradeInstruction::LoadModule {
8462                    module: "hello-rio".into(),
8463                },
8464                UpgradeInstruction::Restart,
8465            ],
8466        }];
8467        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
8468        let err = layout.verify(&c, &root).unwrap_err();
8469        let LayoutError::UpgradeViolation { caixa, issue } = err else {
8470            panic!("expected LayoutError::UpgradeViolation for restart-mixed entry, got {err:?}");
8471        };
8472        assert_eq!(caixa, "demo");
8473        assert!(
8474            issue.contains("0.1.0"),
8475            "UpgradeViolation issue must name the offending entry's `:from` verbatim, got \
8476             {issue:?}"
8477        );
8478        assert!(
8479            issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_RESTART),
8480            "UpgradeViolation issue must name the `:restart` axis verbatim, got {issue:?}"
8481        );
8482        assert!(
8483            issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_LOAD_MODULE),
8484            "UpgradeViolation issue must name the non-:restart peer instruction's lisp-form \
8485             verbatim, got {issue:?}"
8486        );
8487    }
8488
8489    #[test]
8490    fn upgrade_from_restart_duplicated_surfaces_as_upgrade_violation() {
8491        // Companion arm: the duplicate-`(:restart)` mode of
8492        // `RestartNotExclusive` (no typed peers, just multiple
8493        // `Restart` variants) surfaces through the same wiring as the
8494        // mixed-with-typed mode above. The diagnostic still names the
8495        // offending entry's `:from` verbatim even when `other_kinds`
8496        // is empty.
8497        use crate::{UpgradeFromEntry, UpgradeInstruction};
8498        let root = PathBuf::from("/tmp/x");
8499        let manifest = root.join("caixa.lisp");
8500        let svc = root.join("servicos/demo.computeunit.yaml");
8501        let mut c = caixa(CaixaKind::Servico);
8502        c.versao = "0.2.0".into();
8503        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8504        c.upgrade_from = vec![UpgradeFromEntry {
8505            from: "0.1.0".into(),
8506            instructions: vec![UpgradeInstruction::Restart, UpgradeInstruction::Restart],
8507        }];
8508        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
8509        let err = layout.verify(&c, &root).unwrap_err();
8510        let LayoutError::UpgradeViolation { caixa, issue } = err else {
8511            panic!(
8512                "expected LayoutError::UpgradeViolation for duplicate-restart entry, got \
8513                 {err:?}"
8514            );
8515        };
8516        assert_eq!(caixa, "demo");
8517        assert!(
8518            issue.contains("0.1.0"),
8519            "UpgradeViolation issue must name the offending entry's `:from` verbatim, got \
8520             {issue:?}"
8521        );
8522        assert!(
8523            issue.contains("(:restart)")
8524                || issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_RESTART),
8525            "UpgradeViolation issue must name the `:restart` axis verbatim, got {issue:?}"
8526        );
8527    }
8528
8529    #[test]
8530    fn upgrade_from_invalid_surfaces_as_layout_violation() {
8531        // The `:from` semver gate (`UpgradeError::FromInvalid`)
8532        // was likewise unreachable before this wiring landed — a
8533        // typo-shaped `:from "v0.1.0"` (git-tag-shape leaking into
8534        // the semver slot) silently passed `feira build` and
8535        // surfaced only when the operator's hot-upgrade decision
8536        // engine tried to match against the version key it couldn't
8537        // parse. Now wired through `UpgradeViolation` with the
8538        // peer-shaped `{ from, reason }` payload — the
8539        // parser-shaped `reason` flows through `Display` so the
8540        // wrapped issue string carries both the offending value
8541        // *and* the SemVer-2 parser's wording (peer with the
8542        // `VersaoInvalid` / `MembroVersaoInvalid` envelopes on the
8543        // sibling SemVer-2 axes).
8544        use crate::{UpgradeFromEntry, UpgradeInstruction};
8545        use std::path::PathBuf;
8546        let root = PathBuf::from("/tmp/x");
8547        let manifest = root.join("caixa.lisp");
8548        let svc = root.join("servicos/demo.computeunit.yaml");
8549        let mut c = caixa(CaixaKind::Servico);
8550        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8551        c.upgrade_from = vec![UpgradeFromEntry {
8552            from: "v0.1.0".into(), // git-tag-shape, not semver
8553            instructions: vec![UpgradeInstruction::Restart],
8554        }];
8555        let manifest_clone = manifest.clone();
8556        let svc_clone = svc.clone();
8557        let layout =
8558            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8559        let err = layout.verify(&c, &root).unwrap_err();
8560        let LayoutError::UpgradeViolation { caixa, issue } = err else {
8561            panic!("expected UpgradeViolation, got {err:?}");
8562        };
8563        assert_eq!(caixa, "demo");
8564        assert!(
8565            issue.contains("v0.1.0"),
8566            "UpgradeViolation issue must name the offending :from value verbatim, got {issue:?}"
8567        );
8568        assert!(
8569            issue.contains(":from"),
8570            "UpgradeViolation issue must name the :from slot verbatim, got {issue:?}"
8571        );
8572        // Pin the parser-shaped reason flow-through: the renamed
8573        // `FromInvalid { from, reason }` carries the SemVer-2 parser's
8574        // wording verbatim, and the [`UpgradeError`] Display routes it
8575        // into the wrapped `issue` string so the layout envelope
8576        // surfaces both the offending value *and* the parser's
8577        // diagnosis. Mirrors the peer flow-through on
8578        // `ManifestError::VersaoInvalid` (top-level `:versao`) and
8579        // `AplicacaoError::MembroVersaoInvalid` (`:membros :versao`).
8580        assert!(
8581            issue.contains("SemVer-2"),
8582            "UpgradeViolation issue must carry the parser-shaped reason (\"SemVer-2\"), got {issue:?}"
8583        );
8584    }
8585
8586    #[test]
8587    fn missing_lib_gate_routes_through_kind_requires_lib_and_caixa_nome() {
8588        // Fail-before-pass-after pin on the two-part converge landed
8589        // at layout.rs:844-847:
8590        //   (a) `caixa.kind().is_biblioteca()` →
8591        //       `caixa.kind().requires_lib()` — routes the biblioteca
8592        //       required-slot gate onto the same `requires_*()`
8593        //       predicate family the three sibling required-slot
8594        //       gates (`requires_exe()` at :856, `requires_servicos()`
8595        //       at :860, `requires_ci()` at :874) already key off.
8596        //       All four gates in the block now share one convention;
8597        //       a future kind that gains its own required-slot gate
8598        //       (an M4/M5 typed arm the CAIXA-SDLC §I six-kind roster
8599        //       may grow) reaches for the same predicate family and
8600        //       inherits the accessor discipline for free.
8601        //   (b) raw `caixa.nome` → `caixa.nome()` — routes the
8602        //       `expected` path composition through the typed
8603        //       [`crate::Caixa::nome`] accessor, closing the last
8604        //       unlifted raw `caixa.nome` production field-access
8605        //       site in `caixa-core/src/layout.rs` (every peer
8606        //       diagnostic in the file already routes through
8607        //       `caixa.nome().to_string()`).
8608        //
8609        // The behavioral pin: for a Biblioteca kind with no fallback
8610        // `lib/<nome>.lisp` file, MissingLib fires and its `expected`
8611        // path composes through `Caixa::nome()`; for every other
8612        // kind, MissingLib does NOT fire (the gate short-circuits on
8613        // kinds where `requires_lib()` returns false), even when the
8614        // fallback file is likewise absent. A future regression that
8615        // reroutes the gate off `requires_lib()` (e.g. onto
8616        // `is_biblioteca()` again, or onto a hand-authored
8617        // `matches!(caixa.kind(), CaixaKind::Biblioteca)`) that
8618        // *happens* to agree byte-for-byte on today's arm-set trips
8619        // this test the moment a future kind's `requires_lib()`
8620        // returns true for a non-`Biblioteca` arm (or the sibling
8621        // required-slot gates diverge from the same convention).
8622        let root = PathBuf::from("/tmp/x");
8623        let manifest = root.join("caixa.lisp");
8624        let manifest_only = manifest.clone();
8625        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_only);
8626
8627        // Biblioteca kind + no lib fallback → MissingLib fires with
8628        // the expected path composed through `Caixa::nome()`.
8629        let bib = caixa(CaixaKind::Biblioteca);
8630        assert!(
8631            bib.kind().requires_lib(),
8632            "requires_lib() must return true for Biblioteca — the four-required-\
8633             slot-gate family's routing depends on this arm's assignment"
8634        );
8635        let err = layout.verify(&bib, &root).unwrap_err();
8636        let LayoutError::MissingLib {
8637            caixa: cname,
8638            expected,
8639        } = err
8640        else {
8641            panic!("expected MissingLib for Biblioteca kind with no lib fallback, got {err:?}");
8642        };
8643        assert_eq!(
8644            cname,
8645            bib.nome(),
8646            "MissingLib `caixa:` carrier must byte-equal Caixa::nome()"
8647        );
8648        assert_eq!(
8649            expected,
8650            root.join(crate::render::LAYOUT_DIR_LIB)
8651                .join(format!("{}.lisp", bib.nome())),
8652            "MissingLib `expected:` path must compose through Caixa::nome() \
8653             verbatim — a raw-field-access regression would silently drift \
8654             the composed path on any future `:nome` axis extension \
8655             (namespace-qualified rewrite, per-cluster alias overlay)"
8656        );
8657
8658        // Non-Biblioteca kinds → the MissingLib gate short-circuits.
8659        // Different kinds fail on their own required-slot gate
8660        // (BinarioWithoutExe, ServicoWithoutServicos, MissingCi) or
8661        // on downstream M2/M3 invariants; none of them may surface as
8662        // MissingLib, because `requires_lib()` returns false for each.
8663        for kind in [
8664            CaixaKind::Binario,
8665            CaixaKind::Servico,
8666            CaixaKind::Supervisor,
8667            CaixaKind::Aplicacao,
8668            CaixaKind::Acao,
8669        ] {
8670            assert!(
8671                !kind.requires_lib(),
8672                "requires_lib() must return false for {kind:?} — the \
8673                 four-required-slot-gate family's arm assignment pins \
8674                 exactly one kind (Biblioteca) as the arm that requires \
8675                 a `lib/` entry"
8676            );
8677            let c = caixa(kind);
8678            let result = layout.verify(&c, &root);
8679            assert!(
8680                !matches!(result, Err(LayoutError::MissingLib { .. })),
8681                "MissingLib gate at layout.rs:844 must short-circuit for \
8682                 kinds where requires_lib() returns false; unexpectedly \
8683                 fired for {kind:?}: {result:?}"
8684            );
8685        }
8686    }
8687}