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::missing_manifest(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::missing_lib(caixa, expected));
1011            }
1012        }
1013
1014        // Required-slot gate on the three [`CaixaKind`] arms whose
1015        // sole payload is a canonical typed slot — folded onto one
1016        // substrate primitive at
1017        // [`crate::Caixa::validate_required_kind_slot`]. Pre-lift each
1018        // of the three arms lived as a self-similar
1019        // `if caixa.kind().requires_<slot>() && caixa.<slot>().is_<empty>() {
1020        // return Err(LayoutError::<kind>_without_<slot>(caixa)); }` block
1021        // at this call site — three consumers, three identical shapes,
1022        // one substrate primitive on [`Caixa`] closing the duplication
1023        // the PRIME DIRECTIVE names as a bug. Diagnostic order at the
1024        // primitive matches the pre-fold canonical sequence — `:exe` →
1025        // `:servicos` → `:ci` — the same three-arm sweep the peer
1026        // [`CaixaKind`] discriminator carries at its `requires_*`
1027        // accessors; the three arms are mutually exclusive by
1028        // construction (`:kind` is a single-valued discriminator so at
1029        // most one arm can fire per caixa) so no cross-arm ordering
1030        // pin is meaningful.
1031        //
1032        // The paired `Biblioteca`-arm required-slot check
1033        // ([`LayoutError::MissingLib`], immediately above) stays
1034        // open-coded at this altitude by design: it needs the
1035        // [`LayoutInvariants::exists`] filesystem oracle to check the
1036        // default `lib/<nome>.lisp` fallback path, which the pure
1037        // per-`Caixa` typed-shape surface the fold rides on has no
1038        // reference to. Same posture the peer
1039        // [`crate::Caixa::validate_no_code_kind_coherence`] fold
1040        // (3bbf6a2) takes on the on-disk existence loops.
1041        //
1042        // Peer with the per-slot and per-kind compound entry gates
1043        // every substrate primitive on the M2/M3 typed-slot family
1044        // already carries ([`crate::Caixa::validate_deps`] b5dd55e,
1045        // [`crate::Caixa::validate_limits`] baa4688,
1046        // [`crate::Caixa::validate_behavior`] 0d2877a,
1047        // [`crate::Caixa::validate_upgrade_from`] d6801df,
1048        // [`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
1049        // [`crate::Caixa::validate_supervisor_shape`] 4c70105,
1050        // [`crate::Caixa::validate_acao_shape`] 5d6df54,
1051        // [`crate::Caixa::validate_kind_slot_coherence`] f0d286e,
1052        // [`crate::Caixa::validate_no_code_kind_coherence`] 3bbf6a2,
1053        // [`crate::Caixa::validate_ci_kind_coherence`] 9b55beb): the
1054        // layout pipeline routes the three self-similar required-slot
1055        // gates through one substrate primitive rather than three
1056        // open-coded blocks, and every future required-slot arm (a
1057        // per-Aplicacao required-`:membros` gate, a per-Supervisor
1058        // required-`:children` gate — both already carried at
1059        // [`CaixaKind::requires_membros`] / [`CaixaKind::requires_children`]
1060        // without a paired layout-side wire-up) folds onto this
1061        // compound gate as one arm addition rather than a fourth
1062        // open-coded block at the wire-up site.
1063        caixa.validate_required_kind_slot()?;
1064
1065        // On-disk existence probe on the three Caixa-level code-surface
1066        // path lists (`:bibliotecas`, `:exe`, `:servicos`): every declared
1067        // entry must resolve on disk. The `:bibliotecas` sweep folds onto
1068        // the [`Self::probe_declared_entries`] per-slot batch primitive
1069        // so the iteration + per-iteration `probe_declared_entry`
1070        // dispatch reads through one call rather than a three-line
1071        // `for … { self.probe_declared_entry(…)?; }` loop kept in
1072        // lockstep with the peer `:behavior` / `:upgrade-from` batch
1073        // wire-ups below. The `:exe` / `:servicos` peers fold onto the
1074        // sibling [`Self::probe_sandboxed_declared_entries`] per-slot
1075        // batch primitive on the sandboxed axis so the iteration +
1076        // per-iteration `probe_sandboxed_declared_entry` dispatch reads
1077        // through one call rather than a three-line `for … { self.
1078        // probe_sandboxed_declared_entry(…)?; }` loop kept in lockstep
1079        // with each other and with the peer `:bibliotecas` bare-batch
1080        // wire-up above. All three preserve the pre-lift diagnostic
1081        // order: `MissingEntry` fires before `ExeOutsideDir` /
1082        // `ServicoOutsideDir` on the same iteration, and iteration
1083        // proceeds in the caller-supplied iterator order (short-
1084        // circuiting on the first miss).
1085        self.probe_declared_entries(
1086            caixa.bibliotecas(),
1087            root,
1088            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
1089        )?;
1090
1091        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
1092        self.probe_sandboxed_declared_entries(
1093            caixa.exe(),
1094            root,
1095            crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
1096            &exe_dir,
1097            LayoutError::ExeOutsideDir,
1098        )?;
1099
1100        let servicos_dir = root.join(crate::render::LAYOUT_DIR_SERVICOS);
1101        self.probe_sandboxed_declared_entries(
1102            caixa.servicos(),
1103            root,
1104            crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
1105            &servicos_dir,
1106            LayoutError::ServicoOutsideDir,
1107        )?;
1108
1109        // ── M2 typed-substrate invariants ────────────────────────────────
1110
1111        // Compound per-Caixa entry gate on the M2 `:limits` slot: the
1112        // layout pipeline's `if let Some(l) = caixa.limits() { l.validate() }`
1113        // `Option::None → Ok(()) | Some(_) → dispatch` unwrap-and-
1114        // dispatch pattern — the four-axis cascade on the present-slot
1115        // arm ([`crate::LimitsSpec::validate`]'s `:memory` wasm32
1116        // zero-floor / below-page / above-cap / non-page-multiple;
1117        // `:fuel` zero-floor / cap; `:wall-clock` zero-floor / cap;
1118        // `:cpu` zero-floor / cap) folded onto the
1119        // [`crate::Caixa::validate_limits`] substrate primitive. The
1120        // absent-slot arm (`limits: None`, the canonical "no bound
1121        // declared — engine-default applies" author shape) is the
1122        // fold's identity element and passes trivially through the
1123        // primitive, byte-equal to the pre-lift `if let Some(l) = …`
1124        // guard this call site formerly carried. Pinned by the paired
1125        // `validate_limits_folds_arm_matches_gate` equivalence pin and
1126        // the `validate_limits_accepts_none` / `_accepts_clean_fixture`
1127        // positive-control pins in the [`crate::Caixa::validate_limits`]
1128        // pin family (`manifest.rs`).
1129        //
1130        // Same lift discipline the peer per-Caixa compound gates
1131        // ([`crate::Caixa::validate_upgrade_from`] d6801df,
1132        // [`crate::Caixa::validate_deps`] b5dd55e) each carry — one
1133        // named substrate-primitive gate per typed slot folds every
1134        // structural axis on that slot (plus the `Option::None`
1135        // identity element for the `Option`-shaped slots) onto one
1136        // call, so every future consumer that wants to re-check
1137        // `:limits` after a per-`{:memory, :fuel, :wall-clock, :cpu}`
1138        // patch (the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
1139        // materializer's admission webhook, a future `feira validate
1140        // --limits` per-caixa admission verb, a per-`:limits` overlay
1141        // resolver) reaches the four-axis cascade through one dispatch
1142        // rather than re-inlining the `if let Some(l) = …` unwrap-and-
1143        // dispatch pattern in lockstep with this wire-up.
1144        caixa.run_layout_gate(Caixa::validate_limits, LayoutError::limits_violation)?;
1145
1146        // Compound per-Caixa entry gate on the M2 `:behavior` slot's
1147        // pure value-shape surface: the layout pipeline's
1148        // `if let Some(b) = caixa.behavior() { b.validate() }`
1149        // `Option::None → Ok(()) | Some(_) → dispatch` unwrap-and-
1150        // dispatch pattern — the six-slot value-shape cascade on the
1151        // present-slot arm ([`crate::BehaviorSpec::validate`]'s per-
1152        // `:on-init` / `:on-call` / `:on-cast` / `:on-info` /
1153        // `:on-state-change` / `:on-terminate` non-empty / relative /
1154        // no-`..`-parent-escape / terminating-`.lisp`-extension
1155        // arm-set routed through the shared
1156        // [`crate::render::require_sandboxed_lisp_path`] helper) —
1157        // folded onto the [`crate::Caixa::validate_behavior`] substrate
1158        // primitive. The absent-slot arm (`behavior: None`, the
1159        // canonical "no callback declared — the runtime falls back to
1160        // the wasm-engine's default per arm" author shape) is the
1161        // fold's identity element and passes trivially through the
1162        // primitive, byte-equal to the pre-lift `if let Some(b) = …`
1163        // guard this call site formerly carried. Pinned by the paired
1164        // `validate_behavior_folds_arm_matches_gate` equivalence pin
1165        // and the `validate_behavior_accepts_none` /
1166        // `_accepts_clean_fixture` positive-control pins in the
1167        // [`crate::Caixa::validate_behavior`] pin family
1168        // (`manifest.rs`).
1169        //
1170        // The value-shape gate runs BEFORE the on-disk callback-path
1171        // existence walk below so a malformed `:behavior` slot
1172        // surfaces its self-locating per-slot diagnostic (naming the
1173        // offending `:on-*` slot) rather than the less-helpful
1174        // "missing behavior-callback" the existence probe would raise
1175        // against the resolved sandbox-escape path.
1176        //
1177        // Same lift discipline the peer per-Caixa compound gates
1178        // ([`crate::Caixa::validate_limits`] baa4688,
1179        // [`crate::Caixa::validate_upgrade_from`] d6801df,
1180        // [`crate::Caixa::validate_deps`] b5dd55e) each carry — one
1181        // named substrate-primitive gate per typed slot folds every
1182        // structural axis on that slot (plus the `Option::None`
1183        // identity element for the `Option`-shaped slots) onto one
1184        // call, so every future consumer that wants to re-check
1185        // `:behavior` after a per-`{:on-init, …, :on-terminate}`
1186        // patch (the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
1187        // materializer's admission webhook, a future `feira validate
1188        // --behavior` per-caixa admission verb, a per-`:behavior`
1189        // overlay resolver) reaches the six-slot cascade through one
1190        // dispatch rather than re-inlining the `if let Some(b) = …`
1191        // unwrap-and-dispatch pattern in lockstep with this wire-up.
1192        // The paired on-disk existence walk stays open-coded at this
1193        // altitude because it needs the [`LayoutInvariants::exists`]
1194        // filesystem oracle the pure typed-shape surface has no
1195        // reference to — mirror of the peer M2 `:upgrade-from` per-
1196        // instruction script-path existence probe that stayed at this
1197        // altitude after the [`crate::Caixa::validate_upgrade_from`]
1198        // lift for the same reason.
1199        caixa.run_layout_gate(Caixa::validate_behavior, LayoutError::behavior_violation)?;
1200        if let Some(b) = caixa.behavior() {
1201            self.probe_declared_entries(
1202                b.declared_paths(),
1203                root,
1204                crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
1205            )?;
1206        }
1207
1208        // Compound per-Caixa entry gate on `:upgrade-from`: the layout
1209        // pipeline's three-dispatch M2 `:upgrade-from` cascade — the
1210        // per-entry shape + cross-entry duplicate-`:from` gate
1211        // ([`crate::upgrade::validate_upgrade_from`]), the cross-slot
1212        // `:from < :versao` SemVer-2 precedence gate
1213        // ([`crate::upgrade::validate_upgrade_from_against_versao`]), and
1214        // the cross-slot `:state-change` ↔ `:on-state-change` composition
1215        // gate ([`crate::upgrade::validate_upgrade_from_against_behavior`])
1216        // — folded onto the [`crate::Caixa::validate_upgrade_from`]
1217        // substrate primitive. The three dispatches run in the same
1218        // canonical order at the primitive (per-entry → versao → behavior)
1219        // so the fold is byte-for-byte equivalent to the pre-fold
1220        // three-block cascade this call site formerly carried, pinned by
1221        // the paired
1222        // `validate_upgrade_from_folds_{per_entry,versao,behavior}_arm_matches_gate`
1223        // equivalence pins and the
1224        // `validate_upgrade_from_{per_entry_arm_fires_before_versao_arm,
1225        // versao_arm_fires_before_behavior_arm}` ordering pins in the
1226        // [`crate::Caixa::validate_upgrade_from`] pin family
1227        // (`manifest.rs`).
1228        //
1229        // Runs BEFORE the existing per-instruction script-path existence
1230        // pass below so a malformed typed slot surfaces its own
1231        // self-locating diagnostic rather than the less-helpful "missing
1232        // upgrade-script" (which doesn't fire for non-script axes at all).
1233        // Same lift discipline the peer per-slot compound gates
1234        // ([`crate::AplicacaoSpec::validate_contratos`] and its
1235        // `:membros` / `:entrada` / `:placement` / `:politicas` peers,
1236        // [`crate::MeshPolicy::validate`],
1237        // [`crate::SupervisorSpec::validate_children`]) each carry — one
1238        // named substrate-primitive gate per typed slot folds every
1239        // structural axis on that slot onto one call, so every future
1240        // consumer that wants to re-check `:upgrade-from` after a
1241        // per-entry patch (the deferred `caixa.pleme.io/v1alpha1/Caixa`
1242        // CR materializer's admission webhook, a future `feira validate
1243        // --upgrade` per-caixa admission verb, a per-`:upgrade-from`
1244        // overlay resolver) reaches the three-arm compound gate through
1245        // one dispatch rather than re-inlining the three-dispatch
1246        // cascade in lockstep with this wire-up.
1247        //
1248        // The per-instruction on-disk existence-probe walk below stays
1249        // open-coded at the layout wire-up site — that arm needs the
1250        // filesystem oracle on the [`LayoutInvariants`] trait, not on
1251        // the pure per-Caixa typed-shape surface the compound gate
1252        // folds. Same posture [`crate::Caixa::validate_code_paths`] takes
1253        // on the sibling code-path axes: the typed-shape gate fires on
1254        // the per-Caixa surface, the on-disk existence check fires on
1255        // the [`StandardLayout`] surface.
1256        caixa.run_layout_gate(Caixa::validate_upgrade_from, LayoutError::upgrade_violation)?;
1257        self.probe_declared_entries(
1258            caixa
1259                .upgrade_from()
1260                .iter()
1261                .flat_map(crate::UpgradeFromEntry::instructions)
1262                .filter_map(crate::upgrade::UpgradeInstruction::declared_path),
1263            root,
1264            crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
1265        )?;
1266
1267        // Supervisor invariants (typed shape — children, restart strategy).
1268        // The "supervisor doesn't own code" check is at the top of verify()
1269        // so it fires before the existence-check loops.
1270        if caixa.kind().is_supervisor() {
1271            // Raw `:restart-window` parse gate on the flat
1272            // `Caixa::restart_window: Option<String>` axis — the last
1273            // orphan-validator on the typed Caixa surface flagged by the
1274            // [`Self::validate_deps`] wire-up's closing comment (the
1275            // "Supervisor-axis specific" remainder) and the
1276            // [`Caixa::supervisor_view`] doc-comment's "the future
1277            // layout-side wire-up" pin. Until this gate landed
1278            // [`Caixa::validate_restart_window`] existed as `pub fn` on
1279            // [`Caixa`] with full per-arm unit coverage in `manifest::tests`
1280            // (`validate_restart_window_rejects_*` — fractional seconds,
1281            // decimal-shaped integer, half-unit minute, leading sign,
1282            // unknown unit, garbage, empty-after-trim; eight rejection
1283            // arms total), but no production code path called it —
1284            // `feira build` (the canonical author-time gate; routes
1285            // through [`StandardLayout::verify`]) silently accepted a
1286            // malformed `:restart-window` and [`Caixa::supervisor_view`]
1287            // soft-swallowed the parse failure as `restart_window: None`
1288            // (i.e. the canonical "omit the slot to express no reset"
1289            // sentinel), turning every malformed window into a never-reset
1290            // supervisor far from the source `caixa.lisp`, with no field
1291            // naming the offending `:restart-window`. The Erlang/OTP
1292            // `MaxIntensity / Period` invariant the typed [`SupervisorSpec`]
1293            // gate (`view.validate()` immediately below) enforces on the
1294            // `Option<Duration>` value never reached the gate at all on
1295            // these inputs: the parse error was already laundered to
1296            // `None`, and `None` is the canonical "never reset" shape
1297            // that always validates cleanly. Lifting the parse gate to
1298            // the layout-pipeline wire-up closes the laundering — every
1299            // value past this gate either parses through the shared
1300            // `crate::supervisor::duration_codec::parse` (and therefore
1301            // round-trips canonically) or fires the new
1302            // [`Self::RestartWindowViolation`] envelope at the source.
1303            //
1304            // Runs *inside* the `kind == Supervisor` branch (rather than
1305            // alongside the peer flat-Caixa gates `validate_nome` /
1306            // `validate_versao` / `validate_deps` / `validate_code_paths`
1307            // above the kind dispatch) because `:restart-window` is in
1308            // the Supervisor slot set per [`Caixa::declared_supervisor_slots`]
1309            // — every non-Supervisor caixa with `:restart-window` set
1310            // already errors upstream via the
1311            // [`Self::SupervisorSlotsOnNonSupervisor`] kind-coherence gate
1312            // (line 243-252), so reaching this gate on a non-Supervisor
1313            // kind would be a no-op (the field is `None` by construction).
1314            // Runs *before* `view.validate()` so the parse-side diagnostic
1315            // surfaces first on the raw-string axis — a `:restart-window
1316            // "1.5s"` lands on the more self-locating
1317            // `RestartWindowViolation` (which names the offending raw
1318            // string verbatim) rather than the laundered-to-`None`
1319            // soft-pass that the typed view would silently let through.
1320            //
1321            // Same per-axis `*Violation { caixa, issue }` envelope every
1322            // peer flat-Caixa wrap exposes ([`Self::NomeViolation`] /
1323            // [`Self::VersaoViolation`] 1f74a5f,
1324            // [`Self::DepsViolation`] aa77d0f, [`Self::CodePathViolation`]
1325            // b868442). Threads [`ManifestError::RestartWindowMalformed`]
1326            // Display through verbatim — the per-arm reason already names
1327            // the offending raw value (e.g. `":restart-window \"1.5s\" is
1328            // not a canonical duration: …"`), so the wrap's `issue`
1329            // carries a self-locating "which axis, which value, why"
1330            // without re-shaping the parser-side reason.
1331            caixa.run_layout_gate(
1332                Caixa::validate_restart_window,
1333                LayoutError::restart_window_violation,
1334            )?;
1335            // Compound per-Caixa entry gate on the Supervisor-kind
1336            // supervision-tree slot family: the layout pipeline's paired
1337            // `let view = caixa.supervisor_view().expect(...);
1338            // view.validate() … validate_no_self_supervision(...) …`
1339            // cascade — the typed-shape cascade
1340            // ([`crate::SupervisorSpec::validate`]'s per-slot gates on
1341            // `:estrategia` ↔ `:children` invariants, `:max-restarts` /
1342            // `:restart-window` bounds, per-child DNS-1123 `:caixa`
1343            // names, semver-valid `:versao` constraints, the
1344            // set-not-multiset duplicate-child gate) and the cross-slot
1345            // self-edge gate
1346            // ([`crate::supervisor::validate_no_self_supervision`], the
1347            // `:children :caixa` ≠ `:nome` invariant the typed view
1348            // cannot enforce on its own because it carries the children
1349            // but not the parent `:nome`) — folded onto the
1350            // [`crate::Caixa::validate_supervisor_shape`] substrate
1351            // primitive. The two arms run in the same canonical order at
1352            // the primitive (typed-shape cascade → cross-slot self-edge)
1353            // so the fold is byte-for-byte equivalent to the pre-fold
1354            // two-block cascade this call site formerly carried, pinned
1355            // by the paired
1356            // `validate_supervisor_shape_folds_{view,self_supervision}_arm_matches_gate`
1357            // equivalence pins and the
1358            // `validate_supervisor_shape_view_arm_fires_before_self_supervision_arm`
1359            // ordering pin in the
1360            // [`crate::Caixa::validate_supervisor_shape`] pin family
1361            // (`manifest.rs`).
1362            //
1363            // Same lift discipline the peer per-Caixa compound gates
1364            // ([`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
1365            // [`crate::Caixa::validate_upgrade_from`] d6801df,
1366            // [`crate::Caixa::validate_deps`] b5dd55e,
1367            // [`crate::Caixa::validate_limits`] baa4688,
1368            // [`crate::Caixa::validate_behavior`] 0d2877a) each carry —
1369            // one named substrate-primitive gate folds every structural +
1370            // cross-slot axis on that slot family onto one call, so every
1371            // future consumer that wants to re-check the Supervisor shape
1372            // after a per-slot patch (the wasm-operator's hierarchical
1373            // reconciliation scheduler, the M4
1374            // `mesh.pleme.io/v1alpha1/Supervisor` CR materializer's
1375            // admission webhook, a future `feira validate --supervisor`
1376            // per-caixa admission verb, a per-Supervisor overlay
1377            // resolver) reaches the two-arm compound gate through one
1378            // dispatch rather than re-inlining the two-dispatch cascade
1379            // in lockstep with this wire-up. Peer with the
1380            // [`crate::render::require_supervisor_view`] compound entry
1381            // gate every per-Supervisor renderer would route through
1382            // (which already folds the same `spec.validate()` +
1383            // `validate_no_self_supervision` two-arm cascade behind its
1384            // `require_kind` + `validate_restart_window` prelude): the
1385            // two consumers of the Supervisor-shape cascade now share
1386            // one substrate primitive on each side of the
1387            // author-time-vs-renderer split, rather than two open-coded
1388            // cascades kept in lockstep.
1389            //
1390            // `:restart-window` stays wired above through its own
1391            // per-axis `LayoutError::RestartWindowViolation` envelope
1392            // (the raw-string parse gate on the flat
1393            // `Caixa::restart_window: Option<String>` axis, distinct
1394            // from the typed view's `Duration`-shape gate) — the fold
1395            // covers the two arms that share the
1396            // `LayoutError::SupervisorViolation` envelope; the
1397            // parse-gate arm keeps its self-locating envelope so a
1398            // malformed `:restart-window` surfaces the raw-value
1399            // diagnostic rather than the laundered-to-`None` soft-pass.
1400            caixa.run_layout_gate(
1401                Caixa::validate_supervisor_shape,
1402                LayoutError::supervisor_violation,
1403            )?;
1404        }
1405
1406        // Aplicacao invariants — typed graph composition. Like
1407        // Supervisor, an Aplicacao runs no code itself.
1408        //
1409        // Compound per-Caixa entry gate on the Aplicacao-kind mesh-slot
1410        // family: the layout pipeline's paired `let view =
1411        // caixa.aplicacao_view().expect(...); view.validate() …
1412        // validate_no_self_membership(...) …` cascade — the typed-shape
1413        // cascade ([`crate::AplicacaoSpec::validate`]'s per-slot gates
1414        // on `:membros`, `:contratos`, `:entrada`, `:placement`,
1415        // `:politicas`, in that declared order) and the cross-slot
1416        // self-edge gate ([`crate::aplicacao::validate_no_self_membership`],
1417        // the `:membros :caixa` ≠ `:nome` invariant the typed view
1418        // cannot enforce on its own because it carries the membros but
1419        // not the parent `:nome`) — folded onto the
1420        // [`crate::Caixa::validate_aplicacao_shape`] substrate primitive.
1421        // The two arms run in the same canonical order at the primitive
1422        // (typed-shape cascade → cross-slot self-edge) so the fold is
1423        // byte-for-byte equivalent to the pre-fold two-block cascade
1424        // this call site formerly carried, pinned by the paired
1425        // `validate_aplicacao_shape_folds_{view,self_membership}_arm_matches_gate`
1426        // equivalence pins and the
1427        // `validate_aplicacao_shape_view_arm_fires_before_self_membership_arm`
1428        // ordering pin in the [`crate::Caixa::validate_aplicacao_shape`]
1429        // pin family (`manifest.rs`).
1430        //
1431        // Same lift discipline the peer per-slot compound gates
1432        // ([`crate::Caixa::validate_upgrade_from`] d6801df,
1433        // [`crate::Caixa::validate_deps`] b5dd55e,
1434        // [`crate::Caixa::validate_limits`] baa4688,
1435        // [`crate::Caixa::validate_behavior`] 0d2877a) each carry — one
1436        // named substrate-primitive gate folds every structural +
1437        // cross-slot axis on that slot family onto one call, so every
1438        // future consumer that wants to re-check the Aplicacao shape
1439        // after a per-slot patch (the deferred
1440        // `caixa.pleme.io/v1alpha1/Caixa` CR materializer's admission
1441        // webhook, a future `feira validate --aplicacao` per-caixa
1442        // admission verb, a per-Aplicacao overlay resolver) reaches the
1443        // two-arm compound gate through one dispatch rather than
1444        // re-inlining the two-dispatch cascade in lockstep with this
1445        // wire-up. Peer with the [`crate::render::require_aplicacao_view`]
1446        // compound entry gate every per-Aplicacao renderer routes
1447        // through (3aefefb): the two consumers of the Aplicacao-shape
1448        // cascade now share one substrate primitive on each side of the
1449        // author-time-vs-renderer split, rather than two open-coded
1450        // cascades kept in lockstep.
1451        //
1452        // The outer `if caixa.kind().is_aplicacao()` guard stays because
1453        // [`crate::Caixa::validate_aplicacao_shape`] is the fold's
1454        // identity element on non-Aplicacao kinds (returns `Ok(())`
1455        // without touching the mesh slots — same posture as
1456        // [`crate::Caixa::validate_limits`] / [`Self`]::
1457        // [`crate::Caixa::validate_behavior`] on their `Option`-shaped
1458        // slots); the guard is a redundant but zero-cost fast-path that
1459        // preserves the peer supervisor branch's `if
1460        // caixa.kind().is_supervisor()` parallel structure at this
1461        // altitude.
1462        if caixa.kind().is_aplicacao() {
1463            caixa.run_layout_gate(
1464                Caixa::validate_aplicacao_shape,
1465                LayoutError::aplicacao_violation,
1466            )?;
1467        }
1468
1469        // Acao invariants — typed CI-run decompose. Like Supervisor and
1470        // Aplicacao, an Acao runs no code itself; unlike them, its sole
1471        // payload is a [`canteiro_types::CiRun`] whose declared-node
1472        // shape can carry three structural violations
1473        // ([`canteiro_types::DecomposeError`]: `DuplicateNode`,
1474        // `UnknownDep`, `Cycle`) the layout pipeline formerly deferred
1475        // to [`caixa_actions::validate`] — layout only checked `:ci`
1476        // *presence* via [`Self::MissingCi`] above, so a `:kind Acao`
1477        // carrying a structurally illegal `:ci` (a duplicate node
1478        // name, a dependency on an undeclared node, a dependency
1479        // cycle) passed `feira build` cleanly and surfaced the
1480        // diagnostic only when [`caixa_actions::validate`] later
1481        // refused it — far from the source `caixa.lisp` on the
1482        // author-time gate side.
1483        //
1484        // Compound per-Caixa entry gate on the Acao-kind `:ci` slot
1485        // family: the [`crate::render::decompose_ci`] typed decompose
1486        // gate — the sibling axis owned by the substrate-canonical
1487        // [`crate::render::require_acao_view`] compound helper every
1488        // per-`Acao` renderer routes through — folded onto the
1489        // [`crate::Caixa::validate_acao_shape`] substrate primitive.
1490        // The single-arm fold is byte-for-byte equivalent to the
1491        // pre-fold `decompose_ci(caixa, ci).map(|_| ())?` call this
1492        // wire-up sees, pinned by the paired
1493        // `validate_acao_shape_folds_decompose_arm_matches_gate`
1494        // equivalence pin and the
1495        // `validate_acao_shape_accepts_non_acao_kind` /
1496        // `validate_acao_shape_accepts_absent_ci_slot` identity-
1497        // element pins in the
1498        // [`crate::Caixa::validate_acao_shape`] pin family
1499        // (`manifest.rs`).
1500        //
1501        // Same lift discipline the peer per-kind compound gates
1502        // ([`crate::Caixa::validate_supervisor_shape`] 4c70105,
1503        // [`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
1504        // [`crate::Caixa::validate_upgrade_from`] d6801df,
1505        // [`crate::Caixa::validate_deps`] b5dd55e,
1506        // [`crate::Caixa::validate_limits`] baa4688,
1507        // [`crate::Caixa::validate_behavior`] 0d2877a) each carry —
1508        // one named substrate-primitive gate folds every structural
1509        // axis on that kind onto one call, so every future consumer
1510        // that wants to re-check the Acao shape after a per-node
1511        // patch (a per-`Acao` CR materializer's admission webhook, a
1512        // future `feira validate --acao` per-caixa admission verb, a
1513        // per-`Acao` overlay resolver) reaches the compound gate
1514        // through one dispatch rather than re-inlining the decompose
1515        // cascade in lockstep with this wire-up. Peer with the
1516        // [`crate::render::require_acao_view`] compound entry gate
1517        // every per-`Acao` renderer routes through: the two consumers
1518        // of the Acao-shape cascade now share one substrate primitive
1519        // on each side of the author-time-vs-renderer split, rather
1520        // than two open-coded cascades kept in lockstep. Closes the
1521        // last per-kind asymmetry — with this wire-up the four typed
1522        // named-caixa kinds (`Servico` / `Aplicacao` / `Supervisor` /
1523        // `Acao`) each route through one compound per-Caixa shape
1524        // gate at the layout altitude.
1525        //
1526        // Runs *after* the [`Self::MissingCi`] presence gate above
1527        // (so a `:kind Acao` caixa with `ci = None` surfaces the
1528        // presence diagnostic first — the fold's identity-element arm
1529        // passes cleanly on absent `:ci`) and *after* the sibling
1530        // Supervisor / Aplicacao shape gates so the per-kind
1531        // diagnostic ordering at the layout altitude reads as the
1532        // canonical `Supervisor → Aplicacao → Acao` sweep.
1533        //
1534        // The outer `if caixa.kind().is_acao()` guard stays because
1535        // [`crate::Caixa::validate_acao_shape`] is the fold's
1536        // identity element on non-Acao kinds (returns `Ok(())`
1537        // without touching the `:ci` slot — same posture as
1538        // [`crate::Caixa::validate_supervisor_shape`] /
1539        // [`crate::Caixa::validate_aplicacao_shape`] on the sibling
1540        // typed-view-carrying arms); the guard is a redundant but
1541        // zero-cost fast-path that preserves the peer supervisor /
1542        // aplicacao branches' `if caixa.kind().is_<kind>()` parallel
1543        // structure at this altitude.
1544        if caixa.kind().is_acao() {
1545            caixa.run_layout_gate(Caixa::validate_acao_shape, LayoutError::acao_violation)?;
1546        }
1547
1548        Ok(())
1549    }
1550}
1551
1552#[derive(Debug, Error, PartialEq, Eq)]
1553pub enum LayoutError {
1554    #[error("manifest missing: {}", .0.display())]
1555    MissingManifest(PathBuf),
1556    #[error("caixa '{caixa}' is a Biblioteca but has no lib entry — expected {}", expected.display())]
1557    MissingLib { caixa: String, expected: PathBuf },
1558    #[error("caixa '{0}' is a Binario but has no :exe entries")]
1559    BinarioWithoutExe(String),
1560    #[error("caixa '{0}' is a Servico but has no :servicos entries")]
1561    ServicoWithoutServicos(String),
1562    #[error("declared {kind} entry missing: {}", path.display())]
1563    MissingEntry { kind: &'static str, path: PathBuf },
1564    #[error("exe entry outside exe/ directory: {}", .0.display())]
1565    ExeOutsideDir(PathBuf),
1566    #[error("servico entry outside servicos/ directory: {}", .0.display())]
1567    ServicoOutsideDir(PathBuf),
1568    #[error("caixa '{caixa}' has invalid :nome: {issue}")]
1569    NomeViolation { caixa: String, issue: String },
1570    #[error("caixa '{caixa}' has invalid :versao: {issue}")]
1571    VersaoViolation { caixa: String, issue: String },
1572    #[error("caixa '{caixa}' has invalid :deps / :deps-dev entry: {issue}")]
1573    DepsViolation { caixa: String, issue: String },
1574    #[error("caixa '{caixa}' has invalid :etiquetas entry: {issue}")]
1575    EtiquetasViolation { caixa: String, issue: String },
1576    #[error("caixa '{caixa}' has invalid :autores entry: {issue}")]
1577    AutoresViolation { caixa: String, issue: String },
1578    #[error("caixa '{caixa}' has invalid :repositorio: {issue}")]
1579    RepositorioViolation { caixa: String, issue: String },
1580    #[error("caixa '{caixa}' has invalid :descricao: {issue}")]
1581    DescricaoViolation { caixa: String, issue: String },
1582    #[error("caixa '{caixa}' has invalid :licenca: {issue}")]
1583    LicencaViolation { caixa: String, issue: String },
1584    #[error("caixa '{caixa}' has invalid :edicao: {issue}")]
1585    EdicaoViolation { caixa: String, issue: String },
1586    #[error("caixa '{caixa}' has invalid code-path entry: {issue}")]
1587    CodePathViolation { caixa: String, issue: String },
1588    #[error("caixa '{caixa}' has invalid :limits: {issue}")]
1589    LimitsViolation { caixa: String, issue: String },
1590    #[error("caixa '{caixa}' has invalid :behavior callback: {issue}")]
1591    BehaviorViolation { caixa: String, issue: String },
1592    #[error("caixa '{caixa}' has invalid :upgrade-from entry: {issue}")]
1593    UpgradeViolation { caixa: String, issue: String },
1594    #[error("supervisor caixa '{caixa}' violates typed shape: {issue}")]
1595    SupervisorViolation { caixa: String, issue: String },
1596    #[error("supervisor caixa '{caixa}' has invalid :restart-window: {issue}")]
1597    RestartWindowViolation { caixa: String, issue: String },
1598    #[error(
1599        "supervisor caixa '{0}' must not declare :bibliotecas, :exe, or :servicos — supervisors don't run code, they orchestrate other caixas"
1600    )]
1601    SupervisorOwnsCode(String),
1602    #[error("aplicacao caixa '{caixa}' violates typed shape: {issue}")]
1603    AplicacaoViolation { caixa: String, issue: String },
1604    #[error("acao caixa '{caixa}' violates typed shape: {issue}")]
1605    AcaoViolation { caixa: String, issue: String },
1606    #[error(
1607        "aplicacao caixa '{0}' must not declare :bibliotecas, :exe, or :servicos — aplicacaos compose Servicos, they don't run code themselves"
1608    )]
1609    AplicacaoOwnsCode(String),
1610    #[error(
1611        "acao caixa '{0}' must not declare :bibliotecas, :exe, or :servicos — acaos carry a typed CI run (:ci), they don't run code themselves"
1612    )]
1613    AcaoOwnsCode(String),
1614    #[error(
1615        "caixa '{caixa}' is :kind {kind:?} but declares Aplicacao-only mesh slot(s): {slots} — \
1616         :membros / :contratos / :politicas / :placement / :entrada compose a :kind Aplicacao's \
1617         typed graph (MESH-COMPOSITION §III.1) and are silently ignored on every other kind \
1618         (never validated, never rendered); move them to a :kind Aplicacao caixa or remove them"
1619    )]
1620    MeshSlotsOnNonAplicacao {
1621        caixa: String,
1622        kind: CaixaKind,
1623        slots: String,
1624    },
1625    #[error(
1626        "caixa '{caixa}' is :kind {kind:?} but declares Supervisor-only slot(s): {slots} — \
1627         :estrategia / :max-restarts / :restart-window / :children compose a :kind Supervisor's \
1628         typed OTP supervisor (INSPIRATIONS §II.2) and are silently ignored on every other kind \
1629         (never validated, never reconciled); move them to a :kind Supervisor caixa or remove them"
1630    )]
1631    SupervisorSlotsOnNonSupervisor {
1632        caixa: String,
1633        kind: CaixaKind,
1634        slots: String,
1635    },
1636    #[error(
1637        "caixa '{caixa}' is :kind {kind:?} but declares Servico-only slot(s): {slots} — \
1638         :limits / :behavior / :upgrade-from configure the runtime of a long-running :kind Servico \
1639         wasm component (INSPIRATIONS §III.1 / §II.3 / §II.4) and are silently ignored on every \
1640         other kind (never rendered into a chart or programs.yaml entry); move them to a :kind \
1641         Servico caixa or remove them"
1642    )]
1643    ServicoSlotsOnNonServico {
1644        caixa: String,
1645        kind: CaixaKind,
1646        slots: String,
1647    },
1648    #[error(
1649        "caixa '{caixa}' is :kind {kind:?} but declares foreign code-surface slot(s): {slots} — \
1650         :exe is the nix-built executable surface owned only by :kind Binario, :servicos is the \
1651         wasm-component + ComputeUnit daemon surface owned only by :kind Servico; \
1652         caixa-helm / caixa-flux / caixa-flake gate emission on `require_kind(_, <owning-kind>)`, \
1653         so a declared :exe / :servicos on the wrong code-running kind is silently ignored — the \
1654         path is validated by the layout's path-existence loops but never rendered into a build \
1655         target or programs.yaml entry. Move the slot to its owning kind, change :kind to match \
1656         (Binario for :exe, Servico for :servicos), or drop the slot entirely"
1657    )]
1658    ForeignCodeSlot {
1659        caixa: String,
1660        kind: CaixaKind,
1661        slots: String,
1662    },
1663    #[error("caixa '{0}' is an Acao but has no :ci slot")]
1664    MissingCi(String),
1665    #[error(
1666        "caixa '{caixa}' is :kind {kind:?} but declares the Acao-only :ci slot — \
1667         :ci carries a typed CI run (canteiro_types::CiRun, CANTEIRO §7.1-C) that only the \
1668         caixa-actions renderer validates for :kind Acao, and is silently ignored on every \
1669         other kind (never decomposed, never rendered); move it to a :kind Acao caixa or \
1670         remove it"
1671    )]
1672    CiOnNonAcao { caixa: String, kind: CaixaKind },
1673}
1674
1675// Fold the layout-pipeline per-Caixa violation wrap onto one substrate
1676// primitive per typed slot. Every `LayoutError::*Violation { caixa, issue }`
1677// variant follows the same uniform shape — `caixa = layout-Caixa's :nome`,
1678// `issue = the gate's per-arm Display` — and every wire-up in
1679// [`StandardLayout::verify`] used to open-code the identical five-line
1680// `.map_err(|err| LayoutError::XxxViolation { caixa: caixa.nome().to_string(),
1681// issue: err.to_string() })` block, once per typed slot. Sixteen distinct
1682// wrap-variants × eighteen wire-up sites is exactly the duplication the
1683// PRIME DIRECTIVE names as a bug: every future consumer that wants to add a
1684// new per-slot gate (the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
1685// materializer's admission webhook, a future per-slot `feira validate`
1686// verb, a per-slot overlay resolver) had to re-inline the five-line block
1687// in lockstep with the pre-existing wire-ups.
1688//
1689// The macro below generates one static constructor per variant of shape
1690// `fn <slot>_violation(caixa: &Caixa, err: impl Display) -> LayoutError`,
1691// so every wire-up site collapses onto one dispatch:
1692// `caixa.validate_<slot>().map_err(|err| LayoutError::<slot>_violation(caixa,
1693// err))?;`. The uniform two-slot construction (`caixa: caixa.nome()
1694// .to_string()`, `issue: err.to_string()`) is spelled once — inside the
1695// macro — rather than at every wire-up site. Every constructor is
1696// `#[must_use]` so a caller who mistakenly discards the wrapped error
1697// (rather than routing it through `?`) trips a compile warning at the
1698// wire-up site.
1699//
1700// Peer with the per-slot compound entry gates every substrate primitive
1701// on the M2/M3 typed-slot family already carries
1702// ([`crate::Caixa::validate_deps`] b5dd55e, [`crate::Caixa::validate_limits`]
1703// baa4688, [`crate::Caixa::validate_behavior`] 0d2877a,
1704// [`crate::Caixa::validate_upgrade_from`] d6801df,
1705// [`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
1706// [`crate::AplicacaoSpec::validate_contratos`],
1707// [`crate::MeshPolicy::validate`],
1708// [`crate::SupervisorSpec::validate_children`]): the author-time gates
1709// fold onto one substrate primitive per typed slot; here the layout-side
1710// error-wrap folds onto one substrate primitive per typed variant, so the
1711// two sides of the layout pipeline's per-slot cascade (the gate, the
1712// wrap) each route through one call rather than N open-coded block
1713// repetitions.
1714macro_rules! layout_violation_ctors {
1715    ($($ctor:ident => $variant:ident),* $(,)?) => {
1716        impl LayoutError {
1717            $(
1718                #[doc = concat!(
1719                    "Construct a [`LayoutError::",
1720                    stringify!($variant),
1721                    "`] wrapping `err` under `caixa.nome()`. Folds the ",
1722                    "uniform `{ caixa: caixa.nome().to_string(), issue: ",
1723                    "err.to_string() }` two-slot construction onto one ",
1724                    "substrate primitive so every ",
1725                    "[`StandardLayout::verify`] wire-up on this variant ",
1726                    "reads through one dispatch rather than the pre-lift ",
1727                    "five-line open-coded block."
1728                )]
1729                #[must_use]
1730                pub fn $ctor<E: std::fmt::Display>(caixa: &crate::Caixa, err: E) -> Self {
1731                    Self::$variant {
1732                        caixa: caixa.nome().to_string(),
1733                        issue: err.to_string(),
1734                    }
1735                }
1736            )*
1737        }
1738    };
1739}
1740
1741layout_violation_ctors! {
1742    nome_violation => NomeViolation,
1743    versao_violation => VersaoViolation,
1744    deps_violation => DepsViolation,
1745    etiquetas_violation => EtiquetasViolation,
1746    autores_violation => AutoresViolation,
1747    repositorio_violation => RepositorioViolation,
1748    descricao_violation => DescricaoViolation,
1749    licenca_violation => LicencaViolation,
1750    edicao_violation => EdicaoViolation,
1751    code_path_violation => CodePathViolation,
1752    limits_violation => LimitsViolation,
1753    behavior_violation => BehaviorViolation,
1754    upgrade_violation => UpgradeViolation,
1755    restart_window_violation => RestartWindowViolation,
1756    supervisor_violation => SupervisorViolation,
1757    aplicacao_violation => AplicacaoViolation,
1758    acao_violation => AcaoViolation,
1759}
1760
1761// Fold the four `LayoutError::*SlotsOn*` / `LayoutError::ForeignCodeSlot`
1762// kind-coherence wrap sites onto one substrate primitive per typed variant —
1763// the sibling of [`layout_violation_ctors!`] above on the second uniform
1764// error-envelope shape `LayoutError` carries: `{ caixa: caixa.nome(),
1765// kind: caixa.kind(), slots: <declared_*_slots()>.join(" ") }`. Every
1766// wire-up in [`StandardLayout::verify`] on this shape (four sites: the
1767// M3-mesh gate on non-Aplicacao, the supervisor-tree gate on
1768// non-Supervisor, the M2-runtime gate on non-Servico, the code-surface
1769// `ForeignCodeSlot` gate) used to open-code the identical four-field
1770// `.{ caixa: caixa.nome().to_string(), kind: caixa.kind(), slots:
1771// <declared_*_slots>.join(" ") }` block — the exact "same block re-inlined
1772// at every consumer" shape the PRIME DIRECTIVE names as a bug on the same
1773// altitude the peer [`layout_violation_ctors!`] macro just closed on the
1774// `{ caixa, issue }` sibling shape.
1775//
1776// The macro below generates one static constructor per variant of shape
1777// `fn <slot>_on_non_<owner>(caixa: &Caixa, slots: Vec<&'static str>) ->
1778// LayoutError`, so every wire-up site collapses onto one dispatch:
1779// `return Err(LayoutError::<slot>_on_non_<owner>(caixa, <declared_slots>));`.
1780// The uniform four-field construction is spelled once — inside the macro —
1781// rather than at every wire-up site. Every constructor is `#[must_use]` so
1782// a caller who mistakenly discards the constructed error (rather than
1783// routing it through `return Err(…)`) trips a compile warning at the
1784// wire-up site.
1785//
1786// Peer with the `_violation` constructor family above on the same
1787// `LayoutError` — the two together now fold every uniform-shape
1788// `LayoutError` variant carried by [`StandardLayout::verify`] onto one
1789// substrate primitive per typed variant, so the layout-side error-wrap
1790// surface reads through one dispatch per variant rather than N open-coded
1791// blocks. Every future consumer that wants to construct one of these
1792// variants outside the layout pipeline (a per-slot admission webhook, a
1793// `feira validate --kind X` verb, an overlay resolver rejecting a
1794// kind-foreign patch) reaches its variant through one call, matching the
1795// `_violation` family's substrate-primitive discipline.
1796macro_rules! layout_slot_kind_ctors {
1797    ($($ctor:ident => $variant:ident),* $(,)?) => {
1798        impl LayoutError {
1799            $(
1800                #[doc = concat!(
1801                    "Construct a [`LayoutError::",
1802                    stringify!($variant),
1803                    "`] naming the offending slot list under `caixa.nome()` ",
1804                    "at `caixa.kind()`. Folds the uniform `{ caixa: caixa.",
1805                    "nome().to_string(), kind: caixa.kind(), slots: slots.",
1806                    "join(\" \") }` four-field construction onto one substrate ",
1807                    "primitive so every [`StandardLayout::verify`] wire-up on ",
1808                    "this variant reads through one dispatch rather than the ",
1809                    "pre-lift open-coded block."
1810                )]
1811                #[must_use]
1812                pub fn $ctor(caixa: &crate::Caixa, slots: Vec<&'static str>) -> Self {
1813                    Self::$variant {
1814                        caixa: caixa.nome().to_string(),
1815                        kind: caixa.kind(),
1816                        slots: slots.join(" "),
1817                    }
1818                }
1819            )*
1820        }
1821    };
1822}
1823
1824layout_slot_kind_ctors! {
1825    mesh_slots_on_non_aplicacao => MeshSlotsOnNonAplicacao,
1826    supervisor_slots_on_non_supervisor => SupervisorSlotsOnNonSupervisor,
1827    servico_slots_on_non_servico => ServicoSlotsOnNonServico,
1828    foreign_code_slot => ForeignCodeSlot,
1829}
1830
1831// Fold the five [`LayoutError::MissingEntry`] wire-up sites at
1832// [`StandardLayout::verify`] onto one substrate primitive on `LayoutError` —
1833// the third and last uniform-shape envelope on `LayoutError` after the
1834// `{ caixa, issue }` family the [`layout_violation_ctors!`] macro closed
1835// (131ca0d) and the `{ caixa, kind, slots }` family the peer
1836// [`layout_slot_kind_ctors!`] macro closed (0419438). Each of the five
1837// wire-up sites on `MissingEntry` (`:bibliotecas` iteration line 823,
1838// `:exe` iteration line 834, `:servicos` iteration line 848, `:behavior`
1839// on-disk callback-path iteration line 957, `:upgrade-from` per-
1840// instruction script-path iteration line 1021) opened the same four-line
1841// `LayoutError::MissingEntry { kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_<slot>,
1842// path: full }` struct-literal block — the exact "same block re-inlined
1843// at every consumer" shape the PRIME DIRECTIVE names as a bug, on the
1844// same altitude the peer `_violation` / `_slots_on_non_*` families each
1845// closed on the sibling `LayoutError` envelopes.
1846//
1847// One `#[must_use]` inherent constructor on `LayoutError` collapses the
1848// five sites onto one dispatch:
1849// `return Err(LayoutError::missing_entry(<kind-label>, full));`, byte-
1850// equal to the pre-lift struct-literal block. A macro is not warranted
1851// on the one-variant envelope shape `{ kind: &'static str, path: PathBuf }`
1852// (unlike the sibling 16-variant `_violation` / 4-variant
1853// `_slots_on_non_*` shapes), but the same substrate-primitive discipline
1854// applies: every future consumer that wants to construct a `MissingEntry`
1855// outside the layout pipeline (a per-slot admission webhook probing a
1856// declared path against an out-of-band filesystem oracle, a `feira
1857// validate --lib` / `--exe` / `--servico` / `--behavior` / `--upgrade`
1858// per-caixa admission verb, the deferred `caixa.pleme.io/v1alpha1/Caixa`
1859// CR materializer's admission-webhook floor, a per-cluster overlay
1860// resolver rejecting a missing entry against a cluster-local filesystem
1861// snapshot) reaches the variant through one call rather than re-inlining
1862// the four-line struct-literal block in lockstep with the five
1863// layout-pipeline wire-up sites.
1864impl LayoutError {
1865    /// Construct a [`LayoutError::MissingManifest`] naming the resolved
1866    /// `caixa.lisp` path the layout gate probed for at
1867    /// [`StandardLayout::verify`]'s top-of-body manifest-existence
1868    /// check.
1869    ///
1870    /// Folds the uniform `Self::MissingManifest(path)` one-slot
1871    /// tuple-literal onto one substrate primitive so the sole
1872    /// [`StandardLayout::verify`] wire-up on this variant reads through
1873    /// one dispatch rather than the pre-lift open-coded tuple-literal
1874    /// block. Peer of the sibling [`Self::missing_entry`] `{ kind, path }`
1875    /// ctor on the same envelope's declared-entry-existence axis, and
1876    /// mirror-symmetric sibling of the sibling [`Self::missing_lib`]
1877    /// `{ caixa, expected }` ctor on the enclosing envelope's per-caixa
1878    /// required-fallback-path axis: the top-level manifest-existence
1879    /// gate now routes its emit-site through the same substrate-
1880    /// primitive discipline every sibling variant on the same
1881    /// [`LayoutError`] envelope carries, closing the last un-lifted
1882    /// `<Variant>(PathBuf)` tuple-newtype variant on the envelope
1883    /// carried through a direct open-coded construction (the peer
1884    /// [`Self::ExeOutsideDir`] / [`Self::ServicoOutsideDir`]
1885    /// `<Variant>(PathBuf)` variants stay on their pre-lift
1886    /// tuple-variant-constructor coercion inside
1887    /// [`StandardLayout::probe_sandboxed_declared_entry`]'s
1888    /// `outside_ctor: fn(PathBuf) -> LayoutError` function-pointer
1889    /// parameter — the substrate primitive on that axis is the
1890    /// sandbox-probe helper itself, so the paired variant ctors are
1891    /// already substrate-canonical at their wire-up site by
1892    /// construction).
1893    ///
1894    /// The `PathBuf` parameter takes ownership of the resolved
1895    /// `root.join("caixa.lisp")` path from the wire-up site's
1896    /// [`Path::join`] product — byte-equal to the pre-lift
1897    /// `Self::MissingManifest(manifest)` tuple-literal on the same
1898    /// owned `PathBuf` fixture. `#[must_use]` fires a compile warning
1899    /// at any wire-up that mistakenly discards the constructed error
1900    /// rather than routing it through `return Err(…)`.
1901    ///
1902    /// Every future consumer that wants to construct this variant
1903    /// outside [`StandardLayout::verify`] — a deferred
1904    /// `caixa.pleme.io/v1alpha1/Caixa` CR materializer's admission-
1905    /// webhook re-check probing the resolved manifest path against a
1906    /// mounted cluster-local filesystem snapshot, a future
1907    /// `feira validate --manifest-exists` per-caixa admission verb
1908    /// re-running the manifest-existence gate on demand, a per-cluster
1909    /// overlay resolver rejecting a `:placement`-scoped manifest
1910    /// omission against a cluster-local snapshot — now reaches the
1911    /// variant through one call rather than re-inlining the two-line
1912    /// tuple-literal in lockstep with the in-crate wire-up site.
1913    #[must_use]
1914    pub const fn missing_manifest(path: PathBuf) -> Self {
1915        Self::MissingManifest(path)
1916    }
1917
1918    /// Construct a [`LayoutError::MissingEntry`] naming the missing
1919    /// declared entry at `path` under the canonical `kind` label from
1920    /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`] /
1921    /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] /
1922    /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] /
1923    /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`] /
1924    /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT`].
1925    /// Folds the uniform `{ kind, path }` two-slot construction onto one
1926    /// substrate primitive so every [`StandardLayout::verify`] wire-up
1927    /// on this variant reads through one dispatch rather than the
1928    /// pre-lift open-coded struct-literal block.
1929    ///
1930    /// `pub const fn` — matches the sibling [`Self::missing_manifest`]
1931    /// `pub const fn` shape on the same [`LayoutError`] envelope's
1932    /// path-carrying tuple-newtype axis. The body is a pure two-field
1933    /// struct-literal (`Self::MissingEntry { kind, path }` — no
1934    /// `.to_string()` / `.into()` / accessor projection on either arg,
1935    /// unlike the sibling [`Self::missing_lib`] / [`Self::ci_on_non_acao`]
1936    /// ctors that route through `caixa.nome().to_string()`), so the
1937    /// const-eval surface widens with no body change. Every downstream
1938    /// consumer that wants to fold a per-arm layout diagnostic into a
1939    /// `const` position (a `const MISSING_BIBLIOTECA: LayoutError =
1940    /// LayoutError::missing_entry(LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
1941    /// PathBuf::new());` fixture the future M4 `caixa.pleme.io/v1alpha1/Caixa`
1942    /// CR materializer's admission-webhook per-kind rejection-body
1943    /// lookup table reaches for, a compile-time per-arm diagnostic
1944    /// registry the LSP hover renderer materializes per typed-slot
1945    /// fixture, a `const`-context probe the future
1946    /// `feira validate --entry-exists` per-caixa verb consults) now
1947    /// reads through one const dispatch on the substrate primitive
1948    /// rather than being forced onto the runtime code path.
1949    #[must_use]
1950    pub const fn missing_entry(kind: &'static str, path: PathBuf) -> Self {
1951        Self::MissingEntry { kind, path }
1952    }
1953
1954    /// Construct a [`LayoutError::MissingLib`] naming the offending
1955    /// `caixa.nome()` and the resolved fallback `expected` path.
1956    ///
1957    /// Folds the uniform `Self::MissingLib { caixa: caixa.nome()
1958    /// .to_string(), expected }` two-slot struct-literal onto one
1959    /// substrate primitive so every [`StandardLayout::verify`] wire-up
1960    /// on this variant reads through one dispatch rather than the
1961    /// pre-lift open-coded struct-literal block, projecting the caixa
1962    /// slot through the paired [`crate::Caixa::nome`] accessor — the
1963    /// same discipline the sibling [`Self::missing_entry`]
1964    /// `{ kind, path }` ctor and the [`layout_nome_only_ctors!`]
1965    /// `{ caixa }`-only tuple-variant ctor family already take on the
1966    /// same envelope.
1967    #[must_use]
1968    pub fn missing_lib(caixa: &crate::Caixa, expected: PathBuf) -> Self {
1969        Self::MissingLib {
1970            caixa: caixa.nome().to_string(),
1971            expected,
1972        }
1973    }
1974
1975    /// Construct a [`LayoutError::CiOnNonAcao`] naming the offending
1976    /// `caixa.nome()` and its declared `:kind`.
1977    ///
1978    /// Folds the uniform `Self::CiOnNonAcao { caixa: caixa.nome()
1979    /// .to_string(), kind: caixa.kind() }` two-slot struct-literal
1980    /// onto one substrate primitive so every wire-up on this variant
1981    /// (today the sole [`crate::Caixa::validate_ci_kind_coherence`]
1982    /// call site at `caixa-core/src/manifest.rs:5586` — the deferred
1983    /// `caixa.pleme.io/v1alpha1/Caixa` CR admission webhook and a
1984    /// future `feira validate --ci-kind` per-caixa verb sit at the
1985    /// same axis) reads through one dispatch rather than the pre-lift
1986    /// open-coded struct-literal block. Both slots project through
1987    /// the paired [`crate::Caixa::nome`] / [`crate::Caixa::kind`]
1988    /// accessors — the same discipline every sibling ctor on this
1989    /// envelope ([`Self::missing_lib`] on `{ caixa, expected }`, the
1990    /// [`layout_nome_only_ctors!`] family on `{ caixa }`-only
1991    /// tuple-variants, the [`layout_slot_kind_ctors!`] family on
1992    /// `{ caixa, kind, slots }`) already takes.
1993    #[must_use]
1994    pub fn ci_on_non_acao(caixa: &crate::Caixa) -> Self {
1995        Self::CiOnNonAcao {
1996            caixa: caixa.nome().to_string(),
1997            kind: caixa.kind(),
1998        }
1999    }
2000}
2001
2002// Fold the six `LayoutError::<Variant>(caixa.nome().to_string())` nome-
2003// only tuple-variant wire-up sites at [`StandardLayout::verify`] onto one
2004// substrate primitive per typed variant — the fourth uniform-shape
2005// envelope on `LayoutError` after the `{ caixa, issue }` family the
2006// [`layout_violation_ctors!`] macro closed (131ca0d), the
2007// `{ caixa, kind, slots }` family the peer [`layout_slot_kind_ctors!`]
2008// macro closed (0419438), and the `{ kind, path }`
2009// [`LayoutError::missing_entry`] one-variant ctor (1b09f9d). Each of the
2010// six wire-up sites on this shape (`SupervisorOwnsCode` /
2011// `AplicacaoOwnsCode` / `AcaoOwnsCode` at the no-code kind-coherence gate;
2012// `BinarioWithoutExe` / `ServicoWithoutServicos` / `MissingCi` at the
2013// required-slot gate) opened the identical one-line
2014// `LayoutError::<Variant>(caixa.nome().to_string())` tuple-literal — the
2015// exact "same block re-inlined at every consumer" shape the PRIME
2016// DIRECTIVE names as a bug, on the same altitude the peer `_violation` /
2017// `_slots_on_non_*` / `missing_entry` families each closed on the
2018// sibling `LayoutError` envelopes.
2019//
2020// The macro below generates one static constructor per variant of shape
2021// `fn <slot>(caixa: &Caixa) -> LayoutError`, so every wire-up site
2022// collapses onto one dispatch:
2023// `return Err(LayoutError::<slot>(caixa));`, byte-equal to the pre-lift
2024// tuple-literal. The uniform one-field construction (`caixa: caixa.nome()
2025// .to_string()`) is spelled once — inside the macro — rather than at
2026// every wire-up site. Every constructor is `#[must_use]` so a caller who
2027// mistakenly discards the constructed error (rather than routing it
2028// through `return Err(…)`) trips a compile warning at the wire-up site.
2029//
2030// Peer with the three prior `LayoutError`-envelope constructor families
2031// — the four together now fold every uniform-shape `LayoutError` variant
2032// carried by [`StandardLayout::verify`] onto one substrate primitive per
2033// typed variant, so every layout-side error-wrap on `LayoutError` reads
2034// through one dispatch per variant rather than N open-coded blocks.
2035// Every future consumer that wants to construct one of these variants
2036// outside the layout pipeline (a per-slot admission webhook probing an
2037// Acao's `:ci` slot, a `feira validate --kind <X>` verb refusing a
2038// no-code kind that declares `:bibliotecas` / `:exe` / `:servicos`, an
2039// overlay resolver rejecting a required-slot omission against a
2040// cluster-local snapshot) reaches its variant through one call, matching
2041// the `_violation` / `_slots_on_non_*` / `missing_entry` families'
2042// substrate-primitive discipline.
2043macro_rules! layout_nome_only_ctors {
2044    ($($ctor:ident => $variant:ident),* $(,)?) => {
2045        impl LayoutError {
2046            $(
2047                #[doc = concat!(
2048                    "Construct a [`LayoutError::",
2049                    stringify!($variant),
2050                    "`] naming the offending `caixa.nome()`. Folds the ",
2051                    "uniform `Self::",
2052                    stringify!($variant),
2053                    "(caixa.nome().to_string())` one-field tuple-",
2054                    "literal onto one substrate primitive so every ",
2055                    "[`StandardLayout::verify`] wire-up on this variant ",
2056                    "reads through one dispatch rather than the pre-lift ",
2057                    "open-coded block."
2058                )]
2059                #[must_use]
2060                pub fn $ctor(caixa: &crate::Caixa) -> Self {
2061                    Self::$variant(caixa.nome().to_string())
2062                }
2063            )*
2064        }
2065    };
2066}
2067
2068layout_nome_only_ctors! {
2069    binario_without_exe => BinarioWithoutExe,
2070    servico_without_servicos => ServicoWithoutServicos,
2071    missing_ci => MissingCi,
2072    supervisor_owns_code => SupervisorOwnsCode,
2073    aplicacao_owns_code => AplicacaoOwnsCode,
2074    acao_owns_code => AcaoOwnsCode,
2075}
2076
2077#[cfg(test)]
2078mod tests {
2079    use super::*;
2080    use crate::{Caixa, CaixaKind};
2081    use std::path::PathBuf;
2082
2083    fn caixa(kind: CaixaKind) -> Caixa {
2084        Caixa {
2085            nome: "demo".into(),
2086            versao: "0.1.0".into(),
2087            kind,
2088            edicao: None,
2089            descricao: None,
2090            repositorio: None,
2091            licenca: None,
2092            autores: vec![],
2093            etiquetas: vec![],
2094            deps: vec![],
2095            deps_dev: vec![],
2096            exe: vec![],
2097            bibliotecas: vec![],
2098            servicos: vec![],
2099            // M2 typed-substrate slots default to absent.
2100            limits: None,
2101            behavior: None,
2102            upgrade_from: vec![],
2103            estrategia: None,
2104            max_restarts: None,
2105            restart_window: None,
2106            children: vec![],
2107            // M3 Aplicacao slots default to absent.
2108            membros: vec![],
2109            contratos: vec![],
2110            politicas: None,
2111            placement: None,
2112            entrada: None,
2113            ci: None,
2114        }
2115    }
2116
2117    #[test]
2118    fn missing_manifest_errors() {
2119        let layout = StandardLayout::new().with_path_exists(|_| false);
2120        let err = layout
2121            .verify(&caixa(CaixaKind::Biblioteca), Path::new("/tmp/x"))
2122            .unwrap_err();
2123        assert!(matches!(err, LayoutError::MissingManifest(_)));
2124    }
2125
2126    #[test]
2127    fn missing_manifest_ctor_matches_tuple_literal_wrap() {
2128        // Equivalence pin locking [`LayoutError::missing_manifest`] to
2129        // its tuple-literal peer under PartialEq. The pre-lift wire-up
2130        // at [`StandardLayout::verify`]'s top-of-body manifest-existence
2131        // gate read `LayoutError::MissingManifest(manifest)` — a two-
2132        // line open-coded tuple-literal against the resolved
2133        // `root.join("caixa.lisp")` `PathBuf`. The post-lift dispatch
2134        // reads `LayoutError::missing_manifest(manifest)`. Both must
2135        // produce byte-equal variants — a silent divergence (a
2136        // `.canonicalize()` slip on the ctor body, a stray `.clone()`
2137        // that duplicates the payload's underlying buffer, an
2138        // accidental `.into()` that reshapes the `PathBuf` bytes on
2139        // one side and not the other) surfaces here rather than at a
2140        // downstream diagnostic-shape drift far from the ctor
2141        // definition.
2142        //
2143        // Peer of the sibling
2144        // [`missing_entry_ctor_matches_struct_literal_wrap`] /
2145        // [`missing_lib_ctor_matches_struct_literal_wrap`] /
2146        // [`ci_on_non_acao_ctor_matches_struct_literal_wrap`] pins on
2147        // the same [`LayoutError`] envelope — closes the last
2148        // un-pinned inherent-ctor equivalence-against-open-coded-
2149        // literal pin on the envelope.
2150        let path = PathBuf::from("/tmp/x/caixa.lisp");
2151        assert_eq!(
2152            LayoutError::missing_manifest(path.clone()),
2153            LayoutError::MissingManifest(path),
2154            "missing_manifest ctor must byte-equal the pre-lift tuple-literal",
2155        );
2156    }
2157
2158    #[test]
2159    fn verify_missing_manifest_gate_routes_through_missing_manifest_ctor() {
2160        // Fail-before-pass-after routing pin: the pre-lift
2161        // [`StandardLayout::verify`] wire-up hand-rolled
2162        // `LayoutError::MissingManifest(manifest)` at its top-of-body
2163        // manifest-existence arm; the post-lift dispatch routes the
2164        // same arm through
2165        // [`LayoutError::missing_manifest`]. This pin sweeps a fixture
2166        // whose oracle refuses every path (`with_path_exists(|_| false)`
2167        // — the same shape [`missing_manifest_errors`] uses to prove
2168        // the gate fires at all) and asserts that the emitted
2169        // [`LayoutError`] equals the ctor-built error verbatim under
2170        // [`PartialEq`]. A future de-lift of this wire-up (a
2171        // hand-rolled `Self::MissingManifest(_)` reintroduced at the
2172        // gate, an intercept that unwraps the ctor's payload before
2173        // re-emitting) trips this pin at layout.rs build time rather
2174        // than surfacing as a drift-detection failure at a downstream
2175        // diagnostic-shape consumer far from the wire-up site.
2176        //
2177        // Same discipline the peer
2178        // [`ci_on_non_acao_ctor_matches_struct_literal_wrap`] /
2179        // [`missing_entry_ctor_routes_kind_through_arg_verbatim`] /
2180        // [`missing_lib_ctor_projects_nome_through_accessor`] pins on
2181        // the sibling [`LayoutError`] envelope's per-variant substrate-
2182        // primitive ctor family already carry — extended here onto
2183        // the last un-pinned wire-up-routing gate on the envelope.
2184        let root = Path::new("/tmp/routing-pin");
2185        let layout = StandardLayout::new().with_path_exists(|_| false);
2186        let err = layout
2187            .verify(&caixa(CaixaKind::Biblioteca), root)
2188            .unwrap_err();
2189        assert_eq!(
2190            err,
2191            LayoutError::missing_manifest(root.join("caixa.lisp")),
2192            "StandardLayout::verify's manifest-existence gate must route through \
2193             LayoutError::missing_manifest with the resolved `root.join(\"caixa.lisp\")` \
2194             payload verbatim",
2195        );
2196    }
2197
2198    // ── LayoutError::*_violation constructor family ──────────────────────
2199    //
2200    // The [`layout_violation_ctors!`] macro (below the `LayoutError` enum
2201    // definition) generates one static constructor per `*Violation { caixa,
2202    // issue }` variant that folds the uniform `{ caixa: caixa.nome()
2203    // .to_string(), issue: err.to_string() }` two-slot construction onto
2204    // one substrate primitive. The per-variant equivalence pins below
2205    // (fail-before-pass-after by construction — a byte-mismatched macro
2206    // arm would trip its equivalence pin first) lock each generated
2207    // constructor to its struct-literal peer under `PartialEq`, so every
2208    // wire-up in [`StandardLayout::verify`] on that variant produces a
2209    // byte-equal `LayoutError` to the pre-lift open-coded block. The
2210    // fixture caixa fires under `caixa("demo")` so the `caixa: "demo"`
2211    // half is pinned; the fixture error fires under a fixed `&str` so the
2212    // `issue: <literal>` half is pinned; the two together pin every field
2213    // of every generated variant.
2214
2215    fn layout_violation_ctor_fixture() -> (Caixa, &'static str) {
2216        (caixa(CaixaKind::Biblioteca), "sample issue text")
2217    }
2218
2219    // `assert_eq!` uses `PartialEq::eq(&self, &other)` under the hood, so
2220    // `actual`/`expected` are only ever read, never moved into anything —
2221    // the ergonomic tradeoff (owned + move-in vs. reference + &-borrow at
2222    // every call site) favors the owned form for a test-only assertion
2223    // helper called from 17 wire-up pins. The lint targets the general
2224    // API-shape case where callers still have downstream uses for the
2225    // moved value; the assertion helper terminates on the equality check.
2226    #[allow(clippy::needless_pass_by_value)]
2227    fn assert_violation_ctor_matches(actual: LayoutError, expected: LayoutError) {
2228        assert_eq!(
2229            actual, expected,
2230            "generated constructor must produce byte-equal LayoutError to open-coded struct-literal wrap",
2231        );
2232    }
2233
2234    #[test]
2235    fn nome_violation_ctor_matches_struct_literal_wrap() {
2236        let (c, issue) = layout_violation_ctor_fixture();
2237        assert_violation_ctor_matches(
2238            LayoutError::nome_violation(&c, issue),
2239            LayoutError::NomeViolation {
2240                caixa: c.nome().to_string(),
2241                issue: issue.to_string(),
2242            },
2243        );
2244    }
2245
2246    #[test]
2247    fn versao_violation_ctor_matches_struct_literal_wrap() {
2248        let (c, issue) = layout_violation_ctor_fixture();
2249        assert_violation_ctor_matches(
2250            LayoutError::versao_violation(&c, issue),
2251            LayoutError::VersaoViolation {
2252                caixa: c.nome().to_string(),
2253                issue: issue.to_string(),
2254            },
2255        );
2256    }
2257
2258    #[test]
2259    fn deps_violation_ctor_matches_struct_literal_wrap() {
2260        let (c, issue) = layout_violation_ctor_fixture();
2261        assert_violation_ctor_matches(
2262            LayoutError::deps_violation(&c, issue),
2263            LayoutError::DepsViolation {
2264                caixa: c.nome().to_string(),
2265                issue: issue.to_string(),
2266            },
2267        );
2268    }
2269
2270    #[test]
2271    fn etiquetas_violation_ctor_matches_struct_literal_wrap() {
2272        let (c, issue) = layout_violation_ctor_fixture();
2273        assert_violation_ctor_matches(
2274            LayoutError::etiquetas_violation(&c, issue),
2275            LayoutError::EtiquetasViolation {
2276                caixa: c.nome().to_string(),
2277                issue: issue.to_string(),
2278            },
2279        );
2280    }
2281
2282    #[test]
2283    fn autores_violation_ctor_matches_struct_literal_wrap() {
2284        let (c, issue) = layout_violation_ctor_fixture();
2285        assert_violation_ctor_matches(
2286            LayoutError::autores_violation(&c, issue),
2287            LayoutError::AutoresViolation {
2288                caixa: c.nome().to_string(),
2289                issue: issue.to_string(),
2290            },
2291        );
2292    }
2293
2294    #[test]
2295    fn repositorio_violation_ctor_matches_struct_literal_wrap() {
2296        let (c, issue) = layout_violation_ctor_fixture();
2297        assert_violation_ctor_matches(
2298            LayoutError::repositorio_violation(&c, issue),
2299            LayoutError::RepositorioViolation {
2300                caixa: c.nome().to_string(),
2301                issue: issue.to_string(),
2302            },
2303        );
2304    }
2305
2306    #[test]
2307    fn descricao_violation_ctor_matches_struct_literal_wrap() {
2308        let (c, issue) = layout_violation_ctor_fixture();
2309        assert_violation_ctor_matches(
2310            LayoutError::descricao_violation(&c, issue),
2311            LayoutError::DescricaoViolation {
2312                caixa: c.nome().to_string(),
2313                issue: issue.to_string(),
2314            },
2315        );
2316    }
2317
2318    #[test]
2319    fn licenca_violation_ctor_matches_struct_literal_wrap() {
2320        let (c, issue) = layout_violation_ctor_fixture();
2321        assert_violation_ctor_matches(
2322            LayoutError::licenca_violation(&c, issue),
2323            LayoutError::LicencaViolation {
2324                caixa: c.nome().to_string(),
2325                issue: issue.to_string(),
2326            },
2327        );
2328    }
2329
2330    #[test]
2331    fn edicao_violation_ctor_matches_struct_literal_wrap() {
2332        let (c, issue) = layout_violation_ctor_fixture();
2333        assert_violation_ctor_matches(
2334            LayoutError::edicao_violation(&c, issue),
2335            LayoutError::EdicaoViolation {
2336                caixa: c.nome().to_string(),
2337                issue: issue.to_string(),
2338            },
2339        );
2340    }
2341
2342    #[test]
2343    fn code_path_violation_ctor_matches_struct_literal_wrap() {
2344        let (c, issue) = layout_violation_ctor_fixture();
2345        assert_violation_ctor_matches(
2346            LayoutError::code_path_violation(&c, issue),
2347            LayoutError::CodePathViolation {
2348                caixa: c.nome().to_string(),
2349                issue: issue.to_string(),
2350            },
2351        );
2352    }
2353
2354    #[test]
2355    fn limits_violation_ctor_matches_struct_literal_wrap() {
2356        let (c, issue) = layout_violation_ctor_fixture();
2357        assert_violation_ctor_matches(
2358            LayoutError::limits_violation(&c, issue),
2359            LayoutError::LimitsViolation {
2360                caixa: c.nome().to_string(),
2361                issue: issue.to_string(),
2362            },
2363        );
2364    }
2365
2366    #[test]
2367    fn behavior_violation_ctor_matches_struct_literal_wrap() {
2368        let (c, issue) = layout_violation_ctor_fixture();
2369        assert_violation_ctor_matches(
2370            LayoutError::behavior_violation(&c, issue),
2371            LayoutError::BehaviorViolation {
2372                caixa: c.nome().to_string(),
2373                issue: issue.to_string(),
2374            },
2375        );
2376    }
2377
2378    #[test]
2379    fn upgrade_violation_ctor_matches_struct_literal_wrap() {
2380        let (c, issue) = layout_violation_ctor_fixture();
2381        assert_violation_ctor_matches(
2382            LayoutError::upgrade_violation(&c, issue),
2383            LayoutError::UpgradeViolation {
2384                caixa: c.nome().to_string(),
2385                issue: issue.to_string(),
2386            },
2387        );
2388    }
2389
2390    #[test]
2391    fn restart_window_violation_ctor_matches_struct_literal_wrap() {
2392        let (c, issue) = layout_violation_ctor_fixture();
2393        assert_violation_ctor_matches(
2394            LayoutError::restart_window_violation(&c, issue),
2395            LayoutError::RestartWindowViolation {
2396                caixa: c.nome().to_string(),
2397                issue: issue.to_string(),
2398            },
2399        );
2400    }
2401
2402    #[test]
2403    fn supervisor_violation_ctor_matches_struct_literal_wrap() {
2404        let (c, issue) = layout_violation_ctor_fixture();
2405        assert_violation_ctor_matches(
2406            LayoutError::supervisor_violation(&c, issue),
2407            LayoutError::SupervisorViolation {
2408                caixa: c.nome().to_string(),
2409                issue: issue.to_string(),
2410            },
2411        );
2412    }
2413
2414    #[test]
2415    fn aplicacao_violation_ctor_matches_struct_literal_wrap() {
2416        let (c, issue) = layout_violation_ctor_fixture();
2417        assert_violation_ctor_matches(
2418            LayoutError::aplicacao_violation(&c, issue),
2419            LayoutError::AplicacaoViolation {
2420                caixa: c.nome().to_string(),
2421                issue: issue.to_string(),
2422            },
2423        );
2424    }
2425
2426    #[test]
2427    fn acao_violation_ctor_matches_struct_literal_wrap() {
2428        // Sibling of [`aplicacao_violation_ctor_matches_struct_literal_wrap`]
2429        // / [`supervisor_violation_ctor_matches_struct_literal_wrap`] on
2430        // the third per-kind compound-shape wrap envelope on
2431        // `LayoutError`. Pins the macro-generated `acao_violation`
2432        // constructor to its struct-literal peer under `PartialEq`, so
2433        // every wire-up in [`StandardLayout::verify`] on the
2434        // [`LayoutError::AcaoViolation`] variant produces a byte-equal
2435        // `LayoutError` to the pre-lift open-coded block. Closes the
2436        // pin family the peer per-kind shape wraps already carry.
2437        let (c, issue) = layout_violation_ctor_fixture();
2438        assert_violation_ctor_matches(
2439            LayoutError::acao_violation(&c, issue),
2440            LayoutError::AcaoViolation {
2441                caixa: c.nome().to_string(),
2442                issue: issue.to_string(),
2443            },
2444        );
2445    }
2446
2447    #[test]
2448    fn violation_ctor_routes_issue_through_display_impl() {
2449        // Pin the fold's `issue = err.to_string()` half against any type
2450        // implementing `Display` — a per-arm error type from a foreign
2451        // module (here, `std::io::Error`) threads through byte-equal to
2452        // the struct-literal `.to_string()` construction, so the fold
2453        // does not silently collapse onto `&str`-only inputs.
2454        let c = caixa(CaixaKind::Biblioteca);
2455        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "sample io display source");
2456        let expected_issue = io_err.to_string();
2457        let actual = LayoutError::deps_violation(&c, &io_err);
2458        assert_eq!(
2459            actual,
2460            LayoutError::DepsViolation {
2461                caixa: c.nome().to_string(),
2462                issue: expected_issue,
2463            },
2464        );
2465    }
2466
2467    #[test]
2468    fn violation_ctor_routes_caixa_prefix_through_nome_accessor() {
2469        // Pin the fold's `caixa = caixa.nome().to_string()` half against
2470        // a non-default `:nome` — the accessor threads the caller's
2471        // `:nome` verbatim into the wrap envelope, so the fold does not
2472        // silently collapse onto the default `"demo"` fixture nome.
2473        let mut c = caixa(CaixaKind::Biblioteca);
2474        c.nome = "alt-nome".into();
2475        let actual = LayoutError::behavior_violation(&c, "sample issue");
2476        assert_eq!(
2477            actual,
2478            LayoutError::BehaviorViolation {
2479                caixa: "alt-nome".to_string(),
2480                issue: "sample issue".to_string(),
2481            },
2482        );
2483    }
2484
2485    // ── Caixa::run_layout_gate — per-slot gate + LayoutError wrap fold ───
2486    //
2487    // The [`Caixa::run_layout_gate`] substrate primitive folds the 18
2488    // self-similar `caixa.validate_<slot>().map_err(|err| LayoutError::
2489    // <slot>_violation(caixa, err))?;` wire-up sites at
2490    // [`StandardLayout::verify`] onto one dispatch. The pins below
2491    // (fail-before-pass-after by construction — a silent regression that
2492    // de-folded either arm would trip its own pin first) lock the two
2493    // arms of the fold under `PartialEq`:
2494    //
2495    //   - The `Ok(())` identity-element arm passes through verbatim (no
2496    //     wrap runs, no `caixa` closure capture).
2497    //   - The `Err(E)` arm routes through the caller-supplied `wrap`
2498    //     ctor with `self` bound as the caixa slot, byte-equal to the
2499    //     pre-lift `.map_err(|err| CTOR(caixa, err))` closure.
2500    //
2501    // The third pin (per-arm equivalence) runs the primitive with the
2502    // canonical `Caixa::validate_nome` validator and the paired
2503    // `LayoutError::nome_violation` ctor on a fixture whose `:nome`
2504    // fails `validate_nome`'s DNS-1123 gate ("Bad_Nome" — the uppercase
2505    // + underscore double footgun) and asserts the primitive's
2506    // `Result<(), LayoutError>` matches the open-coded pre-lift cascade
2507    // on the same fixture. A silent regression that de-folded the wrap
2508    // (dropped the `self` binding, threaded a stale caixa nome, swapped
2509    // the wrap ctor) would surface here as a mismatch between the two
2510    // dispatches.
2511
2512    #[test]
2513    fn run_layout_gate_ok_arm_passes_through() {
2514        // Positive control on the identity-element arm: a gate returning
2515        // `Ok(())` short-circuits before the wrap runs, so the caller
2516        // receives `Ok(())` verbatim regardless of what the paired ctor
2517        // would have produced. Pins the fold's `Result::map_err`
2518        // short-circuit semantics — a regression that unconditionally
2519        // wrapped (e.g. always called the ctor) would trip here.
2520        let c = caixa(CaixaKind::Biblioteca);
2521        let result: Result<(), LayoutError> = c.run_layout_gate(
2522            |_c: &Caixa| Ok::<(), &'static str>(()),
2523            |_c: &Caixa, _err: &'static str| {
2524                panic!("wrap must not run on the Ok(()) arm of the fold")
2525            },
2526        );
2527        assert!(
2528            result.is_ok(),
2529            "run_layout_gate must pass Ok(()) through verbatim, got {result:?}"
2530        );
2531    }
2532
2533    #[test]
2534    fn run_layout_gate_err_arm_wraps_via_ctor() {
2535        // Positive control on the err arm: a gate returning `Err(E)`
2536        // threads `E` into the caller-supplied `wrap` ctor with `self`
2537        // bound as the caixa argument. Uses a synthetic `&'static str`
2538        // error and the canonical `LayoutError::deps_violation` ctor so
2539        // the pin covers the fold's two-callable dispatch shape without
2540        // depending on any per-slot validator's specific arm sweep.
2541        let c = caixa(CaixaKind::Biblioteca);
2542        let sample_reason = "sample gate reason";
2543        let result = c.run_layout_gate(
2544            |_c: &Caixa| Err::<(), &'static str>(sample_reason),
2545            LayoutError::deps_violation,
2546        );
2547        let err = result.expect_err("Err arm must reach the caller");
2548        assert_eq!(
2549            err,
2550            LayoutError::DepsViolation {
2551                caixa: c.nome().to_string(),
2552                issue: sample_reason.to_string(),
2553            },
2554            "run_layout_gate must wrap the gate's Err via the caller-supplied \
2555             ctor with `self` bound as the caixa slot"
2556        );
2557    }
2558
2559    #[test]
2560    fn run_layout_gate_folds_arm_matches_gate() {
2561        // Fail-before-pass-after per-arm equivalence pin on the err arm:
2562        // a fixture whose `:nome` fails `validate_nome`'s DNS-1123 gate
2563        // (`"Bad_Nome"` — uppercase + underscore double footgun the
2564        // [`crate::ManifestError::NomeInvalid`] arm rejects) surfaces
2565        // the same `LayoutError::NomeViolation` byte-equal through both
2566        // the primitive `Caixa::run_layout_gate(Caixa::validate_nome,
2567        // LayoutError::nome_violation)` and the open-coded pre-lift
2568        // cascade `Caixa::validate_nome().map_err(|err|
2569        // LayoutError::nome_violation(&c, err))` on the same Caixa
2570        // fixture. Pins the fold — a silent regression that de-folded
2571        // either arm (dropped the `self` binding, threaded a stale
2572        // caixa nome, swapped the wrap ctor) would surface here as a
2573        // mismatch between the two dispatches.
2574        let mut c = caixa(CaixaKind::Biblioteca);
2575        c.nome = "Bad_Nome".into();
2576        let via_primitive = c
2577            .run_layout_gate(Caixa::validate_nome, LayoutError::nome_violation)
2578            .expect_err("Bad_Nome must fail validate_nome");
2579        let via_open_coded = c
2580            .validate_nome()
2581            .map_err(|err| LayoutError::nome_violation(&c, err))
2582            .expect_err("Bad_Nome must fail validate_nome");
2583        assert_eq!(
2584            via_primitive, via_open_coded,
2585            "Caixa::run_layout_gate must surface the err-arm diagnostic \
2586             byte-equal to the open-coded `.map_err(|err| CTOR(caixa, err))` \
2587             cascade on the same Caixa fixture"
2588        );
2589        assert!(
2590            matches!(via_primitive, LayoutError::NomeViolation { .. }),
2591            "expected NomeViolation on Bad_Nome, got {via_primitive:?}"
2592        );
2593    }
2594
2595    // ── LayoutError kind-coherence constructor family ────────────────────
2596    //
2597    // The [`layout_slot_kind_ctors!`] macro (sibling of
2598    // [`layout_violation_ctors!`] beside the `LayoutError` enum definition)
2599    // generates one static constructor per `*SlotsOn*` / `ForeignCodeSlot`
2600    // variant that folds the uniform `{ caixa: caixa.nome().to_string(),
2601    // kind: caixa.kind(), slots: slots.join(" ") }` four-field construction
2602    // onto one substrate primitive. The per-variant equivalence pins below
2603    // (fail-before-pass-after by construction — a byte-mismatched macro arm
2604    // would trip its equivalence pin first) lock each generated constructor
2605    // to its struct-literal peer under `PartialEq`, so every wire-up in
2606    // [`StandardLayout::verify`] on that variant produces a byte-equal
2607    // `LayoutError` to the pre-lift open-coded block. The three cross-axis
2608    // pins that follow (non-default `:nome`, non-default kind, non-trivial
2609    // slots list) route each of the three constructor input axes through
2610    // its declared accessor / arg, so the fold does not silently collapse
2611    // onto a fixture default on any axis.
2612
2613    fn layout_slot_kind_ctor_fixture() -> (Caixa, Vec<&'static str>) {
2614        (caixa(CaixaKind::Biblioteca), vec![":membros", ":contratos"])
2615    }
2616
2617    // Same rationale as `assert_violation_ctor_matches` above: the helper
2618    // terminates on the equality check, so the owned-arg lint's general
2619    // API-shape target does not apply.
2620    #[allow(clippy::needless_pass_by_value)]
2621    fn assert_slot_kind_ctor_matches(actual: LayoutError, expected: LayoutError) {
2622        assert_eq!(
2623            actual, expected,
2624            "generated constructor must produce byte-equal LayoutError to open-coded struct-literal wrap",
2625        );
2626    }
2627
2628    #[test]
2629    fn mesh_slots_on_non_aplicacao_ctor_matches_struct_literal_wrap() {
2630        let (c, slots) = layout_slot_kind_ctor_fixture();
2631        assert_slot_kind_ctor_matches(
2632            LayoutError::mesh_slots_on_non_aplicacao(&c, slots.clone()),
2633            LayoutError::MeshSlotsOnNonAplicacao {
2634                caixa: c.nome().to_string(),
2635                kind: c.kind(),
2636                slots: slots.join(" "),
2637            },
2638        );
2639    }
2640
2641    #[test]
2642    fn supervisor_slots_on_non_supervisor_ctor_matches_struct_literal_wrap() {
2643        let (c, slots) = layout_slot_kind_ctor_fixture();
2644        assert_slot_kind_ctor_matches(
2645            LayoutError::supervisor_slots_on_non_supervisor(&c, slots.clone()),
2646            LayoutError::SupervisorSlotsOnNonSupervisor {
2647                caixa: c.nome().to_string(),
2648                kind: c.kind(),
2649                slots: slots.join(" "),
2650            },
2651        );
2652    }
2653
2654    #[test]
2655    fn servico_slots_on_non_servico_ctor_matches_struct_literal_wrap() {
2656        let (c, slots) = layout_slot_kind_ctor_fixture();
2657        assert_slot_kind_ctor_matches(
2658            LayoutError::servico_slots_on_non_servico(&c, slots.clone()),
2659            LayoutError::ServicoSlotsOnNonServico {
2660                caixa: c.nome().to_string(),
2661                kind: c.kind(),
2662                slots: slots.join(" "),
2663            },
2664        );
2665    }
2666
2667    #[test]
2668    fn foreign_code_slot_ctor_matches_struct_literal_wrap() {
2669        let (c, slots) = layout_slot_kind_ctor_fixture();
2670        assert_slot_kind_ctor_matches(
2671            LayoutError::foreign_code_slot(&c, slots.clone()),
2672            LayoutError::ForeignCodeSlot {
2673                caixa: c.nome().to_string(),
2674                kind: c.kind(),
2675                slots: slots.join(" "),
2676            },
2677        );
2678    }
2679
2680    #[test]
2681    fn slot_kind_ctor_routes_caixa_prefix_through_nome_accessor() {
2682        // Pin the fold's `caixa = caixa.nome().to_string()` half against a
2683        // non-default `:nome` — the accessor threads the caller's `:nome`
2684        // verbatim into the wrap envelope, so the fold does not silently
2685        // collapse onto the default `"demo"` fixture nome. Peer of the
2686        // sibling `violation_ctor_routes_caixa_prefix_through_nome_accessor`
2687        // pin on the `{ caixa, issue }` envelope; extended here onto the
2688        // `{ caixa, kind, slots }` envelope so both `LayoutError`-shape
2689        // constructor families guarantee the `:nome`-derived-caixa slot
2690        // routes through [`Caixa::nome`] rather than a hard-coded string.
2691        let mut c = caixa(CaixaKind::Biblioteca);
2692        c.nome = "alt-nome".into();
2693        let actual = LayoutError::mesh_slots_on_non_aplicacao(&c, vec![":membros"]);
2694        assert_eq!(
2695            actual,
2696            LayoutError::MeshSlotsOnNonAplicacao {
2697                caixa: "alt-nome".to_string(),
2698                kind: CaixaKind::Biblioteca,
2699                slots: ":membros".to_string(),
2700            },
2701        );
2702    }
2703
2704    #[test]
2705    fn slot_kind_ctor_routes_kind_through_caixa_kind_accessor() {
2706        // Pin the fold's `kind = caixa.kind()` half against a non-default
2707        // kind — the accessor threads the caller's `:kind` verbatim into
2708        // the wrap envelope, so the fold does not silently collapse onto
2709        // one hard-coded kind. Sweeps every non-Aplicacao / non-Supervisor
2710        // / non-Servico kind the corresponding gate can fire on so the
2711        // pin covers the kind-derivation axis on every downstream variant.
2712        for kind in [
2713            CaixaKind::Biblioteca,
2714            CaixaKind::Binario,
2715            CaixaKind::Servico,
2716            CaixaKind::Supervisor,
2717            CaixaKind::Aplicacao,
2718            CaixaKind::Acao,
2719        ] {
2720            let c = caixa(kind);
2721            let actual = LayoutError::foreign_code_slot(&c, vec![":exe"]);
2722            assert_eq!(
2723                actual,
2724                LayoutError::ForeignCodeSlot {
2725                    caixa: c.nome().to_string(),
2726                    kind,
2727                    slots: ":exe".to_string(),
2728                },
2729                "foreign_code_slot ctor must thread `caixa.kind()` verbatim on every kind",
2730            );
2731        }
2732    }
2733
2734    #[test]
2735    fn slot_kind_ctor_routes_slots_through_join_separator() {
2736        // Pin the fold's `slots = slots.join(" ")` half against a
2737        // multi-entry slots list — the join threads exactly one ASCII
2738        // space between entries, in caller-supplied order, so the fold
2739        // does not silently collapse onto a fixed separator (`", "`, `";
2740        // "`, `"\n"`), a sorted order, or a single-entry pass-through.
2741        // Uses the M2 servico-slot vocabulary since these are what the
2742        // corresponding `servico_slots_on_non_servico` gate reports.
2743        let c = caixa(CaixaKind::Biblioteca);
2744        let actual = LayoutError::servico_slots_on_non_servico(
2745            &c,
2746            vec![":limits", ":behavior", ":upgrade-from"],
2747        );
2748        assert_eq!(
2749            actual,
2750            LayoutError::ServicoSlotsOnNonServico {
2751                caixa: c.nome().to_string(),
2752                kind: c.kind(),
2753                slots: ":limits :behavior :upgrade-from".to_string(),
2754            },
2755        );
2756    }
2757
2758    // ── LayoutError::missing_entry substrate-primitive constructor ───────
2759    //
2760    // The [`LayoutError::missing_entry`] constructor beside the enum
2761    // definition folds the `{ kind: &'static str, path: PathBuf }`
2762    // uniform-shape envelope onto one substrate primitive — the third
2763    // and last uniform-shape envelope on `LayoutError` after the
2764    // `{ caixa, issue }` family the [`layout_violation_ctors!`] macro
2765    // closed (131ca0d) and the `{ caixa, kind, slots }` family the peer
2766    // [`layout_slot_kind_ctors!`] macro closed (0419438). The pins below
2767    // (fail-before-pass-after by construction — a byte-mismatched
2768    // constructor arm would trip its equivalence pin first) lock the
2769    // constructor to its struct-literal peer under `PartialEq`, so every
2770    // wire-up in [`StandardLayout::verify`] on this variant produces a
2771    // byte-equal `LayoutError` to the pre-lift open-coded block. The two
2772    // cross-axis pins that follow (canonical-kind-label sweep, non-
2773    // default path) route each of the two constructor input axes through
2774    // its arg verbatim, so the fold does not silently collapse onto a
2775    // fixture default on either axis.
2776
2777    #[test]
2778    fn missing_entry_ctor_matches_struct_literal_wrap() {
2779        // Per-envelope equivalence pin — the `missing_entry` constructor
2780        // produces a `LayoutError::MissingEntry` byte-equal under
2781        // `PartialEq` to the open-coded four-line struct-literal wrap on
2782        // the same `(kind, path)` fixture. Peer of the sibling
2783        // `<slot>_violation_ctor_matches_struct_literal_wrap` /
2784        // `<slot>_slots_on_non_<owner>_ctor_matches_struct_literal_wrap`
2785        // pins on the two prior uniform-shape envelopes on the same
2786        // `LayoutError`.
2787        let path = PathBuf::from("/tmp/x/lib/demo.lisp");
2788        assert_eq!(
2789            LayoutError::missing_entry(
2790                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2791                path.clone(),
2792            ),
2793            LayoutError::MissingEntry {
2794                kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2795                path,
2796            },
2797        );
2798    }
2799
2800    #[test]
2801    fn missing_entry_ctor_routes_kind_through_arg_verbatim() {
2802        // Pin the fold's `kind: &'static str` arg through every canonical
2803        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] label the five
2804        // wire-up sites in [`StandardLayout::verify`] pass — so the fold
2805        // does not silently collapse onto one hard-coded label. Sweep
2806        // matches the arm set the peer
2807        // `layout_missing_entry_kind_m2_consts_pin_canonical_kebab_case_labels`
2808        // pin (below) covers on the const-label declarations.
2809        let path = PathBuf::from("/tmp/x/entry");
2810        for kind in [
2811            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2812            crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2813            crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2814            crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
2815            crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
2816        ] {
2817            assert_eq!(
2818                LayoutError::missing_entry(kind, path.clone()),
2819                LayoutError::MissingEntry {
2820                    kind,
2821                    path: path.clone(),
2822                },
2823                "missing_entry ctor must thread `kind` verbatim on every canonical label",
2824            );
2825        }
2826    }
2827
2828    #[test]
2829    fn missing_entry_ctor_routes_path_through_arg_verbatim() {
2830        // Pin the fold's `path: PathBuf` arg against a non-default,
2831        // multi-component `PathBuf` — the ctor threads the caller's
2832        // `PathBuf` verbatim into the wrap envelope, so the fold does
2833        // not silently collapse onto a fixed component prefix, a
2834        // canonicalized form, or a single-component pass-through.
2835        let path = PathBuf::from("/alt/root")
2836            .join("servicos")
2837            .join("hello-rio.computeunit.yaml");
2838        assert_eq!(
2839            LayoutError::missing_entry(
2840                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2841                path.clone(),
2842            ),
2843            LayoutError::MissingEntry {
2844                kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2845                path,
2846            },
2847        );
2848    }
2849
2850    #[test]
2851    fn missing_entry_ctor_is_const_fn_usable_in_const_position() {
2852        // Fail-before-pass-after pin on [`LayoutError::missing_entry`]'s
2853        // `pub const fn` shape — a future accidental downgrade to a
2854        // non-`const` `pub fn` (a body reversion that reaches for a
2855        // `.to_string()` / `.into()` / accessor projection on either
2856        // arg, matching the sibling [`LayoutError::missing_lib`] /
2857        // [`LayoutError::ci_on_non_acao`] non-const-body shape) trips
2858        // here at caixa-core build time with "cannot call non-const fn
2859        // `LayoutError::missing_entry` in constants" rather than
2860        // surfacing as a downstream `const`-context regression at a
2861        // future M4 CR materializer's admission-webhook per-kind
2862        // rejection-body lookup table, a compile-time per-arm
2863        // diagnostic registry the LSP hover renderer materializes, or
2864        // a `const`-context probe the future `feira validate
2865        // --entry-exists` per-caixa verb consults.
2866        //
2867        // Peer of the sibling
2868        // [`crate::behavior::tests::behavior_spec_is_empty_is_const_fn_usable_in_const_position`]
2869        // (ba644e7 — `pub const fn` widening of the M2 `:behavior`
2870        // emptiness predicate) and the sibling per-envelope pins on
2871        // the [`crate::supervisor::RestartStrategy::as_str`] /
2872        // [`crate::supervisor::RestartPolicy::as_str`] /
2873        // [`crate::aplicacao::PlacementStrategy::as_str`] const-fn
2874        // widenings — extends the same "one substrate primitive per
2875        // pure-struct-literal ctor stays `pub const fn` so every
2876        // downstream const-context consumer routes through one const
2877        // dispatch" discipline onto the last un-pinned pure-struct-
2878        // literal ctor on the [`LayoutError`] envelope's path-carrying
2879        // axis (the sibling [`Self::missing_manifest`] already carries
2880        // this posture; [`Self::missing_lib`] / [`Self::ci_on_non_acao`]
2881        // stay non-const by design because their bodies project through
2882        // the paired [`crate::Caixa::nome`] `String → String` accessor
2883        // which is not `const`-stable).
2884        const MISSING_BIBLIOTECA: LayoutError = LayoutError::missing_entry(
2885            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2886            PathBuf::new(),
2887        );
2888        assert_eq!(
2889            MISSING_BIBLIOTECA,
2890            LayoutError::MissingEntry {
2891                kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2892                path: PathBuf::new(),
2893            },
2894            "missing_entry const-position dispatch must byte-equal the runtime-position \
2895             struct-literal wrap on the same (kind, path) fixture",
2896        );
2897    }
2898
2899    // ── StandardLayout::probe_declared_entry substrate primitive ─────────
2900    //
2901    // The [`StandardLayout::probe_declared_entry`] method
2902    // (`layout.rs`) folds the five self-similar
2903    // `let full = root.join(p); if !self.exists(&full) { return
2904    // Err(LayoutError::missing_entry(<kind>, full)); }` existence-probe
2905    // blocks at [`StandardLayout::verify`] (`:bibliotecas` iteration,
2906    // `:exe` iteration, `:servicos` iteration, `:behavior` on-disk
2907    // callback-path iteration, `:upgrade-from` per-instruction script-
2908    // path iteration) onto one substrate primitive. The pins below
2909    // (fail-before-pass-after by construction — a byte-mismatched
2910    // primitive body would trip its equivalence pin first) lock the
2911    // fold to its pre-lift open-coded shape under `PartialEq`, so every
2912    // wire-up in [`StandardLayout::verify`] on this primitive produces
2913    // a byte-equal `LayoutError` on miss and a byte-equal `PathBuf` on
2914    // hit.
2915
2916    #[test]
2917    fn probe_declared_entry_folds_miss_returns_missing_entry() {
2918        // Per-primitive equivalence pin on the miss arm — a
2919        // [`StandardLayout`] whose oracle returns `false` for every
2920        // path yields a `MissingEntry` byte-equal under `PartialEq` to
2921        // the open-coded `LayoutError::missing_entry(<kind>,
2922        // root.join(path))` wrap the pre-lift block carried at each of
2923        // the five wire-up sites. Peer of the sibling
2924        // `missing_entry_ctor_matches_struct_literal_wrap` pin on the
2925        // constructor's own byte-equal shape.
2926        let layout = StandardLayout::new().with_path_exists(|_| false);
2927        let root = PathBuf::from("/tmp/x");
2928        let path = Path::new("lib/demo.lisp");
2929        let err = layout
2930            .probe_declared_entry(
2931                path,
2932                &root,
2933                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2934            )
2935            .unwrap_err();
2936        assert_eq!(
2937            err,
2938            LayoutError::missing_entry(
2939                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2940                root.join(path),
2941            ),
2942            "probe_declared_entry miss arm must produce byte-equal LayoutError to \
2943             open-coded `missing_entry(<kind>, root.join(path))` wrap",
2944        );
2945    }
2946
2947    #[test]
2948    fn probe_declared_entry_folds_hit_returns_resolved_full() {
2949        // Per-primitive equivalence pin on the hit arm — a
2950        // [`StandardLayout`] whose oracle returns `true` for the probed
2951        // resolved path yields `Ok(root.join(path))` byte-equal under
2952        // `PartialEq`. The pre-lift `:exe` / `:servicos` wire-up sites
2953        // needed the resolved `full` for the follow-up sandbox-directory-
2954        // containment check; the fold preserves that hand-off through
2955        // the primitive's `Ok(PathBuf)` return arm rather than
2956        // re-computing `root.join(path)` at the follow-up gate.
2957        let root = PathBuf::from("/tmp/x");
2958        let path = Path::new("exe/tool.lisp");
2959        let full = root.join(path);
2960        let full_probe = full.clone();
2961        let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
2962        let resolved = layout
2963            .probe_declared_entry(path, &root, crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE)
2964            .expect("probe_declared_entry must return Ok(root.join(path)) when the oracle hits");
2965        assert_eq!(
2966            resolved, full,
2967            "probe_declared_entry hit arm must return the resolved `root.join(path)` \
2968             byte-equal so the `:exe` / `:servicos` sandbox-containment follow-up \
2969             reads it verbatim without re-computing",
2970        );
2971    }
2972
2973    #[test]
2974    fn probe_declared_entry_threads_kind_through_arg_verbatim() {
2975        // Cross-axis pin — sweep every canonical
2976        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] label the five
2977        // wire-up sites in [`StandardLayout::verify`] pass. Each miss
2978        // must return a `MissingEntry` whose `kind:` field byte-equals
2979        // the caller-provided arg, so the fold does not silently
2980        // collapse onto one hard-coded label. Sweep matches the arm
2981        // set the peer
2982        // `missing_entry_ctor_routes_kind_through_arg_verbatim` pin
2983        // covers on the constructor arg.
2984        let layout = StandardLayout::new().with_path_exists(|_| false);
2985        let root = PathBuf::from("/tmp/x");
2986        let path = Path::new("some/entry");
2987        for kind in [
2988            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2989            crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2990            crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2991            crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
2992            crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
2993        ] {
2994            let err = layout.probe_declared_entry(path, &root, kind).unwrap_err();
2995            assert_eq!(
2996                err,
2997                LayoutError::missing_entry(kind, root.join(path)),
2998                "probe_declared_entry must thread `kind` verbatim on every canonical label",
2999            );
3000        }
3001    }
3002
3003    #[test]
3004    fn probe_declared_entry_threads_path_through_arg_verbatim() {
3005        // Cross-axis pin — the primitive must compose the `path` arg
3006        // through `root.join` verbatim on the miss arm's `MissingEntry
3007        // { path: … }` field, so the fold does not silently collapse
3008        // onto a fixed component prefix, a canonicalized form, or a
3009        // hand-authored `root.join(<literal>)`. Sweep two multi-
3010        // component `Path` fixtures (one under `servicos/`, one under
3011        // `lib/`) so a byte-drifted composition on either axis would
3012        // trip.
3013        let layout = StandardLayout::new().with_path_exists(|_| false);
3014        let root = PathBuf::from("/alt/root");
3015        for path in [
3016            Path::new("servicos/hello-rio.computeunit.yaml"),
3017            Path::new("lib/demo.lisp"),
3018        ] {
3019            let err = layout
3020                .probe_declared_entry(
3021                    path,
3022                    &root,
3023                    crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3024                )
3025                .unwrap_err();
3026            assert_eq!(
3027                err,
3028                LayoutError::missing_entry(
3029                    crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3030                    root.join(path),
3031                ),
3032                "probe_declared_entry must compose `path` through `root.join` verbatim",
3033            );
3034        }
3035    }
3036
3037    #[test]
3038    fn probe_declared_entry_routes_through_configurable_exists_oracle() {
3039        // Cross-axis pin — the primitive must consult the injected
3040        // [`StandardLayout::with_path_exists`] oracle, not the ambient
3041        // `Path::exists` filesystem probe. Configure a per-path
3042        // discriminator that returns `true` only for one canonical
3043        // resolved path and assert both arms:
3044        // - the "hit" path resolves to `Ok(full)` byte-equal
3045        // - every other path resolves to `MissingEntry` on the same
3046        //   [`StandardLayout`] instance
3047        // The pin traps a regression that reroutes the primitive off
3048        // the injected oracle onto the ambient `Path::exists` (which
3049        // would silently return `false` for every path in `/tmp/x/…`
3050        // and mask the miss-arm hand-off on the hit fixture, or
3051        // silently return `true` for a real system path and mask the
3052        // hit-arm hand-off on the miss fixture).
3053        let root = PathBuf::from("/tmp/x");
3054        let hit_path = Path::new("servicos/keep.computeunit.yaml");
3055        let miss_path = Path::new("servicos/drop.computeunit.yaml");
3056        let hit_full = root.join(hit_path);
3057        let hit_full_probe = hit_full.clone();
3058        let layout = StandardLayout::new().with_path_exists(move |p| p == hit_full_probe);
3059
3060        let resolved = layout
3061            .probe_declared_entry(
3062                hit_path,
3063                &root,
3064                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3065            )
3066            .expect("probe_declared_entry must consult the injected oracle on the hit arm");
3067        assert_eq!(
3068            resolved, hit_full,
3069            "probe_declared_entry hit arm must return the oracle-approved resolved path",
3070        );
3071
3072        let err = layout
3073            .probe_declared_entry(
3074                miss_path,
3075                &root,
3076                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3077            )
3078            .unwrap_err();
3079        assert_eq!(
3080            err,
3081            LayoutError::missing_entry(
3082                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3083                root.join(miss_path),
3084            ),
3085            "probe_declared_entry miss arm must fire when the injected oracle rejects the path",
3086        );
3087    }
3088
3089    // ── StandardLayout::probe_sandboxed_declared_entry substrate primitive ─
3090    //
3091    // The [`StandardLayout::probe_sandboxed_declared_entry`] method
3092    // (`layout.rs`) folds the two self-similar `let full = self.
3093    // probe_declared_entry(p, root, <kind>)?; if !full.starts_with(&<slot>_dir)
3094    // { return Err(LayoutError::<Slot>OutsideDir(full)); }` blocks at
3095    // [`StandardLayout::verify`] (`:exe` iteration, `:servicos` iteration)
3096    // onto one substrate primitive. The pins below (fail-before-pass-after
3097    // by construction — a byte-mismatched primitive body would trip its
3098    // equivalence pin first) lock the fold to its pre-lift open-coded
3099    // shape under `PartialEq`, so every wire-up in
3100    // [`StandardLayout::verify`] on this primitive produces a byte-equal
3101    // `LayoutError` on miss / sandbox-escape and a byte-equal `PathBuf`
3102    // on hit.
3103
3104    #[test]
3105    fn probe_sandboxed_declared_entry_folds_miss_returns_missing_entry() {
3106        // Per-primitive equivalence pin on the miss arm — a
3107        // [`StandardLayout`] whose oracle returns `false` for every
3108        // path yields a `MissingEntry` byte-equal under `PartialEq` to
3109        // the open-coded `LayoutError::missing_entry(<kind>,
3110        // root.join(path))` wrap the sibling
3111        // [`StandardLayout::probe_declared_entry`] primitive routes
3112        // through. Diagnostic-order pin: `MissingEntry` outranks the
3113        // `outside_ctor` sandbox-escape arm on the same iteration, so
3114        // an entry that is both absent *and* outside the sandbox fires
3115        // the `MissingEntry` diagnostic (the pre-lift order the two
3116        // wire-up sites carried).
3117        let layout = StandardLayout::new().with_path_exists(|_| false);
3118        let root = PathBuf::from("/tmp/x");
3119        let path = Path::new("lib/tool");
3120        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3121        let err = layout
3122            .probe_sandboxed_declared_entry(
3123                path,
3124                &root,
3125                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3126                &exe_dir,
3127                LayoutError::ExeOutsideDir,
3128            )
3129            .unwrap_err();
3130        assert_eq!(
3131            err,
3132            LayoutError::missing_entry(
3133                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3134                root.join(path),
3135            ),
3136            "probe_sandboxed_declared_entry miss arm must produce byte-equal \
3137             LayoutError to the sibling probe_declared_entry primitive's miss wrap, \
3138             preserving the pre-lift MissingEntry-before-<Slot>OutsideDir diagnostic order",
3139        );
3140    }
3141
3142    #[test]
3143    fn probe_sandboxed_declared_entry_folds_sandbox_escape_via_outside_ctor() {
3144        // Per-primitive equivalence pin on the sandbox-escape arm — a
3145        // [`StandardLayout`] whose oracle admits the probed resolved
3146        // path (so the miss arm passes) but whose resolved path lies
3147        // outside the caller-provided `sandbox_dir` yields the paired
3148        // `outside_ctor(full)` byte-equal under `PartialEq`. The
3149        // primitive threads the resolved `full` through the caller-
3150        // supplied `fn(PathBuf) -> LayoutError` constructor rather
3151        // than a hard-coded variant, so the fold does not silently
3152        // collapse onto one of the two `:exe` / `:servicos` outside-
3153        // dir variants.
3154        let root = PathBuf::from("/tmp/x");
3155        let path = Path::new("lib/tool");
3156        let full = root.join(path);
3157        let full_probe = full.clone();
3158        let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3159        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3160        let err = layout
3161            .probe_sandboxed_declared_entry(
3162                path,
3163                &root,
3164                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3165                &exe_dir,
3166                LayoutError::ExeOutsideDir,
3167            )
3168            .unwrap_err();
3169        assert_eq!(
3170            err,
3171            LayoutError::ExeOutsideDir(full),
3172            "probe_sandboxed_declared_entry sandbox-escape arm must route the \
3173             resolved `full` through the caller-supplied outside_ctor byte-equal to \
3174             the pre-lift `LayoutError::ExeOutsideDir(full)` tuple-literal",
3175        );
3176    }
3177
3178    #[test]
3179    fn probe_sandboxed_declared_entry_folds_hit_returns_resolved_full() {
3180        // Per-primitive equivalence pin on the hit arm — a
3181        // [`StandardLayout`] whose oracle admits the probed path *and*
3182        // whose resolved path lives inside `sandbox_dir` yields
3183        // `Ok(root.join(path))` byte-equal under `PartialEq`. The
3184        // pre-lift wire-ups discarded the resolved `Ok(PathBuf)` since
3185        // no follow-up per-path gate consumes it after the sandbox
3186        // check; the fold preserves the same hit-arm hand-off through
3187        // the primitive's `Ok(PathBuf)` return so a future consumer
3188        // that wants to run a per-path successor gate reaches the
3189        // resolved path without re-computing `root.join(path)`.
3190        let root = PathBuf::from("/tmp/x");
3191        let path = Path::new("exe/tool");
3192        let full = root.join(path);
3193        let full_probe = full.clone();
3194        let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3195        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3196        let resolved = layout
3197            .probe_sandboxed_declared_entry(
3198                path,
3199                &root,
3200                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3201                &exe_dir,
3202                LayoutError::ExeOutsideDir,
3203            )
3204            .expect(
3205                "probe_sandboxed_declared_entry must return Ok(root.join(path)) \
3206                 when the oracle admits the path and it lives inside sandbox_dir",
3207            );
3208        assert_eq!(
3209            resolved, full,
3210            "probe_sandboxed_declared_entry hit arm must return the resolved \
3211             `root.join(path)` byte-equal so a future per-path successor gate \
3212             reaches it without re-computing",
3213        );
3214    }
3215
3216    #[test]
3217    fn probe_sandboxed_declared_entry_threads_outside_ctor_through_arg_verbatim() {
3218        // Cross-axis pin — the primitive must thread the caller-
3219        // supplied `outside_ctor` verbatim into the sandbox-escape
3220        // arm's `LayoutError` return, so the fold does not silently
3221        // collapse onto one hard-coded variant. Sweep both
3222        // [`LayoutError`] tuple-variants the two wire-up sites in
3223        // [`StandardLayout::verify`] pass — [`LayoutError::ExeOutsideDir`]
3224        // and [`LayoutError::ServicoOutsideDir`] — so a byte-drifted
3225        // ctor-routing on either axis would trip.
3226        let root = PathBuf::from("/tmp/x");
3227        let outside_dir = root.join(crate::render::LAYOUT_DIR_LIB);
3228        for (kind, sandbox_component, ctor, expected_variant) in [
3229            (
3230                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3231                crate::render::LAYOUT_DIR_EXE,
3232                LayoutError::ExeOutsideDir as fn(PathBuf) -> LayoutError,
3233                LayoutError::ExeOutsideDir as fn(PathBuf) -> LayoutError,
3234            ),
3235            (
3236                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3237                crate::render::LAYOUT_DIR_SERVICOS,
3238                LayoutError::ServicoOutsideDir as fn(PathBuf) -> LayoutError,
3239                LayoutError::ServicoOutsideDir as fn(PathBuf) -> LayoutError,
3240            ),
3241        ] {
3242            let path = outside_dir.strip_prefix(&root).unwrap().join("tool");
3243            let full = root.join(&path);
3244            let full_probe = full.clone();
3245            let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3246            let sandbox_dir = root.join(sandbox_component);
3247            let err = layout
3248                .probe_sandboxed_declared_entry(&path, &root, kind, &sandbox_dir, ctor)
3249                .unwrap_err();
3250            assert_eq!(
3251                err,
3252                expected_variant(full),
3253                "probe_sandboxed_declared_entry must thread `outside_ctor` verbatim \
3254                 on every canonical `<Slot>OutsideDir` variant the two wire-up sites pass",
3255            );
3256        }
3257    }
3258
3259    #[test]
3260    fn probe_sandboxed_declared_entry_routes_miss_arm_through_probe_declared_entry() {
3261        // Cross-primitive pin — the sandboxed probe must route its
3262        // miss arm through the sibling [`StandardLayout::
3263        // probe_declared_entry`] primitive rather than re-inlining the
3264        // `root.join` + `exists` + `missing_entry` cascade, so a
3265        // future edit to the miss-arm shape on either primitive lands
3266        // in exactly one place. Byte-parity assertion: on a fixture
3267        // that misses the oracle, the sandboxed primitive's `Err`
3268        // arm must equal the peer [`StandardLayout::
3269        // probe_declared_entry`] primitive's `Err` arm on the same
3270        // fixture — otherwise the fold has drifted from the substrate
3271        // primitive.
3272        let layout = StandardLayout::new().with_path_exists(|_| false);
3273        let root = PathBuf::from("/tmp/x");
3274        let path = Path::new("servicos/hello-rio.computeunit.yaml");
3275        let sandbox_dir = root.join(crate::render::LAYOUT_DIR_SERVICOS);
3276        let via_sandboxed = layout
3277            .probe_sandboxed_declared_entry(
3278                path,
3279                &root,
3280                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3281                &sandbox_dir,
3282                LayoutError::ServicoOutsideDir,
3283            )
3284            .unwrap_err();
3285        let via_probe = layout
3286            .probe_declared_entry(
3287                path,
3288                &root,
3289                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3290            )
3291            .unwrap_err();
3292        assert_eq!(
3293            via_sandboxed, via_probe,
3294            "probe_sandboxed_declared_entry's miss arm must equal the sibling \
3295             probe_declared_entry primitive's miss arm byte-equal — pins that the \
3296             sandboxed primitive routes through the substrate primitive rather than \
3297             re-inlining the `root.join` + `exists` + `missing_entry` cascade",
3298        );
3299    }
3300
3301    // ── StandardLayout::probe_declared_entries substrate primitive ───────
3302    //
3303    // The [`StandardLayout::probe_declared_entries`] method (`layout.rs`)
3304    // folds the three self-similar per-slot existence-probe *loop* blocks
3305    // at [`StandardLayout::verify`] (`:bibliotecas` iteration,
3306    // `:behavior` on-disk callback-path iteration, `:upgrade-from` per-
3307    // instruction script-path iteration) onto one substrate primitive.
3308    // The pins below (fail-before-pass-after by construction — a byte-
3309    // mismatched primitive body would trip its equivalence pin first)
3310    // lock the fold to its pre-lift open-coded shape under `PartialEq`,
3311    // so every wire-up in [`StandardLayout::verify`] on this primitive
3312    // produces a byte-equal `LayoutError` on miss (via the sibling
3313    // [`StandardLayout::probe_declared_entry`] miss arm) and a byte-
3314    // equal `Ok(())` on the empty / all-hit arms (the fold's identity
3315    // element on an empty slot list; the pre-lift `for … { … }` loop's
3316    // vacuous pass-through).
3317    //
3318    // Sibling of the peer per-arm-probe [`StandardLayout::probe_declared_entry`]
3319    // (fda1e35) and two-arm-sandboxed [`StandardLayout::
3320    // probe_sandboxed_declared_entry`] (4940d55) primitive test blocks
3321    // — same substrate-primitive discipline extended onto the per-slot
3322    // batch axis these two per-path primitives compose under.
3323
3324    #[test]
3325    fn probe_declared_entries_folds_empty_iterator_returns_ok() {
3326        // Per-primitive identity-element pin on the empty-iterator arm —
3327        // the fold's `Ok(())` return on an empty `IntoIterator` is byte-
3328        // equal to the pre-lift `for _ in <empty> { … }` loop's vacuous
3329        // pass-through. Peer of the peer-primitive `Option::None →
3330        // Ok(())` identity elements the sibling per-Caixa compound
3331        // gates ([`crate::Caixa::validate_limits`] baa4688,
3332        // [`crate::Caixa::validate_behavior`] 0d2877a) each carry on
3333        // the sibling per-slot compound-gate axis.
3334        let layout = StandardLayout::new().with_path_exists(|_| false);
3335        let root = PathBuf::from("/tmp/x");
3336        let empty: [&Path; 0] = [];
3337        layout
3338            .probe_declared_entries(
3339                empty,
3340                &root,
3341                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3342            )
3343            .expect(
3344                "probe_declared_entries must return Ok(()) on an empty iterator \
3345                 — the fold's identity element on an empty per-slot batch",
3346            );
3347    }
3348
3349    #[test]
3350    fn probe_declared_entries_folds_all_hits_returns_ok() {
3351        // Per-primitive equivalence pin on the all-hit arm — a
3352        // [`StandardLayout`] whose oracle admits every path in the batch
3353        // yields `Ok(())` byte-equal to the pre-lift `for p in … {
3354        // self.probe_declared_entry(p, root, kind)?; }` loop's full-
3355        // consumption pass-through.
3356        let root = PathBuf::from("/tmp/x");
3357        let a = root.join("lib/a.lisp");
3358        let b = root.join("lib/b.lisp");
3359        let a_probe = a.clone();
3360        let b_probe = b.clone();
3361        let layout = StandardLayout::new().with_path_exists(move |p| p == a_probe || p == b_probe);
3362        layout
3363            .probe_declared_entries(
3364                [Path::new("lib/a.lisp"), Path::new("lib/b.lisp")],
3365                &root,
3366                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3367            )
3368            .expect(
3369                "probe_declared_entries must return Ok(()) when every path in the \
3370                 batch is admitted by the oracle",
3371            );
3372    }
3373
3374    #[test]
3375    fn probe_declared_entries_folds_first_miss_short_circuits_via_probe_declared_entry() {
3376        // Per-primitive equivalence pin on the first-miss arm — a
3377        // [`StandardLayout`] whose oracle admits the first path and
3378        // rejects the second yields a `MissingEntry` byte-equal under
3379        // `PartialEq` to the sibling [`StandardLayout::
3380        // probe_declared_entry`] primitive's miss wrap on the *second*
3381        // path (the pre-lift `for … { … ? }` loop's first-error return
3382        // semantics), *not* on the first (admitted) path.
3383        let root = PathBuf::from("/tmp/x");
3384        let hit = root.join("lib/a.lisp");
3385        let hit_probe = hit.clone();
3386        let layout = StandardLayout::new().with_path_exists(move |p| p == hit_probe);
3387        let miss = Path::new("lib/b.lisp");
3388        let err = layout
3389            .probe_declared_entries(
3390                [Path::new("lib/a.lisp"), miss],
3391                &root,
3392                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3393            )
3394            .unwrap_err();
3395        assert_eq!(
3396            err,
3397            LayoutError::missing_entry(
3398                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3399                root.join(miss),
3400            ),
3401            "probe_declared_entries must short-circuit on the first missing entry \
3402             with a `MissingEntry` byte-equal to the sibling probe_declared_entry \
3403             primitive's miss wrap on that entry — the pre-lift `for … {{ … ? }}` \
3404             loop's first-error return semantics",
3405        );
3406    }
3407
3408    #[test]
3409    fn probe_declared_entries_folds_first_miss_short_circuits_before_later_paths() {
3410        // Diagnostic-order pin — the primitive must return on the *first*
3411        // miss in iterator order rather than probing every path and
3412        // returning the last miss (which would silently drop the pre-
3413        // lift `for … { … ? }` loop's first-error contract). Fixture:
3414        // the oracle rejects both paths, so a byte-equal `MissingEntry`
3415        // on the *first* path in the iterator distinguishes the two
3416        // return-order shapes.
3417        let layout = StandardLayout::new().with_path_exists(|_| false);
3418        let root = PathBuf::from("/tmp/x");
3419        let first = Path::new("lib/first.lisp");
3420        let second = Path::new("lib/second.lisp");
3421        let err = layout
3422            .probe_declared_entries(
3423                [first, second],
3424                &root,
3425                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3426            )
3427            .unwrap_err();
3428        assert_eq!(
3429            err,
3430            LayoutError::missing_entry(
3431                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3432                root.join(first),
3433            ),
3434            "probe_declared_entries must return on the *first* miss in iterator order \
3435             — a `MissingEntry` on the second path would silently drop the pre-lift \
3436             `for … {{ … ? }}` loop's first-error contract",
3437        );
3438    }
3439
3440    #[test]
3441    fn probe_declared_entries_threads_kind_through_arg_verbatim() {
3442        // Cross-axis pin — sweep every canonical
3443        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] label the three
3444        // batch wire-up sites in [`StandardLayout::verify`] pass through
3445        // this primitive (`:bibliotecas`, `:behavior` callback,
3446        // `:upgrade-from` script). Each miss must return a `MissingEntry`
3447        // whose `kind:` field byte-equals the caller-provided arg, so
3448        // the fold does not silently collapse onto one hard-coded label.
3449        // Sibling of the peer `probe_declared_entry_threads_kind_through_arg_verbatim`
3450        // pin's five-label sweep on the per-arm primitive.
3451        let layout = StandardLayout::new().with_path_exists(|_| false);
3452        let root = PathBuf::from("/tmp/x");
3453        let path = Path::new("some/entry");
3454        for kind in [
3455            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3456            crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
3457            crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
3458        ] {
3459            let err = layout
3460                .probe_declared_entries([path], &root, kind)
3461                .unwrap_err();
3462            assert_eq!(
3463                err,
3464                LayoutError::missing_entry(kind, root.join(path)),
3465                "probe_declared_entries must thread `kind` verbatim on every canonical \
3466                 batch label",
3467            );
3468        }
3469    }
3470
3471    #[test]
3472    fn probe_declared_entries_accepts_asref_path_shape_wire_up_sites_pass() {
3473        // Cross-axis pin — the primitive's `P: AsRef<Path>` bound must
3474        // admit every concrete iterator element type the three
3475        // [`StandardLayout::verify`] wire-up sites pass:
3476        // - `&String` (from `caixa.bibliotecas(): &[String]`),
3477        // - `&Path` (from `b.declared_paths(): impl Iterator<Item = &Path>`),
3478        // - `&PathBuf` (from the flattened `.filter_map(_::declared_path)`
3479        //   on `:upgrade-from` instructions, whose `declared_path`
3480        //   returns `Option<&PathBuf>`).
3481        //
3482        // Byte-parity assertion: on a shared `/tmp/x/lib/demo.lisp`
3483        // fixture the miss-arm return must be byte-equal across all
3484        // three element-type flavors, so a future re-shape of the
3485        // bound (a narrower `P: Into<PathBuf>` collapse, a stricter
3486        // `&Path`-only signature) would surface here rather than at
3487        // the caller wire-up site.
3488        let layout = StandardLayout::new().with_path_exists(|_| false);
3489        let root = PathBuf::from("/tmp/x");
3490        let literal = "lib/demo.lisp";
3491        let expected = LayoutError::missing_entry(
3492            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3493            root.join(Path::new(literal)),
3494        );
3495
3496        let via_string_slice: Vec<String> = vec![literal.to_string()];
3497        let via_string_err = layout
3498            .probe_declared_entries(
3499                via_string_slice.iter(),
3500                &root,
3501                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3502            )
3503            .unwrap_err();
3504        assert_eq!(
3505            via_string_err, expected,
3506            "probe_declared_entries must accept `&String` items (the `caixa.bibliotecas() \
3507             : &[String]` wire-up shape)",
3508        );
3509
3510        let via_path_slice: [&Path; 1] = [Path::new(literal)];
3511        let via_path_err = layout
3512            .probe_declared_entries(
3513                via_path_slice,
3514                &root,
3515                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3516            )
3517            .unwrap_err();
3518        assert_eq!(
3519            via_path_err, expected,
3520            "probe_declared_entries must accept `&Path` items (the `b.declared_paths() \
3521             : impl Iterator<Item = &Path>` wire-up shape)",
3522        );
3523
3524        let via_pathbuf_slice: Vec<PathBuf> = vec![PathBuf::from(literal)];
3525        let via_pathbuf_err = layout
3526            .probe_declared_entries(
3527                via_pathbuf_slice.iter(),
3528                &root,
3529                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3530            )
3531            .unwrap_err();
3532        assert_eq!(
3533            via_pathbuf_err, expected,
3534            "probe_declared_entries must accept `&PathBuf` items (the `.filter_map( \
3535             _::declared_path)` on `:upgrade-from` instructions wire-up shape)",
3536        );
3537    }
3538
3539    #[test]
3540    fn probe_declared_entries_routes_miss_arm_through_probe_declared_entry() {
3541        // Cross-primitive pin — the batch primitive must route its miss
3542        // arm through the sibling [`StandardLayout::probe_declared_entry`]
3543        // primitive rather than re-inlining the `root.join` + `exists`
3544        // + `missing_entry` cascade, so a future edit to the miss-arm
3545        // shape on either primitive lands in exactly one place. Byte-
3546        // parity assertion: on a fixture that misses the oracle, the
3547        // batch primitive's `Err` arm must equal the peer per-arm
3548        // [`StandardLayout::probe_declared_entry`] primitive's `Err` arm
3549        // on the same fixture — otherwise the batch fold has drifted
3550        // from the substrate primitive. Same discipline the peer
3551        // `probe_sandboxed_declared_entry_routes_miss_arm_through_probe_declared_entry`
3552        // pin establishes on the sibling sandboxed-primitive axis.
3553        let layout = StandardLayout::new().with_path_exists(|_| false);
3554        let root = PathBuf::from("/tmp/x");
3555        let path = Path::new("lib/demo.lisp");
3556        let via_batch = layout
3557            .probe_declared_entries(
3558                [path],
3559                &root,
3560                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3561            )
3562            .unwrap_err();
3563        let via_probe = layout
3564            .probe_declared_entry(
3565                path,
3566                &root,
3567                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3568            )
3569            .unwrap_err();
3570        assert_eq!(
3571            via_batch, via_probe,
3572            "probe_declared_entries's miss arm must equal the sibling probe_declared_entry \
3573             primitive's miss arm byte-equal — pins that the batch primitive routes \
3574             through the substrate primitive rather than re-inlining the `root.join` + \
3575             `exists` + `missing_entry` cascade",
3576        );
3577    }
3578
3579    // ── StandardLayout::probe_sandboxed_declared_entries substrate primitive ─
3580    //
3581    // The [`StandardLayout::probe_sandboxed_declared_entries`] method
3582    // (`layout.rs`) folds the two self-similar per-slot sandboxed-
3583    // existence-probe *loop* blocks at [`StandardLayout::verify`]
3584    // (`:exe` iteration, `:servicos` iteration) onto one substrate
3585    // primitive. The pins below (fail-before-pass-after by construction
3586    // — a byte-mismatched primitive body would trip its equivalence pin
3587    // first) lock the fold to its pre-lift open-coded shape under
3588    // `PartialEq`, so every wire-up in [`StandardLayout::verify`] on
3589    // this primitive produces a byte-equal `LayoutError` on miss / on
3590    // sandbox-escape (via the sibling
3591    // [`StandardLayout::probe_sandboxed_declared_entry`] arms) and a
3592    // byte-equal `Ok(())` on the empty / all-hit arms (the fold's
3593    // identity element on an empty slot list; the pre-lift `for … {
3594    // … }` loop's vacuous pass-through).
3595    //
3596    // Sibling of the peer per-slot batch bare-probe
3597    // [`StandardLayout::probe_declared_entries`] (d1ccb0b), per-arm
3598    // bare-probe [`StandardLayout::probe_declared_entry`] (fda1e35), and
3599    // per-arm sandboxed-probe
3600    // [`StandardLayout::probe_sandboxed_declared_entry`] (4940d55)
3601    // primitive test blocks — same substrate-primitive discipline
3602    // extended onto the fourth and last quadrant of the
3603    // (per-arm | per-slot batch) × (bare | sandboxed) existence-probe
3604    // algebra.
3605
3606    #[test]
3607    fn probe_sandboxed_declared_entries_folds_empty_iterator_returns_ok() {
3608        // Per-primitive identity-element pin on the empty-iterator arm —
3609        // the fold's `Ok(())` return on an empty `IntoIterator` is byte-
3610        // equal to the pre-lift `for _ in <empty> { … }` loop's vacuous
3611        // pass-through. Peer of the sibling
3612        // `probe_declared_entries_folds_empty_iterator_returns_ok` pin
3613        // on the bare-batch axis and the `Option::None → Ok(())`
3614        // identity elements the per-Caixa compound gates
3615        // ([`crate::Caixa::validate_limits`] baa4688,
3616        // [`crate::Caixa::validate_behavior`] 0d2877a) each carry on the
3617        // per-slot compound-gate axis.
3618        let layout = StandardLayout::new().with_path_exists(|_| false);
3619        let root = PathBuf::from("/tmp/x");
3620        let empty: [&Path; 0] = [];
3621        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3622        layout
3623            .probe_sandboxed_declared_entries(
3624                empty,
3625                &root,
3626                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3627                &exe_dir,
3628                LayoutError::ExeOutsideDir,
3629            )
3630            .expect(
3631                "probe_sandboxed_declared_entries must return Ok(()) on an empty iterator \
3632                 — the fold's identity element on an empty per-slot sandboxed batch",
3633            );
3634    }
3635
3636    #[test]
3637    fn probe_sandboxed_declared_entries_folds_all_hits_returns_ok() {
3638        // Per-primitive equivalence pin on the all-hit arm — a
3639        // [`StandardLayout`] whose oracle admits every path in the batch
3640        // *and* whose resolved paths live inside `sandbox_dir` yields
3641        // `Ok(())` byte-equal to the pre-lift `for p in … {
3642        // self.probe_sandboxed_declared_entry(p, root, kind,
3643        // &sandbox_dir, outside_ctor)?; }` loop's full-consumption
3644        // pass-through.
3645        let root = PathBuf::from("/tmp/x");
3646        let a = root.join("exe/a");
3647        let b = root.join("exe/b");
3648        let a_probe = a.clone();
3649        let b_probe = b.clone();
3650        let layout = StandardLayout::new().with_path_exists(move |p| p == a_probe || p == b_probe);
3651        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3652        layout
3653            .probe_sandboxed_declared_entries(
3654                [Path::new("exe/a"), Path::new("exe/b")],
3655                &root,
3656                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3657                &exe_dir,
3658                LayoutError::ExeOutsideDir,
3659            )
3660            .expect(
3661                "probe_sandboxed_declared_entries must return Ok(()) when every path in \
3662                 the batch is admitted by the oracle and lives inside sandbox_dir",
3663            );
3664    }
3665
3666    #[test]
3667    fn probe_sandboxed_declared_entries_folds_first_miss_short_circuits_via_probe_sandboxed_declared_entry()
3668     {
3669        // Per-primitive equivalence pin on the first-miss arm — a
3670        // [`StandardLayout`] whose oracle admits the first path (inside
3671        // sandbox_dir) and rejects the second yields a `MissingEntry`
3672        // byte-equal under `PartialEq` to the sibling
3673        // [`StandardLayout::probe_sandboxed_declared_entry`] primitive's
3674        // miss wrap on the *second* path (the pre-lift `for … { … ? }`
3675        // loop's first-error return semantics), *not* on the first
3676        // (admitted) path.
3677        let root = PathBuf::from("/tmp/x");
3678        let hit = root.join("exe/a");
3679        let hit_probe = hit.clone();
3680        let layout = StandardLayout::new().with_path_exists(move |p| p == hit_probe);
3681        let miss = Path::new("exe/b");
3682        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3683        let err = layout
3684            .probe_sandboxed_declared_entries(
3685                [Path::new("exe/a"), miss],
3686                &root,
3687                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3688                &exe_dir,
3689                LayoutError::ExeOutsideDir,
3690            )
3691            .unwrap_err();
3692        assert_eq!(
3693            err,
3694            LayoutError::missing_entry(
3695                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3696                root.join(miss),
3697            ),
3698            "probe_sandboxed_declared_entries must short-circuit on the first missing \
3699             entry with a `MissingEntry` byte-equal to the sibling \
3700             probe_sandboxed_declared_entry primitive's miss wrap on that entry — the \
3701             pre-lift `for … {{ … ? }}` loop's first-error return semantics",
3702        );
3703    }
3704
3705    #[test]
3706    fn probe_sandboxed_declared_entries_folds_first_miss_short_circuits_before_later_paths() {
3707        // Diagnostic-order pin — the primitive must return on the *first*
3708        // miss in iterator order rather than probing every path and
3709        // returning the last miss (which would silently drop the pre-
3710        // lift `for … { … ? }` loop's first-error contract). Fixture:
3711        // the oracle rejects both paths, so a byte-equal `MissingEntry`
3712        // on the *first* path in the iterator distinguishes the two
3713        // return-order shapes. Sibling of the peer
3714        // `probe_declared_entries_folds_first_miss_short_circuits_before_later_paths`
3715        // pin on the bare-batch axis.
3716        let layout = StandardLayout::new().with_path_exists(|_| false);
3717        let root = PathBuf::from("/tmp/x");
3718        let first = Path::new("exe/first");
3719        let second = Path::new("exe/second");
3720        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3721        let err = layout
3722            .probe_sandboxed_declared_entries(
3723                [first, second],
3724                &root,
3725                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3726                &exe_dir,
3727                LayoutError::ExeOutsideDir,
3728            )
3729            .unwrap_err();
3730        assert_eq!(
3731            err,
3732            LayoutError::missing_entry(
3733                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3734                root.join(first),
3735            ),
3736            "probe_sandboxed_declared_entries must return on the *first* miss in iterator \
3737             order — a `MissingEntry` on the second path would silently drop the pre-lift \
3738             `for … {{ … ? }}` loop's first-error contract",
3739        );
3740    }
3741
3742    #[test]
3743    fn probe_sandboxed_declared_entries_folds_sandbox_escape_via_outside_ctor() {
3744        // Per-primitive equivalence pin on the sandbox-escape arm — a
3745        // [`StandardLayout`] whose oracle admits the probed resolved
3746        // path (so the miss arm passes) but whose resolved path lies
3747        // outside the caller-provided `sandbox_dir` yields the paired
3748        // `outside_ctor(full)` byte-equal under `PartialEq`. The
3749        // primitive threads the resolved `full` through the caller-
3750        // supplied `fn(PathBuf) -> LayoutError` constructor rather than
3751        // a hard-coded variant, so the fold does not silently collapse
3752        // onto one of the two `:exe` / `:servicos` outside-dir variants.
3753        let root = PathBuf::from("/tmp/x");
3754        let path = Path::new("lib/tool");
3755        let full = root.join(path);
3756        let full_probe = full.clone();
3757        let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3758        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3759        let err = layout
3760            .probe_sandboxed_declared_entries(
3761                [path],
3762                &root,
3763                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3764                &exe_dir,
3765                LayoutError::ExeOutsideDir,
3766            )
3767            .unwrap_err();
3768        assert_eq!(
3769            err,
3770            LayoutError::ExeOutsideDir(full),
3771            "probe_sandboxed_declared_entries sandbox-escape arm must route the resolved \
3772             `full` through the caller-supplied outside_ctor byte-equal to the pre-lift \
3773             `LayoutError::ExeOutsideDir(full)` tuple-literal",
3774        );
3775    }
3776
3777    #[test]
3778    fn probe_sandboxed_declared_entries_threads_outside_ctor_through_arg_verbatim() {
3779        // Cross-axis pin — the primitive must thread the caller-supplied
3780        // `outside_ctor` verbatim into the sandbox-escape arm's
3781        // `LayoutError` return, so the fold does not silently collapse
3782        // onto one hard-coded variant. Sweep both [`LayoutError`]
3783        // tuple-variants the two wire-up sites in
3784        // [`StandardLayout::verify`] pass — [`LayoutError::ExeOutsideDir`]
3785        // and [`LayoutError::ServicoOutsideDir`] — so a byte-drifted
3786        // ctor-routing on either axis would trip. Sibling of the peer
3787        // `probe_sandboxed_declared_entry_threads_outside_ctor_through_arg_verbatim`
3788        // pin on the per-arm sandboxed-primitive axis.
3789        let root = PathBuf::from("/tmp/x");
3790        let outside_dir = root.join(crate::render::LAYOUT_DIR_LIB);
3791        for (kind, sandbox_component, ctor) in [
3792            (
3793                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3794                crate::render::LAYOUT_DIR_EXE,
3795                LayoutError::ExeOutsideDir as fn(PathBuf) -> LayoutError,
3796            ),
3797            (
3798                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3799                crate::render::LAYOUT_DIR_SERVICOS,
3800                LayoutError::ServicoOutsideDir as fn(PathBuf) -> LayoutError,
3801            ),
3802        ] {
3803            let path = outside_dir.strip_prefix(&root).unwrap().join("tool");
3804            let full = root.join(&path);
3805            let full_probe = full.clone();
3806            let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3807            let sandbox_dir = root.join(sandbox_component);
3808            let err = layout
3809                .probe_sandboxed_declared_entries([&path], &root, kind, &sandbox_dir, ctor)
3810                .unwrap_err();
3811            assert_eq!(
3812                err,
3813                ctor(full),
3814                "probe_sandboxed_declared_entries must thread `outside_ctor` verbatim on \
3815                 every canonical `<Slot>OutsideDir` variant the two wire-up sites pass",
3816            );
3817        }
3818    }
3819
3820    #[test]
3821    fn probe_sandboxed_declared_entries_threads_kind_through_arg_verbatim() {
3822        // Cross-axis pin — sweep every canonical
3823        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] label the two
3824        // sandboxed-batch wire-up sites in [`StandardLayout::verify`]
3825        // pass through this primitive (`:exe`, `:servicos`). Each miss
3826        // must return a `MissingEntry` whose `kind:` field byte-equals
3827        // the caller-provided arg, so the fold does not silently
3828        // collapse onto one hard-coded label. Sibling of the peer
3829        // `probe_declared_entries_threads_kind_through_arg_verbatim`
3830        // pin's label sweep on the bare-batch axis.
3831        let layout = StandardLayout::new().with_path_exists(|_| false);
3832        let root = PathBuf::from("/tmp/x");
3833        let path = Path::new("some/entry");
3834        for (kind, sandbox_component, ctor) in [
3835            (
3836                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3837                crate::render::LAYOUT_DIR_EXE,
3838                LayoutError::ExeOutsideDir as fn(PathBuf) -> LayoutError,
3839            ),
3840            (
3841                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3842                crate::render::LAYOUT_DIR_SERVICOS,
3843                LayoutError::ServicoOutsideDir as fn(PathBuf) -> LayoutError,
3844            ),
3845        ] {
3846            let sandbox_dir = root.join(sandbox_component);
3847            let err = layout
3848                .probe_sandboxed_declared_entries([path], &root, kind, &sandbox_dir, ctor)
3849                .unwrap_err();
3850            assert_eq!(
3851                err,
3852                LayoutError::missing_entry(kind, root.join(path)),
3853                "probe_sandboxed_declared_entries must thread `kind` verbatim on every \
3854                 canonical sandboxed-batch label",
3855            );
3856        }
3857    }
3858
3859    #[test]
3860    fn probe_sandboxed_declared_entries_accepts_asref_path_shape_wire_up_sites_pass() {
3861        // Cross-axis pin — the primitive's `P: AsRef<Path>` bound must
3862        // admit every concrete iterator element type the two
3863        // [`StandardLayout::verify`] wire-up sites pass:
3864        // - `&String` (from `caixa.exe(): &[String]`),
3865        // - `&String` (from `caixa.servicos(): &[String]`).
3866        //
3867        // Byte-parity assertion: on a shared `/tmp/x/lib/demo`
3868        // fixture the miss-arm return must be byte-equal across
3869        // `&String` and the ergonomic `&Path` / `&PathBuf` element-type
3870        // flavors, so a future re-shape of the bound (a narrower `P:
3871        // Into<PathBuf>` collapse, a stricter `&Path`-only signature)
3872        // would surface here rather than at the caller wire-up site.
3873        // Sibling of the peer
3874        // `probe_declared_entries_accepts_asref_path_shape_wire_up_sites_pass`
3875        // pin on the bare-batch axis.
3876        let layout = StandardLayout::new().with_path_exists(|_| false);
3877        let root = PathBuf::from("/tmp/x");
3878        let literal = "exe/demo";
3879        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3880        let expected = LayoutError::missing_entry(
3881            crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3882            root.join(Path::new(literal)),
3883        );
3884
3885        let via_string_slice: Vec<String> = vec![literal.to_string()];
3886        let via_string_err = layout
3887            .probe_sandboxed_declared_entries(
3888                via_string_slice.iter(),
3889                &root,
3890                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3891                &exe_dir,
3892                LayoutError::ExeOutsideDir,
3893            )
3894            .unwrap_err();
3895        assert_eq!(
3896            via_string_err, expected,
3897            "probe_sandboxed_declared_entries must accept `&String` items (the `caixa.exe() \
3898             : &[String]` / `caixa.servicos(): &[String]` wire-up shape)",
3899        );
3900
3901        let via_path_slice: [&Path; 1] = [Path::new(literal)];
3902        let via_path_err = layout
3903            .probe_sandboxed_declared_entries(
3904                via_path_slice,
3905                &root,
3906                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3907                &exe_dir,
3908                LayoutError::ExeOutsideDir,
3909            )
3910            .unwrap_err();
3911        assert_eq!(
3912            via_path_err, expected,
3913            "probe_sandboxed_declared_entries must accept `&Path` items",
3914        );
3915
3916        let via_pathbuf_slice: Vec<PathBuf> = vec![PathBuf::from(literal)];
3917        let via_pathbuf_err = layout
3918            .probe_sandboxed_declared_entries(
3919                via_pathbuf_slice.iter(),
3920                &root,
3921                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3922                &exe_dir,
3923                LayoutError::ExeOutsideDir,
3924            )
3925            .unwrap_err();
3926        assert_eq!(
3927            via_pathbuf_err, expected,
3928            "probe_sandboxed_declared_entries must accept `&PathBuf` items",
3929        );
3930    }
3931
3932    #[test]
3933    fn probe_sandboxed_declared_entries_routes_miss_arm_through_probe_sandboxed_declared_entry() {
3934        // Cross-primitive pin — the batch primitive must route its miss
3935        // arm through the sibling
3936        // [`StandardLayout::probe_sandboxed_declared_entry`] primitive
3937        // rather than re-inlining the `probe_declared_entry` +
3938        // `starts_with` cascade, so a future edit to the miss-arm shape
3939        // on either primitive lands in exactly one place. Byte-parity
3940        // assertion: on a fixture that misses the oracle, the batch
3941        // primitive's `Err` arm must equal the peer per-arm
3942        // [`StandardLayout::probe_sandboxed_declared_entry`] primitive's
3943        // `Err` arm on the same fixture — otherwise the batch fold has
3944        // drifted from the substrate primitive. Same discipline the peer
3945        // `probe_declared_entries_routes_miss_arm_through_probe_declared_entry`
3946        // pin establishes on the sibling bare-batch axis.
3947        let layout = StandardLayout::new().with_path_exists(|_| false);
3948        let root = PathBuf::from("/tmp/x");
3949        let path = Path::new("servicos/hello-rio.computeunit.yaml");
3950        let sandbox_dir = root.join(crate::render::LAYOUT_DIR_SERVICOS);
3951        let via_batch = layout
3952            .probe_sandboxed_declared_entries(
3953                [path],
3954                &root,
3955                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3956                &sandbox_dir,
3957                LayoutError::ServicoOutsideDir,
3958            )
3959            .unwrap_err();
3960        let via_probe = layout
3961            .probe_sandboxed_declared_entry(
3962                path,
3963                &root,
3964                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3965                &sandbox_dir,
3966                LayoutError::ServicoOutsideDir,
3967            )
3968            .unwrap_err();
3969        assert_eq!(
3970            via_batch, via_probe,
3971            "probe_sandboxed_declared_entries's miss arm must equal the sibling \
3972             probe_sandboxed_declared_entry primitive's miss arm byte-equal — pins that \
3973             the batch primitive routes through the substrate primitive rather than \
3974             re-inlining the `probe_declared_entry` + `starts_with` cascade",
3975        );
3976    }
3977
3978    #[test]
3979    fn probe_sandboxed_declared_entries_routes_sandbox_escape_arm_through_probe_sandboxed_declared_entry()
3980     {
3981        // Cross-primitive pin — the batch primitive's sandbox-escape arm
3982        // must equal the sibling
3983        // [`StandardLayout::probe_sandboxed_declared_entry`] primitive's
3984        // sandbox-escape arm on the same fixture. Byte-parity assertion:
3985        // on a fixture where the oracle admits the probed path but the
3986        // resolved path lies outside sandbox_dir, both primitives must
3987        // return the same `outside_ctor(full)` byte-equal under
3988        // `PartialEq` — otherwise the batch fold has drifted from the
3989        // per-arm primitive on the second-arm dispatch.
3990        let root = PathBuf::from("/tmp/x");
3991        let path = Path::new("lib/escape");
3992        let full = root.join(path);
3993        let full_probe = full.clone();
3994        let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3995        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3996        let via_batch = layout
3997            .probe_sandboxed_declared_entries(
3998                [path],
3999                &root,
4000                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
4001                &exe_dir,
4002                LayoutError::ExeOutsideDir,
4003            )
4004            .unwrap_err();
4005        let via_probe = layout
4006            .probe_sandboxed_declared_entry(
4007                path,
4008                &root,
4009                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
4010                &exe_dir,
4011                LayoutError::ExeOutsideDir,
4012            )
4013            .unwrap_err();
4014        assert_eq!(
4015            via_batch, via_probe,
4016            "probe_sandboxed_declared_entries's sandbox-escape arm must equal the sibling \
4017             probe_sandboxed_declared_entry primitive's sandbox-escape arm byte-equal — \
4018             pins that the batch primitive routes through the substrate primitive on both \
4019             the miss and sandbox-escape arms rather than re-inlining either cascade",
4020        );
4021    }
4022
4023    // ── LayoutError::<nome-only> constructor family ──────────────────────
4024    //
4025    // The [`layout_nome_only_ctors!`] macro (below the `LayoutError` enum
4026    // definition) generates one static constructor per `<Variant>(String)`
4027    // tuple-variant that folds the uniform `Self::<Variant>(caixa.nome()
4028    // .to_string())` one-field construction onto one substrate primitive.
4029    // The per-variant equivalence pins below (fail-before-pass-after by
4030    // construction — a byte-mismatched macro arm would trip its
4031    // equivalence pin first) lock each generated constructor to its
4032    // tuple-literal peer under `PartialEq`, so every wire-up in
4033    // [`StandardLayout::verify`] on that variant produces a byte-equal
4034    // `LayoutError` to the pre-lift open-coded tuple-literal. The
4035    // cross-axis pin that follows (non-default `:nome`) routes the sole
4036    // constructor input axis through its declared accessor, so the fold
4037    // does not silently collapse onto the fixture default `:nome`.
4038
4039    fn layout_nome_only_ctor_fixture() -> Caixa {
4040        caixa(CaixaKind::Biblioteca)
4041    }
4042
4043    // Same rationale as `assert_violation_ctor_matches` / `assert_slot_
4044    // kind_ctor_matches` above: the helper terminates on the equality
4045    // check, so the owned-arg lint's general API-shape target does not
4046    // apply.
4047    #[allow(clippy::needless_pass_by_value)]
4048    fn assert_nome_only_ctor_matches(actual: LayoutError, expected: LayoutError) {
4049        assert_eq!(
4050            actual, expected,
4051            "generated constructor must produce byte-equal LayoutError to open-coded tuple-literal wrap",
4052        );
4053    }
4054
4055    #[test]
4056    fn binario_without_exe_ctor_matches_tuple_literal_wrap() {
4057        let c = layout_nome_only_ctor_fixture();
4058        assert_nome_only_ctor_matches(
4059            LayoutError::binario_without_exe(&c),
4060            LayoutError::BinarioWithoutExe(c.nome().to_string()),
4061        );
4062    }
4063
4064    #[test]
4065    fn servico_without_servicos_ctor_matches_tuple_literal_wrap() {
4066        let c = layout_nome_only_ctor_fixture();
4067        assert_nome_only_ctor_matches(
4068            LayoutError::servico_without_servicos(&c),
4069            LayoutError::ServicoWithoutServicos(c.nome().to_string()),
4070        );
4071    }
4072
4073    #[test]
4074    fn missing_ci_ctor_matches_tuple_literal_wrap() {
4075        let c = layout_nome_only_ctor_fixture();
4076        assert_nome_only_ctor_matches(
4077            LayoutError::missing_ci(&c),
4078            LayoutError::MissingCi(c.nome().to_string()),
4079        );
4080    }
4081
4082    #[test]
4083    fn supervisor_owns_code_ctor_matches_tuple_literal_wrap() {
4084        let c = layout_nome_only_ctor_fixture();
4085        assert_nome_only_ctor_matches(
4086            LayoutError::supervisor_owns_code(&c),
4087            LayoutError::SupervisorOwnsCode(c.nome().to_string()),
4088        );
4089    }
4090
4091    #[test]
4092    fn aplicacao_owns_code_ctor_matches_tuple_literal_wrap() {
4093        let c = layout_nome_only_ctor_fixture();
4094        assert_nome_only_ctor_matches(
4095            LayoutError::aplicacao_owns_code(&c),
4096            LayoutError::AplicacaoOwnsCode(c.nome().to_string()),
4097        );
4098    }
4099
4100    #[test]
4101    fn acao_owns_code_ctor_matches_tuple_literal_wrap() {
4102        let c = layout_nome_only_ctor_fixture();
4103        assert_nome_only_ctor_matches(
4104            LayoutError::acao_owns_code(&c),
4105            LayoutError::AcaoOwnsCode(c.nome().to_string()),
4106        );
4107    }
4108
4109    #[test]
4110    fn nome_only_ctor_routes_caixa_through_nome_accessor() {
4111        // Pin the fold's `caixa.nome().to_string()` sole-field construction
4112        // against a non-default `:nome` — the accessor threads the caller's
4113        // `:nome` verbatim into the tuple-variant, so the fold does not
4114        // silently collapse onto the default `"demo"` fixture nome. Peer of
4115        // the sibling `violation_ctor_routes_caixa_prefix_through_nome_
4116        // accessor` / `slot_kind_ctor_routes_caixa_prefix_through_nome_
4117        // accessor` pins on the `{ caixa, issue }` / `{ caixa, kind,
4118        // slots }` envelopes; extended here onto the sixth
4119        // `<Variant>(String)` envelope so every LayoutError-shape ctor
4120        // family guarantees the `:nome`-derived-caixa slot routes through
4121        // [`Caixa::nome`] rather than a hard-coded string. Sweeps the six
4122        // ctors in the [`layout_nome_only_ctors!`] macro so the pin covers
4123        // every generated arm.
4124        let mut c = caixa(CaixaKind::Biblioteca);
4125        c.nome = "alt-nome".into();
4126        assert_eq!(
4127            LayoutError::binario_without_exe(&c),
4128            LayoutError::BinarioWithoutExe("alt-nome".to_string()),
4129        );
4130        assert_eq!(
4131            LayoutError::servico_without_servicos(&c),
4132            LayoutError::ServicoWithoutServicos("alt-nome".to_string()),
4133        );
4134        assert_eq!(
4135            LayoutError::missing_ci(&c),
4136            LayoutError::MissingCi("alt-nome".to_string()),
4137        );
4138        assert_eq!(
4139            LayoutError::supervisor_owns_code(&c),
4140            LayoutError::SupervisorOwnsCode("alt-nome".to_string()),
4141        );
4142        assert_eq!(
4143            LayoutError::aplicacao_owns_code(&c),
4144            LayoutError::AplicacaoOwnsCode("alt-nome".to_string()),
4145        );
4146        assert_eq!(
4147            LayoutError::acao_owns_code(&c),
4148            LayoutError::AcaoOwnsCode("alt-nome".to_string()),
4149        );
4150    }
4151
4152    #[test]
4153    fn biblioteca_needs_default_lib_path() {
4154        let root = PathBuf::from("/tmp/x");
4155        let expect_manifest = root.join("caixa.lisp");
4156        let layout = StandardLayout::new().with_path_exists(move |p| p == expect_manifest);
4157        let err = layout
4158            .verify(&caixa(CaixaKind::Biblioteca), &root)
4159            .unwrap_err();
4160        assert!(matches!(err, LayoutError::MissingLib { .. }));
4161    }
4162
4163    #[test]
4164    fn biblioteca_passes_when_default_lib_exists() {
4165        let root = PathBuf::from("/tmp/x");
4166        let manifest = root.join("caixa.lisp");
4167        let default_lib = root.join("lib").join("demo.lisp");
4168        let layout =
4169            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4170        layout
4171            .verify(&caixa(CaixaKind::Biblioteca), &root)
4172            .expect("should pass");
4173    }
4174
4175    #[test]
4176    fn binario_without_exe_errors() {
4177        let root = PathBuf::from("/tmp/x");
4178        let manifest = root.join("caixa.lisp");
4179        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
4180        let err = layout
4181            .verify(&caixa(CaixaKind::Binario), &root)
4182            .unwrap_err();
4183        assert!(matches!(err, LayoutError::BinarioWithoutExe(_)));
4184    }
4185
4186    #[test]
4187    fn exe_outside_dir_errors() {
4188        // A relative entry that lives under the caixa root but *not*
4189        // under `exe/` — the canonical case the `starts_with(exe_dir)`
4190        // fence catches. The prior parent-escape shape this test used
4191        // (`"../sibling/tool"`) is now caught at validate time by
4192        // [`Caixa::validate_code_paths`] with the narrower
4193        // [`crate::ManifestError::CodePathParentEscape`] diagnostic
4194        // (see the layout-level integration pin
4195        // `code_path_violation_on_parent_escape_fires_before_existence_check`),
4196        // so this fence pin uses a non-`..` non-absolute shape outside
4197        // `exe/` to preserve coverage of the ExeOutsideDir surface.
4198        let root = PathBuf::from("/tmp/x");
4199        let manifest = root.join("caixa.lisp");
4200        let outside = root.join("lib/tool");
4201        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == outside);
4202        let mut c = caixa(CaixaKind::Binario);
4203        c.exe = vec!["lib/tool".into()];
4204        let err = layout.verify(&c, &root).unwrap_err();
4205        assert!(matches!(err, LayoutError::ExeOutsideDir(_)));
4206    }
4207
4208    // ── code-path shape gate (lifted to layout-level verify) ─────────────
4209
4210    #[test]
4211    fn code_path_violation_on_empty_bibliotecas_entry() {
4212        let root = PathBuf::from("/tmp/x");
4213        let manifest = root.join("caixa.lisp");
4214        let default_lib = root.join("lib").join("demo.lisp");
4215        let layout =
4216            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4217        let mut c = caixa(CaixaKind::Biblioteca);
4218        c.bibliotecas = vec![String::new()];
4219        let err = layout.verify(&c, &root).unwrap_err();
4220        // The wire-up wraps `ManifestError` Display into the
4221        // CodePathViolation envelope (peer of LimitsViolation /
4222        // BehaviorViolation / UpgradeViolation), so the issue string
4223        // names the offending slot at the source.
4224        let LayoutError::CodePathViolation { caixa, issue } = err else {
4225            panic!("expected LayoutError::CodePathViolation, got {err:?}");
4226        };
4227        assert_eq!(caixa, "demo");
4228        assert!(
4229            issue.contains(":bibliotecas"),
4230            "issue must name the offending slot: {issue}",
4231        );
4232    }
4233
4234    #[test]
4235    fn code_path_violation_on_absolute_servicos_entry() {
4236        let root = PathBuf::from("/tmp/x");
4237        let manifest = root.join("caixa.lisp");
4238        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
4239        let mut c = caixa(CaixaKind::Servico);
4240        c.servicos = vec!["/etc/servicos/escape.yaml".into()];
4241        let err = layout.verify(&c, &root).unwrap_err();
4242        let LayoutError::CodePathViolation { caixa, issue } = err else {
4243            panic!("expected LayoutError::CodePathViolation, got {err:?}");
4244        };
4245        assert_eq!(caixa, "demo");
4246        assert!(
4247            issue.contains(":servicos"),
4248            "issue must name the offending slot: {issue}",
4249        );
4250        assert!(
4251            issue.contains("/etc/servicos/escape.yaml"),
4252            "issue must quote the offending path: {issue}",
4253        );
4254    }
4255
4256    #[test]
4257    fn code_path_violation_on_parent_escape_fires_before_existence_check() {
4258        // The new gate runs BEFORE the existence loops, so a
4259        // parent-escaping `:exe` entry surfaces CodePathViolation
4260        // (naming `:exe` at the source) rather than the downstream
4261        // ExeOutsideDir / MissingEntry against the resolved sandbox-
4262        // escape path. Even if the resolved escape target exists
4263        // on disk (which we simulate here by claiming it does), the
4264        // shape diagnostic wins.
4265        let root = PathBuf::from("/tmp/x");
4266        let manifest = root.join("caixa.lisp");
4267        let resolved_escape = root.join("exe/../../escape.lisp");
4268        let layout =
4269            StandardLayout::new().with_path_exists(move |p| p == manifest || p == resolved_escape);
4270        let mut c = caixa(CaixaKind::Binario);
4271        c.exe = vec!["exe/../../escape.lisp".into()];
4272        let err = layout.verify(&c, &root).unwrap_err();
4273        let LayoutError::CodePathViolation { caixa, issue } = err else {
4274            panic!("expected LayoutError::CodePathViolation, got {err:?}");
4275        };
4276        assert_eq!(caixa, "demo");
4277        assert!(
4278            issue.contains(":exe"),
4279            "issue must name the offending slot: {issue}",
4280        );
4281    }
4282
4283    // ── etiquetas universal-axis gate wired into verify ─────────────────
4284    //
4285    // Pins the layout-pipeline wire-up of [`Caixa::validate_etiquetas`]:
4286    // the fourth universal-axis Caixa-level value-shape gate (peer of
4287    // `validate_nome` / `validate_versao` / `validate_deps` /
4288    // `validate_code_paths`), wired before the kind-coherence gates so
4289    // a structurally-invalid `:etiquetas` entry on any kind surfaces
4290    // the per-axis `EtiquetasViolation { caixa, issue }` envelope at
4291    // the source rather than silently rendering as `keywords: [""]`
4292    // in `Chart.yaml` (Servico kind, via caixa-helm's `BTreeSet`
4293    // collect) or silently dedup'ing at chart render (every kind).
4294    // Until this wire-up landed `:etiquetas` had no shape gate at any
4295    // layer — the registry-search-tag axis was the largest universal
4296    // authoring surface on the typed Caixa surface with no validate
4297    // discipline.
4298    //
4299    // Same per-axis `*Violation { caixa, issue }` envelope every peer
4300    // per-axis wrap exposes; the wire-up runs after `validate_deps`
4301    // (universal axis ordering: `:nome` → `:versao` → `:deps` →
4302    // `:etiquetas`) and before every kind-coherence gate
4303    // (`:etiquetas` is universal so its shape diagnostic is more
4304    // fundamental than the partition-on-kind diagnostics).
4305
4306    #[test]
4307    fn etiquetas_violation_on_empty_entry() {
4308        // Canonical paste-from-blank-doc footgun on every kind. The
4309        // wrap envelope wraps [`ManifestError::EtiquetaEmpty`]'s
4310        // Display through verbatim, so the issue string names the
4311        // offending `:etiquetas` axis at the source — the author can
4312        // grep their caixa.lisp for `:etiquetas` and fix the empty
4313        // entry in one edit. Mirrors the peer
4314        // `code_path_violation_on_empty_bibliotecas_entry` shape
4315        // (b868442) on the `:bibliotecas` axis.
4316        let root = PathBuf::from("/tmp/x");
4317        let manifest = root.join("caixa.lisp");
4318        let default_lib = root.join("lib").join("demo.lisp");
4319        let layout =
4320            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4321        let mut c = caixa(CaixaKind::Biblioteca);
4322        c.etiquetas = vec![String::new()];
4323        let err = layout.verify(&c, &root).unwrap_err();
4324        let LayoutError::EtiquetasViolation { caixa, issue } = err else {
4325            panic!("expected LayoutError::EtiquetasViolation, got {err:?}");
4326        };
4327        assert_eq!(caixa, "demo");
4328        assert!(
4329            issue.contains(":etiquetas"),
4330            "issue must name the offending slot: {issue}",
4331        );
4332    }
4333
4334    #[test]
4335    fn etiquetas_violation_on_duplicate_entry() {
4336        // Canonical copy-paste-the-wrong-tag footgun. Without the wire-
4337        // up the duplicate was silently dedup'd by caixa-helm's
4338        // `BTreeSet` collect at chart render — a "second wins / one
4339        // silently disappears" shape. The wrap envelope names the
4340        // offending tag verbatim through the inner
4341        // [`ManifestError::EtiquetaDuplicate`]'s Display.
4342        let root = PathBuf::from("/tmp/x");
4343        let manifest = root.join("caixa.lisp");
4344        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
4345        let mut c = caixa(CaixaKind::Servico);
4346        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
4347        c.etiquetas = vec!["demo".into(), "demo".into()];
4348        // The servicos path doesn't exist in this fixture, but the
4349        // `:etiquetas` gate fires before the existence loop (universal
4350        // axis dominates kind-specific existence checks). Wire is
4351        // intact iff the wrap envelope surfaces first.
4352        let err = layout.verify(&c, &root).unwrap_err();
4353        let LayoutError::EtiquetasViolation { caixa, issue } = err else {
4354            panic!("expected LayoutError::EtiquetasViolation, got {err:?}");
4355        };
4356        assert_eq!(caixa, "demo");
4357        assert!(
4358            issue.contains("demo"),
4359            "issue must quote the offending tag: {issue}",
4360        );
4361    }
4362
4363    #[test]
4364    fn etiquetas_violation_fires_before_kind_coherence_mesh_slot() {
4365        // Cross-axis precedence pin: a Biblioteca with malformed
4366        // `:etiquetas` *and* declared mesh slots (`:membros`) surfaces
4367        // the universal `:etiquetas` diagnostic first, not the
4368        // kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4369        // `:etiquetas` is universal (every kind owns the slot), so its
4370        // shape diagnostic is more fundamental than the partition-on-
4371        // kind diagnostic. Mirrors the peer
4372        // `deps_violation_fires_before_*` precedence pins (aa77d0f) on
4373        // the universal `:deps` axis vs the same kind-coherence gates.
4374        let root = PathBuf::from("/tmp/x");
4375        let manifest = root.join("caixa.lisp");
4376        let default_lib = root.join("lib").join("demo.lisp");
4377        let layout =
4378            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4379        let mut c = caixa(CaixaKind::Biblioteca);
4380        c.etiquetas = vec![String::new()];
4381        c.membros = vec![crate::aplicacao::Membro {
4382            caixa: "x".into(),
4383            versao: "^0.1".into(),
4384        }];
4385        let err = layout.verify(&c, &root).unwrap_err();
4386        assert!(
4387            matches!(err, LayoutError::EtiquetasViolation { .. }),
4388            "got {err:?}",
4389        );
4390    }
4391
4392    #[test]
4393    fn etiquetas_violation_fires_after_deps_violation() {
4394        // Cross-axis precedence pin (inside the universal-axis trio):
4395        // a caixa with both a malformed `:deps` entry *and* a malformed
4396        // `:etiquetas` entry surfaces `DepsViolation` first — `:deps`
4397        // is the third universal axis in declaration order
4398        // (`:nome` → `:versao` → `:deps` → `:etiquetas`) and runs first
4399        // in `verify`. Mirrors the peer
4400        // `nome_violation_fires_before_versao_violation` shape on the
4401        // identity-axis pair.
4402        let root = PathBuf::from("/tmp/x");
4403        let manifest = root.join("caixa.lisp");
4404        let default_lib = root.join("lib").join("demo.lisp");
4405        let layout =
4406            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4407        let mut c = caixa(CaixaKind::Biblioteca);
4408        c.deps = vec![crate::Dep::simple("Caixa-Teia", "^0.1")]; // uppercase :nome
4409        c.etiquetas = vec![String::new()];
4410        let err = layout.verify(&c, &root).unwrap_err();
4411        assert!(
4412            matches!(err, LayoutError::DepsViolation { .. }),
4413            "got {err:?}",
4414        );
4415    }
4416
4417    #[test]
4418    fn etiquetas_violation_accepts_canonical_template() {
4419        // Positive control sanity pin: the canonical `Caixa::template`
4420        // shape (`:etiquetas ()` — empty list) passes the gate
4421        // trivially. Mirrors the peer
4422        // `validate_code_paths_accepts_canonical_template` pin.
4423        let root = PathBuf::from("/tmp/x");
4424        let manifest = root.join("caixa.lisp");
4425        let default_lib = root.join("lib").join("demo.lisp");
4426        let layout =
4427            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4428        let c = caixa(CaixaKind::Biblioteca);
4429        layout.verify(&c, &root).expect("template must pass");
4430    }
4431
4432    #[test]
4433    fn etiquetas_violation_on_non_chart_keyword_shape() {
4434        // Canonical CSV-list-separator-confusion footgun: the author
4435        // confused the CSV-style separator with the `:etiquetas` list
4436        // grammar. The shape gate fires past the empty + duplicate
4437        // arms via [`Caixa::validate_etiquetas`]'s new
4438        // `is_chart_keyword_shape` cascade, and the layout envelope
4439        // wraps [`ManifestError::EtiquetaInvalid`]'s Display through
4440        // verbatim — the issue string names both the offending slot
4441        // and the offending value (debug-escaped). Peer with the
4442        // `autores_violation_on_non_chart_maintainer_shape` pin on
4443        // the sibling universal-axis `Vec<String>` surface — the
4444        // second layout pin on the Vec<String> per-entry shape
4445        // cascade.
4446        let root = PathBuf::from("/tmp/x");
4447        let manifest = root.join("caixa.lisp");
4448        let default_lib = root.join("lib").join("demo.lisp");
4449        let layout =
4450            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4451        let mut c = caixa(CaixaKind::Biblioteca);
4452        c.etiquetas = vec!["mesh,http,grpc".into()];
4453        let err = layout.verify(&c, &root).unwrap_err();
4454        let LayoutError::EtiquetasViolation { caixa, issue } = err else {
4455            panic!("expected LayoutError::EtiquetasViolation, got {err:?}");
4456        };
4457        assert_eq!(caixa, "demo");
4458        assert!(
4459            issue.contains(":etiquetas"),
4460            "issue must name the offending slot: {issue}",
4461        );
4462        assert!(
4463            issue.contains("mesh,http,grpc"),
4464            "issue must quote the offending value: {issue}",
4465        );
4466    }
4467
4468    // ── autores universal-axis gate wired into verify ───────────────────
4469    //
4470    // Pins the layout-pipeline wire-up of [`Caixa::validate_autores`]:
4471    // the fifth universal-axis Caixa-level value-shape gate (peer of
4472    // `validate_nome` / `validate_versao` / `validate_deps` /
4473    // `validate_etiquetas` / `validate_code_paths`), wired immediately
4474    // after `validate_etiquetas` so the two Vec-shaped universal
4475    // metadata axes sit adjacent in the cascade. Until this wire-up
4476    // landed `:autores` had no shape gate at any layer — the
4477    // maintainer-axis was the second largest universal authoring
4478    // surface on the typed Caixa surface with no validate discipline,
4479    // and unlike `:etiquetas` (caixa-helm dedups the rendered
4480    // `keywords:` array via `BTreeSet` collect at chart render),
4481    // `maintainers:` has *no* renderer-side dedup, so duplicate
4482    // `:autores` entries render verbatim as two identical
4483    // `Maintainer { name, email: None }` records — a strictly worse
4484    // footgun than the peer `:etiquetas` shape.
4485
4486    #[test]
4487    fn autores_violation_on_empty_entry() {
4488        // Canonical paste-from-blank-doc footgun on every kind. The
4489        // wrap envelope wraps [`ManifestError::AutorEmpty`]'s Display
4490        // through verbatim, so the issue string names the offending
4491        // `:autores` axis at the source — the author can grep their
4492        // caixa.lisp for `:autores` and fix the empty entry in one
4493        // edit. Mirrors the peer `etiquetas_violation_on_empty_entry`
4494        // shape (360a499) on the `:etiquetas` axis.
4495        let root = PathBuf::from("/tmp/x");
4496        let manifest = root.join("caixa.lisp");
4497        let default_lib = root.join("lib").join("demo.lisp");
4498        let layout =
4499            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4500        let mut c = caixa(CaixaKind::Biblioteca);
4501        c.autores = vec![String::new()];
4502        let err = layout.verify(&c, &root).unwrap_err();
4503        let LayoutError::AutoresViolation { caixa, issue } = err else {
4504            panic!("expected LayoutError::AutoresViolation, got {err:?}");
4505        };
4506        assert_eq!(caixa, "demo");
4507        assert!(
4508            issue.contains(":autores"),
4509            "issue must name the offending slot: {issue}",
4510        );
4511    }
4512
4513    #[test]
4514    fn autores_violation_on_duplicate_entry() {
4515        // Canonical copy-paste-the-wrong-author footgun. Unlike the
4516        // peer `:etiquetas` axis (silently dedup'd by caixa-helm's
4517        // `BTreeSet` collect at chart render), `:autores` duplicates
4518        // stack verbatim in the rendered `maintainers:` — the gate
4519        // closes the footgun at validate time before any renderer
4520        // sees it. The wrap envelope names the offending author
4521        // verbatim through the inner [`ManifestError::AutorDuplicate`]'s
4522        // Display.
4523        let root = PathBuf::from("/tmp/x");
4524        let manifest = root.join("caixa.lisp");
4525        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
4526        let mut c = caixa(CaixaKind::Servico);
4527        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
4528        c.autores = vec!["pleme-io".into(), "pleme-io".into()];
4529        // The servicos path doesn't exist in this fixture, but the
4530        // `:autores` gate fires before the existence loop (universal
4531        // axis dominates kind-specific existence checks). Wire is
4532        // intact iff the wrap envelope surfaces first.
4533        let err = layout.verify(&c, &root).unwrap_err();
4534        let LayoutError::AutoresViolation { caixa, issue } = err else {
4535            panic!("expected LayoutError::AutoresViolation, got {err:?}");
4536        };
4537        assert_eq!(caixa, "demo");
4538        assert!(
4539            issue.contains("pleme-io"),
4540            "issue must quote the offending author: {issue}",
4541        );
4542    }
4543
4544    #[test]
4545    fn autores_violation_fires_before_kind_coherence_mesh_slot() {
4546        // Cross-axis precedence pin: a Biblioteca with malformed
4547        // `:autores` *and* declared mesh slots (`:membros`) surfaces
4548        // the universal `:autores` diagnostic first, not the
4549        // kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4550        // `:autores` is universal (every kind owns the slot), so its
4551        // shape diagnostic is more fundamental than the partition-on-
4552        // kind diagnostic. Mirrors the peer
4553        // `etiquetas_violation_fires_before_kind_coherence_mesh_slot`
4554        // pin (360a499) on the `:etiquetas` axis vs the same kind-
4555        // coherence gates.
4556        let root = PathBuf::from("/tmp/x");
4557        let manifest = root.join("caixa.lisp");
4558        let default_lib = root.join("lib").join("demo.lisp");
4559        let layout =
4560            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4561        let mut c = caixa(CaixaKind::Biblioteca);
4562        c.autores = vec![String::new()];
4563        c.membros = vec![crate::aplicacao::Membro {
4564            caixa: "x".into(),
4565            versao: "^0.1".into(),
4566        }];
4567        let err = layout.verify(&c, &root).unwrap_err();
4568        assert!(
4569            matches!(err, LayoutError::AutoresViolation { .. }),
4570            "got {err:?}",
4571        );
4572    }
4573
4574    #[test]
4575    fn autores_violation_fires_after_etiquetas_violation() {
4576        // Cross-axis precedence pin (inside the Vec-shaped universal
4577        // metadata pair): a caixa with both a malformed `:etiquetas`
4578        // entry *and* a malformed `:autores` entry surfaces
4579        // `EtiquetasViolation` first — `:etiquetas` is the fourth
4580        // universal axis in the cascade and runs before `:autores`,
4581        // peer with the canonical identity-axis-first cascade the
4582        // peer gates establish. Mirrors the peer
4583        // `etiquetas_violation_fires_after_deps_violation` precedence
4584        // pin (360a499) on the dep-axis-before-tag-axis pair.
4585        let root = PathBuf::from("/tmp/x");
4586        let manifest = root.join("caixa.lisp");
4587        let default_lib = root.join("lib").join("demo.lisp");
4588        let layout =
4589            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4590        let mut c = caixa(CaixaKind::Biblioteca);
4591        c.etiquetas = vec![String::new()];
4592        c.autores = vec![String::new()];
4593        let err = layout.verify(&c, &root).unwrap_err();
4594        assert!(
4595            matches!(err, LayoutError::EtiquetasViolation { .. }),
4596            "got {err:?}",
4597        );
4598    }
4599
4600    #[test]
4601    fn autores_violation_accepts_canonical_template() {
4602        // Positive control sanity pin: the canonical `Caixa::template`
4603        // shape (`:autores ()` — empty list) passes the gate trivially.
4604        // Mirrors the peer `etiquetas_violation_accepts_canonical_template`
4605        // pin (360a499).
4606        let root = PathBuf::from("/tmp/x");
4607        let manifest = root.join("caixa.lisp");
4608        let default_lib = root.join("lib").join("demo.lisp");
4609        let layout =
4610            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4611        let c = caixa(CaixaKind::Biblioteca);
4612        layout.verify(&c, &root).expect("template must pass");
4613    }
4614
4615    #[test]
4616    fn autores_violation_on_non_chart_maintainer_shape() {
4617        // Canonical paste-from-multiline-doc footgun: the author
4618        // pasted a multi-line block of author records into one
4619        // `:autores` entry instead of splitting into one entry per
4620        // author. The shape gate fires past the empty + duplicate arms
4621        // via [`Caixa::validate_autores`]'s new
4622        // `is_chart_maintainer_name_shape` cascade, and the layout
4623        // envelope wraps [`ManifestError::AutorInvalid`]'s Display
4624        // through verbatim — the issue string names both the offending
4625        // slot and the offending value (debug-escaped). Peer with the
4626        // `descricao_violation_on_non_chart_shape` pin on the sibling
4627        // universal-axis `Option<String>` surface and the
4628        // `licenca_violation_on_non_spdx_shape` /
4629        // `edicao_violation_on_non_year_shape` peers — and the first
4630        // layout pin on the Vec<String> per-entry shape cascade.
4631        let root = PathBuf::from("/tmp/x");
4632        let manifest = root.join("caixa.lisp");
4633        let default_lib = root.join("lib").join("demo.lisp");
4634        let layout =
4635            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4636        let mut c = caixa(CaixaKind::Biblioteca);
4637        c.autores = vec!["alice\nbob".into()];
4638        let err = layout.verify(&c, &root).unwrap_err();
4639        let LayoutError::AutoresViolation { caixa, issue } = err else {
4640            panic!("expected LayoutError::AutoresViolation, got {err:?}");
4641        };
4642        assert_eq!(caixa, "demo");
4643        assert!(
4644            issue.contains(":autores"),
4645            "issue must name the offending slot: {issue}",
4646        );
4647        assert!(
4648            issue.contains("alice\\nbob"),
4649            "issue must quote the offending value (debug-escaped): {issue}",
4650        );
4651    }
4652
4653    // ── repositorio universal-axis gate wired into verify ────────────────
4654    //
4655    // Pins the layout-pipeline wire-up of [`Caixa::validate_repositorio`]:
4656    // the sixth universal-axis Caixa-level value-shape gate (peer of
4657    // `validate_nome` / `validate_versao` / `validate_deps` /
4658    // `validate_etiquetas` / `validate_autores` / `validate_code_paths`),
4659    // wired immediately after `validate_autores` so the universal
4660    // git-URL axis sits adjacent to the two Vec-shaped universal
4661    // metadata axes (`:etiquetas`, `:autores`) in the cascade. Until
4662    // this wire-up landed `:repositorio` had no shape gate at any
4663    // layer — the universal git-shaped homepage axis was the third
4664    // largest universal authoring surface on the typed Caixa with no
4665    // validate discipline, routing the same string through two
4666    // load-bearing substrate consumers (`caixa-helm`'s `Chart.yaml
4667    // home:` field and `caixa-flux`'s FluxCD `GitRepository.spec.url`)
4668    // via `Option::unwrap_or_else` fallbacks that only fire on `None` —
4669    // a `Some("")` silently passed every fallback and rendered as an
4670    // empty URL in both consumers, breaking at `helm template` /
4671    // FluxCD reconcile time far from the source `caixa.lisp`. The
4672    // gate closes the divergence and makes the two `git URL`-shaped
4673    // surfaces on the typed Caixa (`:repositorio` here, `:deps :fonte
4674    // :repo` peer routed through the same shared
4675    // `crate::render::is_git_repo_url` predicate) structurally
4676    // equivalent by construction.
4677
4678    #[test]
4679    fn repositorio_violation_on_empty_some() {
4680        // Canonical paste-from-blank-doc footgun on every kind. The
4681        // wrap envelope wraps [`ManifestError::RepositorioEmpty`]'s
4682        // Display through verbatim, so the issue string names the
4683        // offending `:repositorio` axis at the source — the author
4684        // can grep their caixa.lisp for `:repositorio ""` and fix the
4685        // empty value in one edit. Mirrors the peer
4686        // `autores_violation_on_empty_entry` shape (86c769b) on the
4687        // `:autores` axis.
4688        let root = PathBuf::from("/tmp/x");
4689        let manifest = root.join("caixa.lisp");
4690        let default_lib = root.join("lib").join("demo.lisp");
4691        let layout =
4692            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4693        let mut c = caixa(CaixaKind::Biblioteca);
4694        c.repositorio = Some(String::new());
4695        let err = layout.verify(&c, &root).unwrap_err();
4696        let LayoutError::RepositorioViolation { caixa, issue } = err else {
4697            panic!("expected LayoutError::RepositorioViolation, got {err:?}");
4698        };
4699        assert_eq!(caixa, "demo");
4700        assert!(
4701            issue.contains(":repositorio"),
4702            "issue must name the offending slot: {issue}",
4703        );
4704    }
4705
4706    #[test]
4707    fn repositorio_violation_on_malformed_shape() {
4708        // Canonical CLI-argument-injection footgun: a leading `-`
4709        // value (`-upload-pack=evil`) escapes the `git clone <repo>`
4710        // subprocess argument boundary at clone time. The shared
4711        // `is_git_repo_url` predicate — the same parser the peer
4712        // `:deps :fonte :repo` axis routes through via
4713        // `DepSource::validate` — refuses every leading-`-` shape at
4714        // validate time. The wrap envelope names the offending value
4715        // verbatim through the inner [`ManifestError::RepositorioInvalid`]'s
4716        // Display.
4717        let root = PathBuf::from("/tmp/x");
4718        let manifest = root.join("caixa.lisp");
4719        let default_lib = root.join("lib").join("demo.lisp");
4720        let layout =
4721            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4722        let mut c = caixa(CaixaKind::Biblioteca);
4723        c.repositorio = Some("-upload-pack=evil".into());
4724        let err = layout.verify(&c, &root).unwrap_err();
4725        let LayoutError::RepositorioViolation { caixa, issue } = err else {
4726            panic!("expected LayoutError::RepositorioViolation, got {err:?}");
4727        };
4728        assert_eq!(caixa, "demo");
4729        assert!(
4730            issue.contains("-upload-pack=evil"),
4731            "issue must quote the offending value: {issue}",
4732        );
4733    }
4734
4735    #[test]
4736    fn repositorio_violation_fires_before_kind_coherence_mesh_slot() {
4737        // Cross-axis precedence pin: a Biblioteca with malformed
4738        // `:repositorio` *and* declared mesh slots (`:membros`)
4739        // surfaces the universal `:repositorio` diagnostic first, not
4740        // the kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4741        // `:repositorio` is universal (every kind owns the slot), so
4742        // its shape diagnostic is more fundamental than the
4743        // partition-on-kind diagnostic. Mirrors the peer
4744        // `autores_violation_fires_before_kind_coherence_mesh_slot`
4745        // pin (86c769b) on the `:autores` axis vs the same
4746        // kind-coherence gates.
4747        let root = PathBuf::from("/tmp/x");
4748        let manifest = root.join("caixa.lisp");
4749        let default_lib = root.join("lib").join("demo.lisp");
4750        let layout =
4751            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4752        let mut c = caixa(CaixaKind::Biblioteca);
4753        c.repositorio = Some(String::new());
4754        c.membros = vec![crate::aplicacao::Membro {
4755            caixa: "x".into(),
4756            versao: "^0.1".into(),
4757        }];
4758        let err = layout.verify(&c, &root).unwrap_err();
4759        assert!(
4760            matches!(err, LayoutError::RepositorioViolation { .. }),
4761            "got {err:?}",
4762        );
4763    }
4764
4765    #[test]
4766    fn repositorio_violation_fires_after_autores_violation() {
4767        // Cross-axis precedence pin (inside the universal metadata
4768        // trio): a caixa with both a malformed `:autores` entry *and*
4769        // a malformed `:repositorio` value surfaces `AutoresViolation`
4770        // first — `:autores` is the fifth universal axis in the
4771        // cascade and runs before `:repositorio`, peer with the
4772        // canonical identity-axis-first cascade the peer gates
4773        // establish. Mirrors the peer
4774        // `autores_violation_fires_after_etiquetas_violation`
4775        // precedence pin (86c769b) on the tag-axis-before-author-axis
4776        // pair.
4777        let root = PathBuf::from("/tmp/x");
4778        let manifest = root.join("caixa.lisp");
4779        let default_lib = root.join("lib").join("demo.lisp");
4780        let layout =
4781            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4782        let mut c = caixa(CaixaKind::Biblioteca);
4783        c.autores = vec![String::new()];
4784        c.repositorio = Some(String::new());
4785        let err = layout.verify(&c, &root).unwrap_err();
4786        assert!(
4787            matches!(err, LayoutError::AutoresViolation { .. }),
4788            "got {err:?}",
4789        );
4790    }
4791
4792    #[test]
4793    fn repositorio_violation_accepts_canonical_template() {
4794        // Positive control sanity pin: the canonical `Caixa::template`
4795        // shape (omits `:repositorio` entirely → `None` on the typed
4796        // surface) passes the gate trivially — the gate is a no-op
4797        // when the author didn't author a value. Mirrors the peer
4798        // `autores_violation_accepts_canonical_template` pin (86c769b).
4799        let root = PathBuf::from("/tmp/x");
4800        let manifest = root.join("caixa.lisp");
4801        let default_lib = root.join("lib").join("demo.lisp");
4802        let layout =
4803            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4804        let c = caixa(CaixaKind::Biblioteca);
4805        layout.verify(&c, &root).expect("template must pass");
4806    }
4807
4808    #[test]
4809    fn repositorio_violation_accepts_canonical_github_shorthand() {
4810        // Positive control pin on the canonical pleme-io `:repositorio`
4811        // shape: the `github:org/repo` shorthand the README quickstart
4812        // and the `caixa-helm` / `caixa-mesh` / `caixa-flux` fixtures
4813        // all use passes the gate end-to-end. Closes the structural
4814        // equivalence between this surface and the peer `:deps :fonte
4815        // :repo` axis — both consume `crate::render::is_git_repo_url`
4816        // and both must agree on the same accepted shape set.
4817        let root = PathBuf::from("/tmp/x");
4818        let manifest = root.join("caixa.lisp");
4819        let default_lib = root.join("lib").join("demo.lisp");
4820        let layout =
4821            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4822        let mut c = caixa(CaixaKind::Biblioteca);
4823        c.repositorio = Some("github:pleme-io/hello-rio".into());
4824        layout.verify(&c, &root).expect("canonical shape must pass");
4825    }
4826
4827    // ── descricao universal-axis gate wired into verify ──────────────────
4828    //
4829    // Pins the layout-pipeline wire-up of [`Caixa::validate_descricao`]:
4830    // the seventh universal-axis Caixa-level value-shape gate (peer of
4831    // `validate_nome` / `validate_versao` / `validate_deps` /
4832    // `validate_etiquetas` / `validate_autores` / `validate_repositorio` /
4833    // `validate_code_paths`), wired immediately after `validate_repositorio`
4834    // so the universal free-form-prose axis sits adjacent to the
4835    // universal git-URL axis in the cascade. Until this wire-up landed
4836    // `:descricao` had no shape gate at any layer — the empty
4837    // `Some("")` silently passed both `caixa-helm` consumers'
4838    // `Option::unwrap_or_else(|| <fallback>)` (which only fire on
4839    // `None`) and rendered as `Chart.yaml description: ""` plus a
4840    // blank `README.md` header, breaking at `helm lint` time
4841    // (`WARNING [chart.metadata.description]: description is required`
4842    // on `apiVersion: v2` charts) far from the source `caixa.lisp`.
4843    // Closes the same `Some("")` skips-`unwrap_or_else` footgun the
4844    // peer `:repositorio` gate (577b0a9) closed, on the universal
4845    // free-form-prose summary axis.
4846
4847    #[test]
4848    fn descricao_violation_on_empty_some() {
4849        // Canonical paste-from-blank-doc footgun on every kind. The
4850        // wrap envelope wraps [`ManifestError::DescricaoEmpty`]'s
4851        // Display through verbatim, so the issue string names the
4852        // offending `:descricao` axis at the source — the author can
4853        // grep their caixa.lisp for `:descricao ""` and fix the empty
4854        // value in one edit. Mirrors the peer
4855        // `repositorio_violation_on_empty_some` shape (577b0a9) on
4856        // the sibling `Option<String>` `:repositorio` axis.
4857        let root = PathBuf::from("/tmp/x");
4858        let manifest = root.join("caixa.lisp");
4859        let default_lib = root.join("lib").join("demo.lisp");
4860        let layout =
4861            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4862        let mut c = caixa(CaixaKind::Biblioteca);
4863        c.descricao = Some(String::new());
4864        let err = layout.verify(&c, &root).unwrap_err();
4865        let LayoutError::DescricaoViolation { caixa, issue } = err else {
4866            panic!("expected LayoutError::DescricaoViolation, got {err:?}");
4867        };
4868        assert_eq!(caixa, "demo");
4869        assert!(
4870            issue.contains(":descricao"),
4871            "issue must name the offending slot: {issue}",
4872        );
4873    }
4874
4875    #[test]
4876    fn descricao_violation_fires_before_kind_coherence_mesh_slot() {
4877        // Cross-axis precedence pin: a Biblioteca with empty
4878        // `:descricao` *and* declared mesh slots (`:membros`)
4879        // surfaces the universal `:descricao` diagnostic first, not
4880        // the kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4881        // `:descricao` is universal (every kind owns the slot), so
4882        // its shape diagnostic is more fundamental than the
4883        // partition-on-kind diagnostic. Mirrors the peer
4884        // `repositorio_violation_fires_before_kind_coherence_mesh_slot`
4885        // pin (577b0a9) on the `:repositorio` axis vs the same
4886        // kind-coherence gates.
4887        let root = PathBuf::from("/tmp/x");
4888        let manifest = root.join("caixa.lisp");
4889        let default_lib = root.join("lib").join("demo.lisp");
4890        let layout =
4891            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4892        let mut c = caixa(CaixaKind::Biblioteca);
4893        c.descricao = Some(String::new());
4894        c.membros = vec![crate::aplicacao::Membro {
4895            caixa: "x".into(),
4896            versao: "^0.1".into(),
4897        }];
4898        let err = layout.verify(&c, &root).unwrap_err();
4899        assert!(
4900            matches!(err, LayoutError::DescricaoViolation { .. }),
4901            "got {err:?}",
4902        );
4903    }
4904
4905    #[test]
4906    fn descricao_violation_fires_after_repositorio_violation() {
4907        // Cross-axis precedence pin (inside the universal metadata
4908        // cascade): a caixa with both a malformed `:repositorio` *and*
4909        // an empty `:descricao` surfaces `RepositorioViolation`
4910        // first — `:repositorio` is the sixth universal axis in the
4911        // cascade and runs before `:descricao`, peer with the
4912        // canonical identity-axis-first cascade the peer gates
4913        // establish. Mirrors the peer
4914        // `repositorio_violation_fires_after_autores_violation`
4915        // precedence pin (577b0a9) on the autores-axis-before-
4916        // repositorio-axis pair.
4917        let root = PathBuf::from("/tmp/x");
4918        let manifest = root.join("caixa.lisp");
4919        let default_lib = root.join("lib").join("demo.lisp");
4920        let layout =
4921            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4922        let mut c = caixa(CaixaKind::Biblioteca);
4923        c.repositorio = Some(String::new());
4924        c.descricao = Some(String::new());
4925        let err = layout.verify(&c, &root).unwrap_err();
4926        assert!(
4927            matches!(err, LayoutError::RepositorioViolation { .. }),
4928            "got {err:?}",
4929        );
4930    }
4931
4932    #[test]
4933    fn descricao_violation_accepts_none() {
4934        // Positive control sanity pin: a caixa that omits
4935        // `:descricao` entirely (the canonical `Caixa::template` shape
4936        // carries `Some("FIXME — describe this caixa")`, but the
4937        // layout-test fixture defaults to `None`) passes the gate
4938        // trivially — the gate is a no-op when the author didn't
4939        // author a value. Mirrors the peer
4940        // `repositorio_violation_accepts_canonical_template` pin
4941        // (577b0a9).
4942        let root = PathBuf::from("/tmp/x");
4943        let manifest = root.join("caixa.lisp");
4944        let default_lib = root.join("lib").join("demo.lisp");
4945        let layout =
4946            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4947        let c = caixa(CaixaKind::Biblioteca);
4948        layout.verify(&c, &root).expect("None must pass");
4949    }
4950
4951    #[test]
4952    fn descricao_violation_accepts_canonical_summary() {
4953        // Positive control pin on the canonical pleme-io `:descricao`
4954        // shape: a short free-form prose summary the `caixa-helm` /
4955        // `caixa-flux` / `caixa-mesh` fixtures all carry passes the
4956        // gate end-to-end.
4957        let root = PathBuf::from("/tmp/x");
4958        let manifest = root.join("caixa.lisp");
4959        let default_lib = root.join("lib").join("demo.lisp");
4960        let layout =
4961            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4962        let mut c = caixa(CaixaKind::Biblioteca);
4963        c.descricao = Some("Canonical Rust→wasm32-wasip2 caixa Servico.".into());
4964        layout
4965            .verify(&c, &root)
4966            .expect("canonical summary must pass");
4967    }
4968
4969    #[test]
4970    fn descricao_violation_on_non_chart_shape() {
4971        // Shape-predicate wire-up pin: a malformed `:descricao` value
4972        // that's a non-empty `Some(s)` but carries a paste-from-
4973        // multiline-doc embedded newline surfaces the
4974        // `DescricaoViolation` envelope via the manifest-layer
4975        // `ManifestError::DescricaoInvalid` arm. Mirrors the peer
4976        // `descricao_violation_on_empty_some` shape on the empty arm
4977        // of the same axis and the peer
4978        // `licenca_violation_on_non_spdx_shape` shape on the sibling
4979        // `:licenca` axis. Until this gate landed a value like
4980        // `"Checkout\nflow."` (an embedded newline) or `"Checkout
4981        // flow. "` (a trailing whitespace) silently passed
4982        // `StandardLayout::verify` and landed in the rendered
4983        // Chart.yaml `description:` field as a YAML-illegal
4984        // multi-line scalar or a silently-trimmed whitespace
4985        // round-trip far from the source caixa.lisp.
4986        let root = PathBuf::from("/tmp/x");
4987        let manifest = root.join("caixa.lisp");
4988        let default_lib = root.join("lib").join("demo.lisp");
4989        let layout =
4990            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4991        let mut c = caixa(CaixaKind::Biblioteca);
4992        c.descricao = Some("Checkout\nflow.".into());
4993        let err = layout.verify(&c, &root).unwrap_err();
4994        let LayoutError::DescricaoViolation { caixa, issue } = err else {
4995            panic!("expected LayoutError::DescricaoViolation, got {err:?}");
4996        };
4997        assert_eq!(caixa, "demo");
4998        assert!(
4999            issue.contains(":descricao"),
5000            "issue must name the offending slot: {issue}",
5001        );
5002        // The wrapped `ManifestError::DescricaoInvalid` Display uses
5003        // `{descricao:?}` (Debug) so the embedded newline surfaces
5004        // debug-escaped as `\n` in the issue string.
5005        assert!(
5006            issue.contains("Checkout\\nflow."),
5007            "issue must quote the offending value (debug-escaped): {issue}",
5008        );
5009    }
5010
5011    // ── :licenca empty-Some shape wired into verify (universal axis) ──
5012    //
5013    // Until this wire-up landed `Caixa::validate_licenca` did not
5014    // exist — the universal SPDX-shaped license-expression axis had
5015    // no shape gate at any layer, so an empty `Some("")` silently
5016    // passed `Caixa::from_lisp` and `StandardLayout::verify` and
5017    // landed as a bare trailing period in the rendered
5018    // `lareira-<nome>` chart's `README.md` `## License` section via
5019    // the `caixa-helm` consumer's `caixa.licenca.clone().unwrap_or_else(||
5020    // "MIT".into())` (which only fires on `None`) at
5021    // `caixa-helm/src/lib.rs:361`. Closes the same `Some("")`
5022    // skips-`unwrap_or_else` footgun the peer `:repositorio`
5023    // (577b0a9) and `:descricao` (4e6db38) gates closed, on the
5024    // universal license-expression axis.
5025
5026    #[test]
5027    fn licenca_violation_on_empty_some() {
5028        // Canonical paste-from-blank-doc footgun on every kind. The
5029        // wrap envelope wraps [`ManifestError::LicencaEmpty`]'s
5030        // Display through verbatim, so the issue string names the
5031        // offending `:licenca` axis at the source — the author can
5032        // grep their caixa.lisp for `:licenca ""` and fix the empty
5033        // value in one edit. Mirrors the peer
5034        // `descricao_violation_on_empty_some` shape (4e6db38) on
5035        // the sibling `Option<String>` `:licenca` axis.
5036        let root = PathBuf::from("/tmp/x");
5037        let manifest = root.join("caixa.lisp");
5038        let default_lib = root.join("lib").join("demo.lisp");
5039        let layout =
5040            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5041        let mut c = caixa(CaixaKind::Biblioteca);
5042        c.licenca = Some(String::new());
5043        let err = layout.verify(&c, &root).unwrap_err();
5044        let LayoutError::LicencaViolation { caixa, issue } = err else {
5045            panic!("expected LayoutError::LicencaViolation, got {err:?}");
5046        };
5047        assert_eq!(caixa, "demo");
5048        assert!(
5049            issue.contains(":licenca"),
5050            "issue must name the offending slot: {issue}",
5051        );
5052    }
5053
5054    #[test]
5055    fn licenca_violation_fires_before_kind_coherence_mesh_slot() {
5056        // Cross-axis precedence pin: a Biblioteca with empty
5057        // `:licenca` *and* declared mesh slots (`:membros`)
5058        // surfaces the universal `:licenca` diagnostic first, not
5059        // the kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
5060        // `:licenca` is universal (every kind owns the slot), so
5061        // its shape diagnostic is more fundamental than the
5062        // partition-on-kind diagnostic. Mirrors the peer
5063        // `descricao_violation_fires_before_kind_coherence_mesh_slot`
5064        // pin (4e6db38) on the `:descricao` axis vs the same
5065        // kind-coherence gates.
5066        let root = PathBuf::from("/tmp/x");
5067        let manifest = root.join("caixa.lisp");
5068        let default_lib = root.join("lib").join("demo.lisp");
5069        let layout =
5070            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5071        let mut c = caixa(CaixaKind::Biblioteca);
5072        c.licenca = Some(String::new());
5073        c.membros = vec![crate::aplicacao::Membro {
5074            caixa: "x".into(),
5075            versao: "^0.1".into(),
5076        }];
5077        let err = layout.verify(&c, &root).unwrap_err();
5078        assert!(
5079            matches!(err, LayoutError::LicencaViolation { .. }),
5080            "got {err:?}",
5081        );
5082    }
5083
5084    #[test]
5085    fn licenca_violation_fires_after_descricao_violation() {
5086        // Cross-axis precedence pin (inside the universal metadata
5087        // cascade): a caixa with both an empty `:descricao` *and*
5088        // an empty `:licenca` surfaces `DescricaoViolation`
5089        // first — `:descricao` is the seventh universal axis in the
5090        // cascade and runs before `:licenca`, peer with the
5091        // canonical identity-axis-first cascade the peer gates
5092        // establish. Mirrors the peer
5093        // `descricao_violation_fires_after_repositorio_violation`
5094        // precedence pin (4e6db38) on the repositorio-axis-before-
5095        // descricao-axis pair.
5096        let root = PathBuf::from("/tmp/x");
5097        let manifest = root.join("caixa.lisp");
5098        let default_lib = root.join("lib").join("demo.lisp");
5099        let layout =
5100            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5101        let mut c = caixa(CaixaKind::Biblioteca);
5102        c.descricao = Some(String::new());
5103        c.licenca = Some(String::new());
5104        let err = layout.verify(&c, &root).unwrap_err();
5105        assert!(
5106            matches!(err, LayoutError::DescricaoViolation { .. }),
5107            "got {err:?}",
5108        );
5109    }
5110
5111    #[test]
5112    fn licenca_violation_accepts_none() {
5113        // Positive control sanity pin: a caixa that omits `:licenca`
5114        // entirely (the layout-test fixture defaults to `None`)
5115        // passes the gate trivially — the gate is a no-op when the
5116        // author didn't author a value. Mirrors the peer
5117        // `descricao_violation_accepts_none` pin (4e6db38).
5118        let root = PathBuf::from("/tmp/x");
5119        let manifest = root.join("caixa.lisp");
5120        let default_lib = root.join("lib").join("demo.lisp");
5121        let layout =
5122            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5123        let c = caixa(CaixaKind::Biblioteca);
5124        layout.verify(&c, &root).expect("None must pass");
5125    }
5126
5127    #[test]
5128    fn licenca_violation_accepts_canonical_expression() {
5129        // Positive control pin on the canonical pleme-io `:licenca`
5130        // shape: a non-empty SPDX expression the `caixa-helm` /
5131        // `caixa-flux` / `caixa-mesh` fixtures all carry (`"MIT"`)
5132        // passes the gate end-to-end.
5133        let root = PathBuf::from("/tmp/x");
5134        let manifest = root.join("caixa.lisp");
5135        let default_lib = root.join("lib").join("demo.lisp");
5136        let layout =
5137            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5138        let mut c = caixa(CaixaKind::Biblioteca);
5139        c.licenca = Some("Apache-2.0 OR MIT".into());
5140        layout
5141            .verify(&c, &root)
5142            .expect("canonical SPDX expression must pass");
5143    }
5144
5145    #[test]
5146    fn licenca_violation_on_non_spdx_shape() {
5147        // Shape-predicate wire-up pin: a malformed `:licenca` value
5148        // that's a non-empty `Some(s)` but falls outside the SPDX
5149        // expression alphabet floor surfaces the `LicencaViolation`
5150        // envelope via the manifest-layer `ManifestError::LicencaInvalid`
5151        // arm. Mirrors the peer `licenca_violation_on_empty_some`
5152        // shape on the empty arm of the same axis and the peer
5153        // `edicao_violation_on_non_year_shape` shape on the sibling
5154        // `:edicao` axis. Until this gate landed a value like
5155        // `"Apache_2.0"` (an underscore-instead-of-hyphen typo) or
5156        // `"MIT, Apache-2.0"` (a comma-instead-of-`OR`-keyword
5157        // colloquial idiom) silently passed `StandardLayout::verify`
5158        // and landed in the rendered chart `README.md` `## License`
5159        // section + a future SPDX-aware Chart.yaml `license:`
5160        // emitter would refuse the value at `helm lint` time far
5161        // from the source caixa.lisp.
5162        let root = PathBuf::from("/tmp/x");
5163        let manifest = root.join("caixa.lisp");
5164        let default_lib = root.join("lib").join("demo.lisp");
5165        let layout =
5166            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5167        let mut c = caixa(CaixaKind::Biblioteca);
5168        c.licenca = Some("Apache_2.0".into());
5169        let err = layout.verify(&c, &root).unwrap_err();
5170        let LayoutError::LicencaViolation { caixa, issue } = err else {
5171            panic!("expected LayoutError::LicencaViolation, got {err:?}");
5172        };
5173        assert_eq!(caixa, "demo");
5174        assert!(
5175            issue.contains(":licenca"),
5176            "issue must name the offending slot: {issue}",
5177        );
5178        assert!(
5179            issue.contains("Apache_2.0"),
5180            "issue must quote the offending value: {issue}",
5181        );
5182    }
5183
5184    // ── :edicao empty-Some shape wired into verify (universal axis) ──
5185    //
5186    // Until this wire-up landed `Caixa::validate_edicao` did not
5187    // exist — the universal language-edition axis had no shape gate
5188    // at any layer, so an empty `Some("")` silently passed
5189    // `Caixa::from_lisp` and `StandardLayout::verify` and landed as a
5190    // bare `(:edicao "")` line in the rendered caixa.lisp, ready for
5191    // a future renderer-side consumer's `Option::unwrap_or_else`
5192    // (which only fires on `None`) to skip its fallback. Closes the
5193    // same `Some("")`-skips-`unwrap_or_else` footgun the peer
5194    // `:repositorio` (577b0a9), `:descricao` (4e6db38), and
5195    // `:licenca` (3d1e535) gates closed, on the universal language-
5196    // edition axis — the last un-gated universal-axis
5197    // `Option<String>` Caixa-level value-shape surface.
5198
5199    #[test]
5200    fn edicao_violation_on_empty_some() {
5201        // Canonical paste-from-blank-doc footgun on every kind. The
5202        // wrap envelope wraps [`ManifestError::EdicaoEmpty`]'s
5203        // Display through verbatim, so the issue string names the
5204        // offending `:edicao` axis at the source — the author can
5205        // grep their caixa.lisp for `:edicao ""` and fix the empty
5206        // value in one edit. Mirrors the peer
5207        // `licenca_violation_on_empty_some` shape (3d1e535) on the
5208        // sibling `Option<String>` `:edicao` axis.
5209        let root = PathBuf::from("/tmp/x");
5210        let manifest = root.join("caixa.lisp");
5211        let default_lib = root.join("lib").join("demo.lisp");
5212        let layout =
5213            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5214        let mut c = caixa(CaixaKind::Biblioteca);
5215        c.edicao = Some(String::new());
5216        let err = layout.verify(&c, &root).unwrap_err();
5217        let LayoutError::EdicaoViolation { caixa, issue } = err else {
5218            panic!("expected LayoutError::EdicaoViolation, got {err:?}");
5219        };
5220        assert_eq!(caixa, "demo");
5221        assert!(
5222            issue.contains(":edicao"),
5223            "issue must name the offending slot: {issue}",
5224        );
5225    }
5226
5227    #[test]
5228    fn edicao_violation_fires_before_kind_coherence_mesh_slot() {
5229        // Cross-axis precedence pin: a Biblioteca with empty
5230        // `:edicao` *and* declared mesh slots (`:membros`) surfaces
5231        // the universal `:edicao` diagnostic first, not the
5232        // kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
5233        // `:edicao` is universal (every kind owns the slot), so
5234        // its shape diagnostic is more fundamental than the
5235        // partition-on-kind diagnostic. Mirrors the peer
5236        // `licenca_violation_fires_before_kind_coherence_mesh_slot`
5237        // pin (3d1e535) on the `:licenca` axis vs the same
5238        // kind-coherence gates.
5239        let root = PathBuf::from("/tmp/x");
5240        let manifest = root.join("caixa.lisp");
5241        let default_lib = root.join("lib").join("demo.lisp");
5242        let layout =
5243            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5244        let mut c = caixa(CaixaKind::Biblioteca);
5245        c.edicao = Some(String::new());
5246        c.membros = vec![crate::aplicacao::Membro {
5247            caixa: "x".into(),
5248            versao: "^0.1".into(),
5249        }];
5250        let err = layout.verify(&c, &root).unwrap_err();
5251        assert!(
5252            matches!(err, LayoutError::EdicaoViolation { .. }),
5253            "got {err:?}",
5254        );
5255    }
5256
5257    #[test]
5258    fn edicao_violation_fires_after_licenca_violation() {
5259        // Cross-axis precedence pin (inside the universal metadata
5260        // cascade): a caixa with both an empty `:licenca` *and* an
5261        // empty `:edicao` surfaces `LicencaViolation` first —
5262        // `:licenca` is the eighth universal axis in the cascade
5263        // and runs before `:edicao`, peer with the canonical
5264        // identity-axis-first cascade the peer gates establish.
5265        // Mirrors the peer
5266        // `licenca_violation_fires_after_descricao_violation`
5267        // precedence pin (3d1e535) on the descricao-axis-before-
5268        // licenca-axis pair.
5269        let root = PathBuf::from("/tmp/x");
5270        let manifest = root.join("caixa.lisp");
5271        let default_lib = root.join("lib").join("demo.lisp");
5272        let layout =
5273            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5274        let mut c = caixa(CaixaKind::Biblioteca);
5275        c.licenca = Some(String::new());
5276        c.edicao = Some(String::new());
5277        let err = layout.verify(&c, &root).unwrap_err();
5278        assert!(
5279            matches!(err, LayoutError::LicencaViolation { .. }),
5280            "got {err:?}",
5281        );
5282    }
5283
5284    #[test]
5285    fn edicao_violation_accepts_none() {
5286        // Positive control sanity pin: a caixa that omits `:edicao`
5287        // entirely (the layout-test fixture defaults to `None`)
5288        // passes the gate trivially — the gate is a no-op when the
5289        // author didn't author a value. Mirrors the peer
5290        // `licenca_violation_accepts_none` pin (3d1e535).
5291        let root = PathBuf::from("/tmp/x");
5292        let manifest = root.join("caixa.lisp");
5293        let default_lib = root.join("lib").join("demo.lisp");
5294        let layout =
5295            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5296        let c = caixa(CaixaKind::Biblioteca);
5297        layout.verify(&c, &root).expect("None must pass");
5298    }
5299
5300    #[test]
5301    fn edicao_violation_accepts_canonical_value() {
5302        // Positive control pin on the canonical pleme-io `:edicao`
5303        // shape: the `"2026"` edition every `caixa-helm` /
5304        // `caixa-flux` / `caixa-mesh` / `caixa-core/src/render.rs`
5305        // fixture carries by construction passes the gate end-to-end.
5306        let root = PathBuf::from("/tmp/x");
5307        let manifest = root.join("caixa.lisp");
5308        let default_lib = root.join("lib").join("demo.lisp");
5309        let layout =
5310            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5311        let mut c = caixa(CaixaKind::Biblioteca);
5312        c.edicao = Some("2026".into());
5313        layout
5314            .verify(&c, &root)
5315            .expect("canonical edition must pass");
5316    }
5317
5318    #[test]
5319    fn edicao_violation_on_non_year_shape() {
5320        // Shape-predicate wire-up pin: a malformed `:edicao` value
5321        // that's a non-empty `Some(s)` but not a 4-digit ASCII
5322        // decimal year surfaces the `EdicaoViolation` envelope via
5323        // the manifest-layer `ManifestError::EdicaoInvalid` arm.
5324        // Mirrors the peer `edicao_violation_on_empty_some` shape
5325        // on the empty arm of the same axis. Until this gate landed
5326        // a value like `"v2026"` (a familiar git-tag idiom that
5327        // doesn't apply to the year-shaped edition axis) silently
5328        // passed `StandardLayout::verify` and broke at the
5329        // substrate's build-time edition selector far from the
5330        // source caixa.lisp.
5331        let root = PathBuf::from("/tmp/x");
5332        let manifest = root.join("caixa.lisp");
5333        let default_lib = root.join("lib").join("demo.lisp");
5334        let layout =
5335            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5336        let mut c = caixa(CaixaKind::Biblioteca);
5337        c.edicao = Some("v2026".into());
5338        let err = layout.verify(&c, &root).unwrap_err();
5339        let LayoutError::EdicaoViolation { caixa, issue } = err else {
5340            panic!("expected LayoutError::EdicaoViolation, got {err:?}");
5341        };
5342        assert_eq!(caixa, "demo");
5343        assert!(
5344            issue.contains(":edicao"),
5345            "issue must name the offending slot: {issue}",
5346        );
5347        assert!(
5348            issue.contains("v2026"),
5349            "issue must quote the offending value: {issue}",
5350        );
5351    }
5352
5353    // ── Caixa-identity gates (`:nome`, `:versao`) wired into verify ────
5354    //
5355    // Until this wire-up landed `Caixa::validate_nome` and
5356    // `Caixa::validate_versao` lived as `pub fn` on `Caixa` with full
5357    // per-arm unit coverage in `manifest::tests`, but no production
5358    // path called them — `feira build` silently accepted malformed
5359    // `:nome` / `:versao` and the failure surfaced at `helm install` /
5360    // `kubectl apply` / `feira publish` / lacre-resolve / `:upgrade-from
5361    // :from` matching time, far from the source `caixa.lisp`. The
5362    // following pins fence the layout-pipeline wire-up: every layout
5363    // verify on a structurally-invalid Caixa identity axis surfaces
5364    // the per-axis `*Violation { caixa, issue }` envelope before any
5365    // kind-coherence, code-path, or downstream gate sees it.
5366
5367    #[test]
5368    fn nome_violation_on_uppercase() {
5369        let root = PathBuf::from("/tmp/x");
5370        let manifest = root.join("caixa.lisp");
5371        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5372        let mut c = caixa(CaixaKind::Biblioteca);
5373        c.nome = "MyApp".into();
5374        let err = layout.verify(&c, &root).unwrap_err();
5375        let LayoutError::NomeViolation { caixa, issue } = err else {
5376            panic!("expected LayoutError::NomeViolation, got {err:?}");
5377        };
5378        assert_eq!(caixa, "MyApp");
5379        assert!(
5380            issue.contains("MyApp"),
5381            "issue must quote the offending nome: {issue}",
5382        );
5383    }
5384
5385    #[test]
5386    fn nome_violation_on_underscore() {
5387        let root = PathBuf::from("/tmp/x");
5388        let manifest = root.join("caixa.lisp");
5389        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5390        let mut c = caixa(CaixaKind::Biblioteca);
5391        c.nome = "my_app".into();
5392        let err = layout.verify(&c, &root).unwrap_err();
5393        assert!(
5394            matches!(err, LayoutError::NomeViolation { ref caixa, .. } if caixa == "my_app"),
5395            "got {err:?}",
5396        );
5397    }
5398
5399    #[test]
5400    fn nome_violation_on_empty() {
5401        // Empty `:nome` surfaces NomeViolation wrapping the narrower
5402        // `ManifestError::NomeEmpty` arm — the empty-first cascade the
5403        // peer per-axis name gates already use.
5404        let root = PathBuf::from("/tmp/x");
5405        let manifest = root.join("caixa.lisp");
5406        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5407        let mut c = caixa(CaixaKind::Biblioteca);
5408        c.nome = String::new();
5409        let err = layout.verify(&c, &root).unwrap_err();
5410        let LayoutError::NomeViolation { caixa, issue } = err else {
5411            panic!("expected LayoutError::NomeViolation, got {err:?}");
5412        };
5413        assert!(caixa.is_empty());
5414        assert!(
5415            issue.contains(":nome is empty"),
5416            "issue must surface the empty-arm diagnostic: {issue}",
5417        );
5418    }
5419
5420    #[test]
5421    fn versao_violation_on_missing_patch() {
5422        // `"0.1"` — the canonical "I shortened it" footgun. Helm /
5423        // OCI / lacre-resolve / `:upgrade-from :from` all strict-parse
5424        // through `semver::Version::parse`, which refuses a two-part
5425        // shape.
5426        let root = PathBuf::from("/tmp/x");
5427        let manifest = root.join("caixa.lisp");
5428        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5429        let mut c = caixa(CaixaKind::Biblioteca);
5430        c.versao = "0.1".into();
5431        let err = layout.verify(&c, &root).unwrap_err();
5432        let LayoutError::VersaoViolation { caixa, issue } = err else {
5433            panic!("expected LayoutError::VersaoViolation, got {err:?}");
5434        };
5435        assert_eq!(caixa, "demo");
5436        assert!(
5437            issue.contains("0.1"),
5438            "issue must quote the offending versao: {issue}",
5439        );
5440    }
5441
5442    #[test]
5443    fn versao_violation_on_git_tag_shape() {
5444        // `"v0.1.0"` — the git-tag-shape-leaking-into-versao typo.
5445        let root = PathBuf::from("/tmp/x");
5446        let manifest = root.join("caixa.lisp");
5447        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5448        let mut c = caixa(CaixaKind::Biblioteca);
5449        c.versao = "v0.1.0".into();
5450        let err = layout.verify(&c, &root).unwrap_err();
5451        assert!(
5452            matches!(err, LayoutError::VersaoViolation { ref issue, .. }
5453                if issue.contains("v0.1.0")),
5454            "got {err:?}",
5455        );
5456    }
5457
5458    #[test]
5459    fn versao_violation_on_empty() {
5460        let root = PathBuf::from("/tmp/x");
5461        let manifest = root.join("caixa.lisp");
5462        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5463        let mut c = caixa(CaixaKind::Biblioteca);
5464        c.versao = String::new();
5465        let err = layout.verify(&c, &root).unwrap_err();
5466        let LayoutError::VersaoViolation { caixa, issue } = err else {
5467            panic!("expected LayoutError::VersaoViolation, got {err:?}");
5468        };
5469        assert_eq!(caixa, "demo");
5470        assert!(
5471            issue.contains(":versao is empty"),
5472            "issue must surface the empty-arm diagnostic: {issue}",
5473        );
5474    }
5475
5476    #[test]
5477    fn nome_violation_fires_before_versao_violation() {
5478        // Precedence pin: when both `:nome` and `:versao` are malformed,
5479        // `:nome` surfaces first — the canonical declaration-order
5480        // precedence the `ManifestError` family establishes, the same
5481        // grep-order the author follows when fixing in `caixa.lisp`.
5482        let root = PathBuf::from("/tmp/x");
5483        let manifest = root.join("caixa.lisp");
5484        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5485        let mut c = caixa(CaixaKind::Biblioteca);
5486        c.nome = "MyApp".into();
5487        c.versao = "0.1".into();
5488        let err = layout.verify(&c, &root).unwrap_err();
5489        assert!(
5490            matches!(err, LayoutError::NomeViolation { .. }),
5491            "got {err:?} — nome must fire before versao",
5492        );
5493    }
5494
5495    #[test]
5496    fn nome_violation_fires_before_kind_coherence() {
5497        // Precedence pin: a Biblioteca caixa with a malformed `:nome`
5498        // AND a declared mesh slot surfaces NomeViolation, not
5499        // MeshSlotsOnNonAplicacao — the identity-axis gate is more
5500        // fundamental than the kind-coherence gate (which carries
5501        // `caixa.nome` verbatim in its diagnostic, and so depends on the
5502        // name being structurally valid to render a useful message).
5503        let root = PathBuf::from("/tmp/x");
5504        let manifest = root.join("caixa.lisp");
5505        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5506        let mut c = caixa(CaixaKind::Biblioteca);
5507        c.nome = "MyApp".into();
5508        c.membros = vec![crate::aplicacao::Membro {
5509            caixa: "x".into(),
5510            versao: "^0.1".into(),
5511        }];
5512        let err = layout.verify(&c, &root).unwrap_err();
5513        assert!(
5514            matches!(err, LayoutError::NomeViolation { .. }),
5515            "got {err:?} — nome must fire before MeshSlotsOnNonAplicacao",
5516        );
5517    }
5518
5519    #[test]
5520    fn nome_violation_fires_before_owncode() {
5521        // Precedence pin: a Supervisor with a malformed `:nome` AND
5522        // declared `:bibliotecas` surfaces NomeViolation, not
5523        // SupervisorOwnsCode — same rationale as above.
5524        let root = PathBuf::from("/tmp/x");
5525        let manifest = root.join("caixa.lisp");
5526        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5527        let mut c = caixa(CaixaKind::Supervisor);
5528        c.nome = "MyApp".into();
5529        c.bibliotecas = vec!["lib/x.lisp".into()];
5530        let err = layout.verify(&c, &root).unwrap_err();
5531        assert!(
5532            matches!(err, LayoutError::NomeViolation { .. }),
5533            "got {err:?} — nome must fire before SupervisorOwnsCode",
5534        );
5535    }
5536
5537    #[test]
5538    fn versao_violation_fires_before_kind_coherence() {
5539        // Precedence pin: a Biblioteca with a valid `:nome` but a
5540        // malformed `:versao` AND a declared servico slot surfaces
5541        // VersaoViolation before ServicoSlotsOnNonServico.
5542        let root = PathBuf::from("/tmp/x");
5543        let manifest = root.join("caixa.lisp");
5544        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5545        let mut c = caixa(CaixaKind::Biblioteca);
5546        c.versao = "v0.1.0".into();
5547        c.limits = Some(crate::LimitsSpec {
5548            memory: Some(64 * 1024 * 1024),
5549            ..Default::default()
5550        });
5551        let err = layout.verify(&c, &root).unwrap_err();
5552        assert!(
5553            matches!(err, LayoutError::VersaoViolation { .. }),
5554            "got {err:?} — versao must fire before ServicoSlotsOnNonServico",
5555        );
5556    }
5557
5558    #[test]
5559    fn nome_violation_fires_before_missing_lib() {
5560        // Precedence pin: a Biblioteca with a malformed `:nome` and no
5561        // lib entry surfaces NomeViolation, not MissingLib — the
5562        // identity-axis gate is more fundamental than the layout's
5563        // `lib/<nome>.lisp` default-path check (which derives the
5564        // expected path from `:nome` itself, so would surface a
5565        // misleading "expected lib/MyApp.lisp" diagnostic against an
5566        // unrecoverable name).
5567        let root = PathBuf::from("/tmp/x");
5568        let manifest = root.join("caixa.lisp");
5569        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5570        let mut c = caixa(CaixaKind::Biblioteca);
5571        c.nome = "MyApp".into();
5572        let err = layout.verify(&c, &root).unwrap_err();
5573        assert!(
5574            matches!(err, LayoutError::NomeViolation { .. }),
5575            "got {err:?} — nome must fire before MissingLib",
5576        );
5577    }
5578
5579    #[test]
5580    fn nome_versao_violations_fire_after_missing_manifest() {
5581        // Precedence pin: `MissingManifest` still dominates — there's
5582        // no caixa to identity-check when the manifest is missing.
5583        let root = PathBuf::from("/tmp/x");
5584        let layout = StandardLayout::new().with_path_exists(|_| false);
5585        let mut c = caixa(CaixaKind::Biblioteca);
5586        c.nome = "MyApp".into();
5587        c.versao = "0.1".into();
5588        let err = layout.verify(&c, &root).unwrap_err();
5589        assert!(
5590            matches!(err, LayoutError::MissingManifest(_)),
5591            "got {err:?} — MissingManifest must dominate identity gates",
5592        );
5593    }
5594
5595    #[test]
5596    fn valid_nome_versao_passes_to_downstream_gates() {
5597        // Sanity pin: the canonical "demo" / "0.1.0" identity passes
5598        // both axes; downstream gates (MissingLib here) take over.
5599        let root = PathBuf::from("/tmp/x");
5600        let manifest = root.join("caixa.lisp");
5601        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5602        let err = layout
5603            .verify(&caixa(CaixaKind::Biblioteca), &root)
5604            .unwrap_err();
5605        assert!(
5606            matches!(err, LayoutError::MissingLib { .. }),
5607            "got {err:?} — valid identity must pass to MissingLib",
5608        );
5609    }
5610
5611    // ── :deps / :deps-dev shape gate (lifted to layout-level verify) ─────
5612    //
5613    // Until this wire-up landed `Caixa::validate_deps` lived as `pub fn`
5614    // on `Caixa` with full per-arm unit coverage in `manifest::tests` +
5615    // `dep::tests` but no production path called it — `feira build`
5616    // silently accepted a malformed `:deps` / `:deps-dev` entry and the
5617    // failure surfaced at lacre-resolve / `git clone` / `cargo metadata`
5618    // / `helm install` time on the *first* downstream consumer to
5619    // strict-parse the value, far from the source `caixa.lisp` and
5620    // without any field naming the offending `:deps` axis. The following
5621    // pins fence the layout-pipeline wire-up: every layout verify on a
5622    // structurally-invalid `:deps` value-shape surfaces the per-axis
5623    // `DepsViolation { caixa, issue }` envelope (peer of
5624    // `NomeViolation` / `VersaoViolation` / `CodePathViolation` /
5625    // `LimitsViolation` / `BehaviorViolation` / `UpgradeViolation` /
5626    // `SupervisorViolation` / `AplicacaoViolation`) before any kind-
5627    // coherence, code-path, or downstream gate sees it.
5628
5629    #[test]
5630    fn deps_violation_on_empty_dep_nome() {
5631        // Empty `:nome` on a `:deps` entry surfaces the narrower
5632        // `DepError::NomeEmpty` arm through the wrap envelope.
5633        use crate::Dep;
5634        let root = PathBuf::from("/tmp/x");
5635        let manifest = root.join("caixa.lisp");
5636        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5637        let mut c = caixa(CaixaKind::Biblioteca);
5638        c.deps = vec![Dep::simple("", "^0.1")];
5639        let err = layout.verify(&c, &root).unwrap_err();
5640        let LayoutError::DepsViolation { caixa, issue } = err else {
5641            panic!("expected LayoutError::DepsViolation, got {err:?}");
5642        };
5643        assert_eq!(caixa, "demo");
5644        assert!(
5645            issue.contains(":deps") && issue.contains(":nome"),
5646            "issue must name the offending slot + axis: {issue}",
5647        );
5648    }
5649
5650    #[test]
5651    fn deps_violation_on_uppercase_dep_nome() {
5652        // Uppercase `:nome` on a `:deps` entry surfaces
5653        // `DepError::NomeInvalid` (DNS-1123 violation) through the wrap.
5654        use crate::Dep;
5655        let root = PathBuf::from("/tmp/x");
5656        let manifest = root.join("caixa.lisp");
5657        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5658        let mut c = caixa(CaixaKind::Biblioteca);
5659        c.deps = vec![Dep::simple("Caixa-Teia", "^0.1")];
5660        let err = layout.verify(&c, &root).unwrap_err();
5661        let LayoutError::DepsViolation { caixa, issue } = err else {
5662            panic!("expected LayoutError::DepsViolation, got {err:?}");
5663        };
5664        assert_eq!(caixa, "demo");
5665        assert!(
5666            issue.contains("Caixa-Teia"),
5667            "issue must quote the offending dep nome verbatim: {issue}",
5668        );
5669    }
5670
5671    #[test]
5672    fn deps_violation_on_unparseable_dep_versao() {
5673        // Unparseable `:versao` requirement on a `:deps` entry surfaces
5674        // `DepError::VersaoInvalid` through the wrap — the canonical
5675        // "the semver::Error reached the resolver, far from the source"
5676        // footgun closed at author time.
5677        use crate::Dep;
5678        let root = PathBuf::from("/tmp/x");
5679        let manifest = root.join("caixa.lisp");
5680        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5681        let mut c = caixa(CaixaKind::Biblioteca);
5682        c.deps = vec![Dep::simple("caixa-teia", "not-a-req")];
5683        let err = layout.verify(&c, &root).unwrap_err();
5684        let LayoutError::DepsViolation { caixa, issue } = err else {
5685            panic!("expected LayoutError::DepsViolation, got {err:?}");
5686        };
5687        assert_eq!(caixa, "demo");
5688        assert!(
5689            issue.contains("caixa-teia") && issue.contains("not-a-req"),
5690            "issue must quote the dep nome + offending versao: {issue}",
5691        );
5692    }
5693
5694    #[test]
5695    fn deps_violation_on_duplicate_nome_in_deps() {
5696        // Within-list `:deps :nome` duplicate surfaces
5697        // `DepError::DuplicateNome { list: ":deps" }` through the wrap.
5698        use crate::Dep;
5699        let root = PathBuf::from("/tmp/x");
5700        let manifest = root.join("caixa.lisp");
5701        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5702        let mut c = caixa(CaixaKind::Biblioteca);
5703        c.deps = vec![
5704            Dep::simple("caixa-teia", "^0.1"),
5705            Dep::simple("caixa-teia", "^0.2"),
5706        ];
5707        let err = layout.verify(&c, &root).unwrap_err();
5708        let LayoutError::DepsViolation { caixa, issue } = err else {
5709            panic!("expected LayoutError::DepsViolation, got {err:?}");
5710        };
5711        assert_eq!(caixa, "demo");
5712        assert!(
5713            issue.contains("caixa-teia") && issue.contains(":deps"),
5714            "issue must quote the duplicated nome + list: {issue}",
5715        );
5716    }
5717
5718    #[test]
5719    fn deps_violation_on_duplicate_nome_in_deps_dev() {
5720        // Within-list `:deps-dev :nome` duplicate surfaces the same
5721        // diagnostic on the dev-only axis — neither list is a
5722        // second-class citizen of the typed surface.
5723        use crate::Dep;
5724        let root = PathBuf::from("/tmp/x");
5725        let manifest = root.join("caixa.lisp");
5726        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5727        let mut c = caixa(CaixaKind::Biblioteca);
5728        c.deps_dev = vec![
5729            Dep::simple("caixa-teia", "^0.1"),
5730            Dep::simple("caixa-teia", "^0.2"),
5731        ];
5732        let err = layout.verify(&c, &root).unwrap_err();
5733        let LayoutError::DepsViolation { caixa, issue } = err else {
5734            panic!("expected LayoutError::DepsViolation, got {err:?}");
5735        };
5736        assert_eq!(caixa, "demo");
5737        assert!(
5738            issue.contains(":deps-dev"),
5739            "issue must name the offending list: {issue}",
5740        );
5741    }
5742
5743    #[test]
5744    fn deps_violation_in_deps_fires_before_deps_dev() {
5745        // Precedence pin: when *both* `:deps` and `:deps-dev` carry a
5746        // malformed entry, the `:deps` walk fires first — the canonical
5747        // declaration-order precedence `Caixa::validate_deps` establishes
5748        // (the same author-grep ordering the typed-graph peers use on
5749        // every other Vec-shaped surface).
5750        use crate::Dep;
5751        let root = PathBuf::from("/tmp/x");
5752        let manifest = root.join("caixa.lisp");
5753        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5754        let mut c = caixa(CaixaKind::Biblioteca);
5755        c.deps = vec![Dep::simple("Bad-In-Deps", "^0.1")];
5756        c.deps_dev = vec![Dep::simple("Bad-In-Deps-Dev", "^0.1")];
5757        let err = layout.verify(&c, &root).unwrap_err();
5758        let LayoutError::DepsViolation { caixa: _, issue } = err else {
5759            panic!("expected LayoutError::DepsViolation, got {err:?}");
5760        };
5761        assert!(
5762            issue.contains("Bad-In-Deps") && !issue.contains("Bad-In-Deps-Dev"),
5763            "issue must name the :deps offender, not :deps-dev: {issue}",
5764        );
5765    }
5766
5767    #[test]
5768    fn deps_violation_fires_after_versao_violation() {
5769        // Precedence pin: when both the top-level `:versao` and a `:deps`
5770        // entry are malformed, the Caixa-identity gate fires first — the
5771        // canonical declaration order on `Caixa` (`:nome` → `:versao` →
5772        // ... → `:deps`) and the same identity-axis-dominates-content-
5773        // axis discipline the peer `validate_nome` / `validate_versao`
5774        // wire-up established (1f74a5f). A malformed `:versao` would
5775        // otherwise quote `caixa.nome` against a downstream-shaped
5776        // diagnostic.
5777        use crate::Dep;
5778        let root = PathBuf::from("/tmp/x");
5779        let manifest = root.join("caixa.lisp");
5780        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5781        let mut c = caixa(CaixaKind::Biblioteca);
5782        c.versao = "v0.1.0".into();
5783        c.deps = vec![Dep::simple("Bad-Dep", "^0.1")];
5784        let err = layout.verify(&c, &root).unwrap_err();
5785        assert!(
5786            matches!(err, LayoutError::VersaoViolation { .. }),
5787            "got {err:?} — versao must fire before DepsViolation",
5788        );
5789    }
5790
5791    #[test]
5792    fn deps_violation_fires_before_kind_coherence() {
5793        // Precedence pin: a Supervisor with a malformed `:deps` entry
5794        // AND declared `:bibliotecas` (the canonical SupervisorOwnsCode
5795        // shape) surfaces DepsViolation, not SupervisorOwnsCode — the
5796        // dep surface is universal across all kinds and its shape gate
5797        // is more fundamental than the kind-coherence partitions on
5798        // `:bibliotecas` / `:exe` / `:servicos`. The author can fix the
5799        // dep typo without first being told to move their `:bibliotecas`
5800        // off a Supervisor.
5801        use crate::Dep;
5802        let root = PathBuf::from("/tmp/x");
5803        let manifest = root.join("caixa.lisp");
5804        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5805        let mut c = caixa(CaixaKind::Supervisor);
5806        c.deps = vec![Dep::simple("Bad-Dep", "^0.1")];
5807        c.bibliotecas = vec!["lib/x.lisp".into()];
5808        let err = layout.verify(&c, &root).unwrap_err();
5809        assert!(
5810            matches!(err, LayoutError::DepsViolation { .. }),
5811            "got {err:?} — DepsViolation must fire before SupervisorOwnsCode",
5812        );
5813    }
5814
5815    #[test]
5816    fn deps_violation_fires_after_missing_manifest() {
5817        // Precedence pin: `MissingManifest` still dominates — there's no
5818        // caixa to deps-check when the manifest is missing.
5819        use crate::Dep;
5820        let root = PathBuf::from("/tmp/x");
5821        let layout = StandardLayout::new().with_path_exists(|_| false);
5822        let mut c = caixa(CaixaKind::Biblioteca);
5823        c.deps = vec![Dep::simple("Bad-Dep", "^0.1")];
5824        let err = layout.verify(&c, &root).unwrap_err();
5825        assert!(
5826            matches!(err, LayoutError::MissingManifest(_)),
5827            "got {err:?} — MissingManifest must dominate the deps gate",
5828        );
5829    }
5830
5831    #[test]
5832    fn deps_violation_on_self_dep_in_deps() {
5833        // Cross-slot self-edge: a caixa whose `:deps` lists its own
5834        // `:nome` is rejected at the layout wire-up, the diagnostic
5835        // surfaces through the `DepsViolation` envelope with both the
5836        // offending list tag (`":deps"`) and the parent's `:nome`
5837        // verbatim. Until this wire-up landed the self-dep silently
5838        // passed `feira build` and the resolver's lacre-pipeline
5839        // closure walk either rejected mid-traversal (infinite
5840        // recursion detected far from the source caixa.lisp) or, on
5841        // the unbounded path, recursed until it exhausted its stack.
5842        // Mirrors the supervision-tree
5843        // [`supervisor_violation_on_self_supervision`] and the
5844        // Aplicacao-membership self-edge wire-up tests on the peer
5845        // typed-name-graph axes.
5846        use crate::Dep;
5847        let root = PathBuf::from("/tmp/x");
5848        let manifest = root.join("caixa.lisp");
5849        let default_lib = root.join("lib").join("demo.lisp");
5850        let layout =
5851            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5852        let mut c = caixa(CaixaKind::Biblioteca);
5853        c.deps = vec![Dep::simple("demo", "^0.1")];
5854        let err = layout.verify(&c, &root).unwrap_err();
5855        let LayoutError::DepsViolation { caixa, issue } = err else {
5856            panic!("expected LayoutError::DepsViolation, got {err:?}");
5857        };
5858        assert_eq!(caixa, "demo");
5859        assert!(
5860            issue.contains(":deps") && issue.contains("demo"),
5861            "issue must name the offending list + parent :nome: {issue}",
5862        );
5863    }
5864
5865    #[test]
5866    fn deps_violation_on_self_dep_in_deps_dev() {
5867        // Same cross-slot self-edge gate on the `:deps-dev` axis —
5868        // neither dep list is a second-class citizen of the typed
5869        // surface. The diagnostic names `:deps-dev` so the author can
5870        // grep their caixa.lisp for the offending block directly.
5871        use crate::Dep;
5872        let root = PathBuf::from("/tmp/x");
5873        let manifest = root.join("caixa.lisp");
5874        let default_lib = root.join("lib").join("demo.lisp");
5875        let layout =
5876            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5877        let mut c = caixa(CaixaKind::Biblioteca);
5878        c.deps_dev = vec![Dep::simple("demo", "^0.1")];
5879        let err = layout.verify(&c, &root).unwrap_err();
5880        let LayoutError::DepsViolation { caixa, issue } = err else {
5881            panic!("expected LayoutError::DepsViolation, got {err:?}");
5882        };
5883        assert_eq!(caixa, "demo");
5884        assert!(
5885            issue.contains(":deps-dev"),
5886            "issue must name the offending list: {issue}",
5887        );
5888    }
5889
5890    #[test]
5891    fn self_dep_fires_after_per_entry_dep_shape() {
5892        // Precedence pin: the per-entry shape gates of
5893        // [`Caixa::validate_deps`] (DNS-1123 / SemVer / fonte / etc.)
5894        // fire first on a self-dep entry whose `:nome` is malformed.
5895        // Same ordering posture every peer cross-slot gate uses
5896        // (`validate_no_self_supervision` after `SupervisorSpec::validate`,
5897        // `validate_no_self_membership` after `AplicacaoSpec::validate`).
5898        // A malformed self-dep `:nome` surfaces the narrower
5899        // per-entry diagnostic (which already names the parser-side
5900        // reason) before the self-edge gate sees the entry.
5901        use crate::Dep;
5902        let root = PathBuf::from("/tmp/x");
5903        let manifest = root.join("caixa.lisp");
5904        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5905        let mut c = caixa(CaixaKind::Biblioteca);
5906        // The parent is "demo" (DNS-1123 valid); the dep is "DEMO"
5907        // (DNS-1123 invalid). The per-entry shape gate fires on the
5908        // upper-case nome, masking the self-edge gate (and that's the
5909        // canonical precedence — fix the dep shape first, then the
5910        // structural self-edge becomes the next live diagnostic).
5911        c.deps = vec![Dep::simple("DEMO", "^0.1")];
5912        let err = layout.verify(&c, &root).unwrap_err();
5913        let LayoutError::DepsViolation { caixa: _, issue } = err else {
5914            panic!("expected LayoutError::DepsViolation, got {err:?}");
5915        };
5916        assert!(
5917            issue.contains("DNS-1123"),
5918            "issue must be the per-entry shape diagnostic, not the self-edge gate: {issue}",
5919        );
5920    }
5921
5922    #[test]
5923    fn valid_deps_pass_to_downstream_gates() {
5924        // Positive control pin: the canonical authoring shape (one
5925        // `:deps` entry naming a DNS-1123 nome + Cargo-shaped requirement,
5926        // one `:deps-dev` entry on a distinct nome) passes the dep gate;
5927        // downstream gates (MissingLib here) take over. Drift here =
5928        // a future tighten that rejects any canonical shape surfaces as
5929        // a regression at this layout-level pin.
5930        use crate::Dep;
5931        let root = PathBuf::from("/tmp/x");
5932        let manifest = root.join("caixa.lisp");
5933        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5934        let mut c = caixa(CaixaKind::Biblioteca);
5935        c.deps = vec![Dep::simple("caixa-teia", "^0.1")];
5936        c.deps_dev = vec![Dep::simple("caixa-lint", "^0.2")];
5937        let err = layout.verify(&c, &root).unwrap_err();
5938        assert!(
5939            matches!(err, LayoutError::MissingLib { .. }),
5940            "got {err:?} — valid deps must pass to MissingLib",
5941        );
5942    }
5943
5944    #[test]
5945    fn code_path_gate_runs_after_foreign_code_slot_gate() {
5946        // Precedence pin: a Servico that declares `:exe` (foreign code
5947        // surface) surfaces ForeignCodeSlot, *not* a per-entry path
5948        // shape diagnostic, even when the `:exe` entry is itself
5949        // malformed. The kind-coherence gate is the load-bearing
5950        // diagnostic at this site — once the slot is moved off the
5951        // wrong kind, the per-entry shape gate becomes the next live
5952        // diagnostic.
5953        let root = PathBuf::from("/tmp/x");
5954        let manifest = root.join("caixa.lisp");
5955        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5956        let mut c = caixa(CaixaKind::Servico);
5957        c.servicos = vec!["servicos/ok.yaml".into()];
5958        c.exe = vec!["/etc/foreign".into()];
5959        let err = layout.verify(&c, &root).unwrap_err();
5960        assert!(
5961            matches!(err, LayoutError::ForeignCodeSlot { .. }),
5962            "expected ForeignCodeSlot (kind-coherence wins over per-entry shape), got {err:?}",
5963        );
5964    }
5965
5966    // ── M2 typed-substrate invariants ────────────────────────────────────
5967
5968    #[test]
5969    fn behavior_callback_path_must_exist() {
5970        use crate::BehaviorSpec;
5971        use std::path::PathBuf;
5972        let root = PathBuf::from("/tmp/x");
5973        let manifest = root.join("caixa.lisp");
5974        let mut c = caixa(CaixaKind::Servico);
5975        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5976        let svc = root.join("servicos/demo.computeunit.yaml");
5977        c.behavior = Some(BehaviorSpec {
5978            on_init: Some(PathBuf::from("lib/init.lisp")),
5979            ..Default::default()
5980        });
5981        let manifest_clone = manifest.clone();
5982        let svc_clone = svc.clone();
5983        let layout =
5984            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
5985        let err = layout.verify(&c, &root).unwrap_err();
5986        assert!(matches!(
5987            err,
5988            LayoutError::MissingEntry { kind, .. }
5989                if kind == crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK
5990        ));
5991
5992        // Now declare the path exists — passes.
5993        let init = root.join("lib/init.lisp");
5994        let layout =
5995            StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc || p == init);
5996        layout.verify(&c, &root).unwrap();
5997    }
5998
5999    #[test]
6000    fn behavior_absolute_callback_is_violation_not_missing() {
6001        // An absolute path silently subverts `root.join(p)` (Path::join
6002        // replaces the base when the right side is absolute). Before
6003        // BehaviorSpec::validate ran, an `:on-init "/etc/passwd"` would
6004        // surface as a confusing "missing behavior-callback /etc/passwd"
6005        // — or, worse, pass when /etc/passwd happens to exist. Now it's
6006        // a value-shape error naming the slot.
6007        use crate::BehaviorSpec;
6008        let root = PathBuf::from("/tmp/x");
6009        let manifest = root.join("caixa.lisp");
6010        let svc = root.join("servicos/demo.computeunit.yaml");
6011        let mut c = caixa(CaixaKind::Servico);
6012        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6013        c.behavior = Some(BehaviorSpec {
6014            on_init: Some(PathBuf::from("/etc/passwd")),
6015            ..Default::default()
6016        });
6017        // Path exists check would *succeed* on /etc/passwd (proving the
6018        // sandbox bypass) — value-shape pass must fire first.
6019        let layout = StandardLayout::new()
6020            .with_path_exists(move |p| p == manifest || p == svc || p == Path::new("/etc/passwd"));
6021        let err = layout.verify(&c, &root).unwrap_err();
6022        assert!(
6023            matches!(err, LayoutError::BehaviorViolation { ref caixa, .. } if caixa == "demo"),
6024            "got {err:?}",
6025        );
6026    }
6027
6028    #[test]
6029    fn behavior_empty_callback_is_violation() {
6030        use crate::BehaviorSpec;
6031        let root = PathBuf::from("/tmp/x");
6032        let manifest = root.join("caixa.lisp");
6033        let svc = root.join("servicos/demo.computeunit.yaml");
6034        let mut c = caixa(CaixaKind::Servico);
6035        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6036        c.behavior = Some(BehaviorSpec {
6037            on_call: Some(PathBuf::new()),
6038            ..Default::default()
6039        });
6040        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
6041        let err = layout.verify(&c, &root).unwrap_err();
6042        assert!(matches!(err, LayoutError::BehaviorViolation { .. }));
6043    }
6044
6045    #[test]
6046    fn upgrade_from_duplicate_surfaces_as_upgrade_violation() {
6047        // Wiring pin: the cross-entry duplicate-`:from` gate in
6048        // `validate_upgrade_from` lands on the same
6049        // `LayoutError::UpgradeViolation` axis the per-entry
6050        // `UpgradeFromEntry::validate` already does (26da2c7), so a
6051        // caixa.lisp with two `(:from "0.1.0" …)` blocks surfaces at
6052        // `feira build` time naming the offending caixa rather than
6053        // silently passing into the wasm-operator's non-deterministic
6054        // dispatch. Mirrors `behavior_empty_callback_is_violation` on
6055        // the peer M2 typed slot.
6056        use crate::{UpgradeFromEntry, UpgradeInstruction};
6057        let root = PathBuf::from("/tmp/x");
6058        let manifest = root.join("caixa.lisp");
6059        let svc = root.join("servicos/demo.computeunit.yaml");
6060        let mut c = caixa(CaixaKind::Servico);
6061        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6062        c.upgrade_from = vec![
6063            UpgradeFromEntry {
6064                from: "0.1.0".into(),
6065                instructions: vec![UpgradeInstruction::Restart],
6066            },
6067            UpgradeFromEntry {
6068                from: "0.1.0".into(),
6069                instructions: vec![UpgradeInstruction::Restart],
6070            },
6071        ];
6072        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
6073        let err = layout.verify(&c, &root).unwrap_err();
6074        let LayoutError::UpgradeViolation { caixa, issue } = err else {
6075            panic!("expected LayoutError::UpgradeViolation for duplicate `:from`, got {err:?}");
6076        };
6077        assert_eq!(caixa, "demo");
6078        assert!(
6079            issue.contains("0.1.0"),
6080            "UpgradeViolation issue must name the offending `:from` verbatim, got {issue:?}"
6081        );
6082    }
6083
6084    #[test]
6085    fn upgrade_from_downgrade_surfaces_as_upgrade_violation() {
6086        // Wiring pin: the cross-slot precedence gate in
6087        // `validate_upgrade_from_against_versao` lands on the same
6088        // `LayoutError::UpgradeViolation` axis the per-entry and
6089        // cross-entry gates already do (26da2c7, 7c6aef2), so a
6090        // caixa.lisp whose `:upgrade-from :from` is greater than the
6091        // caixa's own `:versao` surfaces at `feira build` time
6092        // naming the offending caixa rather than silently passing
6093        // into the wasm-operator's `:from`-match dispatch where the
6094        // entry would sit dormant forever. Mirrors
6095        // `upgrade_from_duplicate_surfaces_as_upgrade_violation` on
6096        // the peer cross-entry gate.
6097        use crate::{UpgradeFromEntry, UpgradeInstruction};
6098        let root = PathBuf::from("/tmp/x");
6099        let manifest = root.join("caixa.lisp");
6100        let svc = root.join("servicos/demo.computeunit.yaml");
6101        let mut c = caixa(CaixaKind::Servico);
6102        c.versao = "0.1.5".into();
6103        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6104        c.upgrade_from = vec![UpgradeFromEntry {
6105            from: "0.2.0".into(),
6106            instructions: vec![UpgradeInstruction::Restart],
6107        }];
6108        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
6109        let err = layout.verify(&c, &root).unwrap_err();
6110        let LayoutError::UpgradeViolation { caixa, issue } = err else {
6111            panic!(
6112                "expected LayoutError::UpgradeViolation for downgrade-shaped `:from`, got {err:?}"
6113            );
6114        };
6115        assert_eq!(caixa, "demo");
6116        assert!(
6117            issue.contains("0.2.0") && issue.contains("0.1.5"),
6118            "UpgradeViolation issue must name both `:from` and `:versao` verbatim, got {issue:?}"
6119        );
6120    }
6121
6122    #[test]
6123    fn upgrade_from_equal_to_versao_surfaces_as_upgrade_violation() {
6124        // Self-upgrade no-op arm: `:from "0.1.0"` while
6125        // `:versao "0.1.0"` declares "upgrade from myself to
6126        // myself", which the operator's dispatch either skips
6127        // silently or trivially "succeeds" with no observable
6128        // transition. Surfaces at validate time naming both values
6129        // so the author can fix in one edit.
6130        use crate::{UpgradeFromEntry, UpgradeInstruction};
6131        let root = PathBuf::from("/tmp/x");
6132        let manifest = root.join("caixa.lisp");
6133        let svc = root.join("servicos/demo.computeunit.yaml");
6134        let mut c = caixa(CaixaKind::Servico);
6135        c.versao = "0.1.0".into();
6136        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6137        c.upgrade_from = vec![UpgradeFromEntry {
6138            from: "0.1.0".into(),
6139            instructions: vec![UpgradeInstruction::Restart],
6140        }];
6141        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
6142        let err = layout.verify(&c, &root).unwrap_err();
6143        let LayoutError::UpgradeViolation { caixa, issue } = err else {
6144            panic!(
6145                "expected LayoutError::UpgradeViolation for self-upgrade `:from == :versao`, got \
6146                 {err:?}"
6147            );
6148        };
6149        assert_eq!(caixa, "demo");
6150        assert!(
6151            issue.contains("0.1.0"),
6152            "UpgradeViolation issue must name the equal `:from`/`:versao` verbatim, got {issue:?}"
6153        );
6154    }
6155
6156    #[test]
6157    fn upgrade_from_strict_upgrade_passes_layout() {
6158        // Positive control for the precedence gate at the
6159        // LayoutInvariants level: a valid `:from < :versao` chain
6160        // (`0.1.0 → 0.2.0`) must not regress into a false-positive
6161        // `UpgradeViolation`. Mirrors `behavior_callback_path_must_exist`'s
6162        // positive-control arm.
6163        use crate::{UpgradeFromEntry, UpgradeInstruction};
6164        let root = PathBuf::from("/tmp/x");
6165        let manifest = root.join("caixa.lisp");
6166        let svc = root.join("servicos/demo.computeunit.yaml");
6167        let mut c = caixa(CaixaKind::Servico);
6168        c.versao = "0.2.0".into();
6169        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6170        c.upgrade_from = vec![UpgradeFromEntry {
6171            from: "0.1.0".into(),
6172            instructions: vec![UpgradeInstruction::Restart],
6173        }];
6174        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
6175        layout.verify(&c, &root).unwrap();
6176    }
6177
6178    #[test]
6179    fn upgrade_script_path_must_exist() {
6180        use crate::{BehaviorSpec, UpgradeFromEntry, UpgradeInstruction};
6181        use std::path::PathBuf;
6182        let root = PathBuf::from("/tmp/x");
6183        let manifest = root.join("caixa.lisp");
6184        let svc = root.join("servicos/demo.computeunit.yaml");
6185        let on_state_change = root.join("lib/migrations.lisp");
6186        let mut c = caixa(CaixaKind::Servico);
6187        // `:versao` past the entry's `:from` so the cross-slot
6188        // precedence gate (`FromNotBeforeVersao`) lets this case
6189        // through to the path-existence pass under test.
6190        c.versao = "0.2.0".into();
6191        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6192        // `:on-state-change` declared so the cross-slot composition
6193        // gate (`validate_upgrade_from_against_behavior`) lets the
6194        // `:state-change` entry through to the path-existence pass
6195        // under test. Without the callback the missing-callback gate
6196        // would surface first and the path-existence pass wouldn't be
6197        // exercised.
6198        c.behavior = Some(BehaviorSpec {
6199            on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
6200            ..Default::default()
6201        });
6202        // A `:load-module` precedes the `:state-change` so the entry
6203        // satisfies the within-entry state-change-ordering gate
6204        // (`StateChangeWithoutPriorLoad`) and the path-existence pass
6205        // under test is the gate actually exercised. `:load-module`
6206        // carries no on-disk path, so it adds no existence requirement.
6207        c.upgrade_from = vec![UpgradeFromEntry {
6208            from: "0.1.0".into(),
6209            instructions: vec![
6210                UpgradeInstruction::LoadModule {
6211                    module: "demo".into(),
6212                },
6213                UpgradeInstruction::StateChange {
6214                    script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
6215                },
6216            ],
6217        }];
6218        let manifest_clone = manifest.clone();
6219        let svc_clone = svc.clone();
6220        let on_state_change_clone = on_state_change.clone();
6221        let layout = StandardLayout::new().with_path_exists(move |p| {
6222            p == manifest_clone || p == svc_clone || p == on_state_change_clone
6223        });
6224        let err = layout.verify(&c, &root).unwrap_err();
6225        assert!(matches!(
6226            err,
6227            LayoutError::MissingEntry { kind, .. }
6228                if kind == crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT
6229        ));
6230    }
6231
6232    #[test]
6233    fn layout_missing_entry_kind_m2_consts_pin_canonical_kebab_case_labels() {
6234        // Byte-identity pin: the two per-M2-slot leaf-kind labels the
6235        // [`LayoutError::MissingEntry`] `kind: &'static str`
6236        // discriminator surfaces under (the M2 `:behavior` per-callback
6237        // on-disk-leaf axis, the M2 `:upgrade-from :instructions`
6238        // per-`:state-change` script-path on-disk-leaf axis) route
6239        // through the lifted [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`]
6240        // / [`crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT`]
6241        // consts, so a future rebrand that reaches the const but not
6242        // the production emit / test probe (or vice versa) surfaces
6243        // here at build time rather than at runtime as a downstream
6244        // [`LayoutError::MissingEntry`] `kind: <stale-label>`
6245        // diagnostic mismatch far from the rename's commit. Mirror of
6246        // the peer
6247        // [`crate::aplicacao::tests::contrato_author_key_consts_pin_canonical_kebab_case_labels`]
6248        // (f50c875) and
6249        // [`crate::upgrade::tests::upgrade_instruction_kind_consts_pin_canonical_kebab_case_tags`]
6250        // (56120ef) byte-identity pins on the sibling M3 `:contratos`
6251        // per-entry endpoint-label + M2 `:upgrade-from :instructions`
6252        // per-variant kind-tag axes.
6253        assert_eq!(
6254            crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
6255            "behavior-callback"
6256        );
6257        assert_eq!(
6258            crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
6259            "upgrade-script"
6260        );
6261    }
6262
6263    #[test]
6264    fn layout_missing_entry_kind_m0_consts_pin_canonical_code_slot_labels() {
6265        // Byte-identity pin: the three per-M0-code-slot leaf-kind
6266        // labels the [`LayoutError::MissingEntry`] `kind: &'static
6267        // str` discriminator surfaces under (the `:bibliotecas`
6268        // per-entry axis, the `:exe` per-entry axis, the `:servicos`
6269        // per-entry axis) route through the lifted
6270        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`] /
6271        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] /
6272        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] consts,
6273        // so a future rebrand that reaches the const but not the
6274        // production emit (or vice versa) surfaces here at build time
6275        // rather than at runtime as a downstream
6276        // [`LayoutError::MissingEntry`] `kind: <stale-label>`
6277        // diagnostic mismatch far from the rename's commit. Mirror of
6278        // the peer M2-tier pin
6279        // [`layout_missing_entry_kind_m2_consts_pin_canonical_kebab_case_labels`]
6280        // (95c9c4c) on the sibling `:behavior` / `:upgrade-from`
6281        // per-slot leaf-kind axes.
6282        assert_eq!(
6283            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
6284            "biblioteca"
6285        );
6286        assert_eq!(crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE, "exe");
6287        assert_eq!(crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO, "servico");
6288    }
6289
6290    #[test]
6291    fn layout_missing_entry_kind_m0_consts_align_with_caixa_kind_as_str() {
6292        // Cross-axis byte-identity pin: the two `:kind`-namesake M0
6293        // leaf-kind labels ([`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`]
6294        // = `"biblioteca"`,
6295        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] =
6296        // `"servico"`) must equal [`crate::CaixaKind::Biblioteca`] /
6297        // [`crate::CaixaKind::Servico`]'s
6298        // [`crate::CaixaKind::as_str`] outputs verbatim — the
6299        // substrate's canonical human-readable-kind axis and the
6300        // layout diagnostic's per-slot leaf-kind axis share one
6301        // vocabulary for these two arms by design (both label the
6302        // caixa's code-producing shape by its Portuguese-native
6303        // idiom), so drift between the two lands as a build-time
6304        // pattern-arm miss here rather than as a runtime diagnostic
6305        // that reads inconsistently across `feira build`'s
6306        // per-invocation output.
6307        //
6308        // The third M0 arm ([`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`]
6309        // = `"exe"`) is deliberately *distinct* from
6310        // [`crate::CaixaKind::Binario`]'s [`crate::CaixaKind::as_str`]
6311        // output (`"binario"`) — the `:exe` code slot names the
6312        // per-directory leaf-kind at the `exe/` subtree, whereas
6313        // [`crate::CaixaKind::Binario`] names the caixa's own runtime
6314        // kind. Two axes, two labels — the inequality assertion here
6315        // pins the split so a future accidental collapse of the two
6316        // onto one scalar (a rebrand that reroutes either axis to
6317        // match the other) trips at build time.
6318        assert_eq!(
6319            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
6320            CaixaKind::Biblioteca.as_str()
6321        );
6322        assert_eq!(
6323            crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
6324            CaixaKind::Servico.as_str()
6325        );
6326        assert_ne!(
6327            crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
6328            CaixaKind::Binario.as_str(),
6329            "`exe` leaf-kind label names the per-directory code-slot \
6330             axis; `binario` names the caixa-kind axis — the two must \
6331             not silently collapse onto one scalar"
6332        );
6333    }
6334
6335    #[test]
6336    fn layout_missing_entry_kind_consts_are_pairwise_distinct() {
6337        // Distinctness pin: the five [`LayoutError::MissingEntry`]
6338        // `kind: &'static str` accept-set members
6339        // ([`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`] /
6340        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] /
6341        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] on the
6342        // M0 code-slot arms plus
6343        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`]
6344        // / [`crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT`]
6345        // on the M2 slot arms) must be pairwise distinct — an
6346        // accidental copy-paste flip that reroutes one label's byte-
6347        // string to also match another silently collapses two
6348        // per-slot diagnostics onto one, so an operator running
6349        // `feira build` reads `kind: "biblioteca"` for what should
6350        // have surfaced as a `:behavior :on-init` script-not-found
6351        // diagnostic (or vice versa). This pin catches any such
6352        // flip at build time. Mirror of the peer
6353        // [`crate::render::tests::m2_limits_key_consts_are_pairwise_distinct`]
6354        // / peer distinctness pins on other closed-set typed axes.
6355        let entries: &[(&str, &str)] = &[
6356            (
6357                "LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA",
6358                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
6359            ),
6360            (
6361                "LAYOUT_MISSING_ENTRY_KIND_EXE",
6362                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
6363            ),
6364            (
6365                "LAYOUT_MISSING_ENTRY_KIND_SERVICO",
6366                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
6367            ),
6368            (
6369                "LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK",
6370                crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
6371            ),
6372            (
6373                "LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT",
6374                crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
6375            ),
6376        ];
6377        for (i, (name_a, value_a)) in entries.iter().enumerate() {
6378            for (name_b, value_b) in entries.iter().skip(i + 1) {
6379                assert_ne!(
6380                    value_a, value_b,
6381                    "LAYOUT_MISSING_ENTRY_KIND_* consts must be \
6382                     pairwise-distinct byte-strings — {name_a} and \
6383                     {name_b} both resolve to {value_a:?}"
6384                );
6385            }
6386        }
6387    }
6388
6389    #[test]
6390    fn layout_dir_consts_pin_canonical_directory_names() {
6391        // Scalar-value pin for the three [`crate::render::LAYOUT_DIR_*`]
6392        // consts naming the CSE-invariant per-[`CaixaKind`]
6393        // on-disk-directory-name axes the substrate's layout invariants
6394        // pin (`lib/` for [`CaixaKind::Biblioteca`], `exe/` for
6395        // [`CaixaKind::Binario`], `servicos/` for [`CaixaKind::Servico`]).
6396        // A future rebrand of any of the three on-disk directory landing
6397        // conventions must reach this pin — the const-edit lands on one
6398        // arm, the assertion here re-pins the new byte-string, and every
6399        // downstream consumer (the caixa-feira `init` / `fmt` / `lint` /
6400        // `tofu` scaffolders, the [`crate::LayoutInvariants::verify`]
6401        // sandbox reconstruction, the future
6402        // `feira app deploy`-cluster scaffolder) picks up the new
6403        // directory name at build time. Mirror of the peer
6404        // [`layout_missing_entry_kind_m0_consts_pin_canonical_code_slot_labels`]
6405        // (fe2a898) on the sibling
6406        // [`crate::LayoutError::MissingEntry`] `kind:` discriminator
6407        // axis this on-disk-directory axis composes with.
6408        assert_eq!(crate::render::LAYOUT_DIR_LIB, "lib");
6409        assert_eq!(crate::render::LAYOUT_DIR_EXE, "exe");
6410        assert_eq!(crate::render::LAYOUT_DIR_SERVICOS, "servicos");
6411    }
6412
6413    #[test]
6414    fn layout_dir_consts_are_pairwise_distinct() {
6415        // Distinctness pin: the three per-[`CaixaKind`]
6416        // on-disk-directory-name arms must resolve to pairwise-distinct
6417        // byte-strings — a future accidental copy-paste flip that
6418        // reroutes any one of the three onto another's value silently
6419        // collapses two per-kind on-disk sandboxes onto one, so
6420        // [`crate::LayoutInvariants::verify`] would gate a
6421        // [`CaixaKind::Binario`] caixa's `:exe` entries against the
6422        // wrong sub-tree (or a `:kind Servico` caixa's `:servicos`
6423        // entries against `lib/` and pass every entry `feira build`
6424        // should have rejected as [`crate::LayoutError::ServicoOutsideDir`]).
6425        // Mirror of the peer
6426        // [`layout_missing_entry_kind_consts_are_pairwise_distinct`]
6427        // (fe2a898) on the sibling leaf-kind label accept-set.
6428        let entries: &[(&str, &str)] = &[
6429            ("LAYOUT_DIR_LIB", crate::render::LAYOUT_DIR_LIB),
6430            ("LAYOUT_DIR_EXE", crate::render::LAYOUT_DIR_EXE),
6431            ("LAYOUT_DIR_SERVICOS", crate::render::LAYOUT_DIR_SERVICOS),
6432        ];
6433        for (i, (name_a, value_a)) in entries.iter().enumerate() {
6434            for (name_b, value_b) in entries.iter().skip(i + 1) {
6435                assert_ne!(
6436                    value_a, value_b,
6437                    "LAYOUT_DIR_* consts must be pairwise-distinct \
6438                     byte-strings — {name_a} and {name_b} both resolve \
6439                     to {value_a:?}"
6440                );
6441            }
6442        }
6443    }
6444
6445    #[test]
6446    fn layout_dir_exe_matches_layout_missing_entry_kind_exe() {
6447        // Cross-axis byte-identity pin: [`crate::render::LAYOUT_DIR_EXE`]
6448        // (the on-disk-directory-name arm) equals
6449        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] (the
6450        // [`LayoutError::MissingEntry`] `kind:` leaf-kind categorization
6451        // arm) verbatim — the M0 `:kind Binario` on-disk-directory axis
6452        // and the [`crate::LayoutError::MissingEntry`] `kind:` leaf-kind
6453        // discriminator name the same three-byte sub-tree (`exe/`), a
6454        // coincidence [`crate::LayoutInvariants::verify`] itself relies
6455        // on: it joins `root` with [`crate::render::LAYOUT_DIR_EXE`] to
6456        // reconstruct `exe_dir` and emits [`crate::LayoutError::MissingEntry
6457        // { kind: LAYOUT_MISSING_ENTRY_KIND_EXE, path: <under exe_dir> }`]
6458        // for every non-resolving entry. Making the coincidence
6459        // load-bearing means a future rebrand touching either axis
6460        // without the other (a per-consumer disambiguation collapsing
6461        // the leaf-kind label onto `"binary"` while the directory stays
6462        // `"exe"`, or vice versa) trips at caixa-core build time rather
6463        // than surfacing at runtime as a mismatched
6464        // [`crate::LayoutInvariants::verify`] diagnostic whose `kind:`
6465        // reads one label while the `path:` sits under a differently-named
6466        // sub-tree.
6467        assert_eq!(
6468            crate::render::LAYOUT_DIR_EXE,
6469            crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
6470            "LAYOUT_DIR_EXE must equal LAYOUT_MISSING_ENTRY_KIND_EXE — \
6471             both name the M0 `:kind Binario` sub-tree by the same \
6472             three-byte scalar"
6473        );
6474    }
6475
6476    #[test]
6477    fn layout_dir_bib_is_distinct_from_layout_missing_entry_kind_bib() {
6478        // Cross-axis distinctness pin: [`crate::render::LAYOUT_DIR_LIB`]
6479        // (`"lib"`, the Cargo-style abbreviated on-disk directory name)
6480        // is *deliberately* distinct from
6481        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`]
6482        // (`"biblioteca"`, the full-form Portuguese-native leaf-kind
6483        // label) — the substrate splits the on-disk convention terse
6484        // (`lib/`) from the diagnostic vocabulary full (`biblioteca`),
6485        // matching Cargo's `src/lib.rs` abbreviation of the `library`
6486        // crate-type discriminator. A future accidental collapse of the
6487        // two axes onto one scalar (a rebrand aligning either arm with
6488        // the other for schema-clarity, an English-uniformity pass that
6489        // renames `LAYOUT_DIR_LIB` to `LAYOUT_DIR_BIBLIOTECA` or the
6490        // diagnostic label to `"lib"`) would silently reroute either
6491        // consumer onto the other's byte-string. This pin catches the
6492        // collapse at build time. Peer of the sibling
6493        // [`layout_missing_entry_kind_m0_consts_align_with_caixa_kind_as_str`]
6494        // (fe2a898) that pins the analogous *equality* between the M0
6495        // `:kind Biblioteca` diagnostic-label arm and
6496        // [`crate::CaixaKind::Biblioteca`]'s [`crate::CaixaKind::as_str`]
6497        // output — the two pins jointly encode the "which of the three
6498        // Biblioteca-related scalars are load-bearing-equal, which are
6499        // load-bearing-distinct" invariant across the substrate's
6500        // per-kind vocabulary.
6501        assert_ne!(
6502            crate::render::LAYOUT_DIR_LIB,
6503            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
6504            "LAYOUT_DIR_LIB (`\"lib\"`) and LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA \
6505             (`\"biblioteca\"`) name two distinct axes — the on-disk \
6506             directory convention (Cargo-style abbreviated) and the \
6507             layout-diagnostic leaf-kind label (full-form Portuguese) — \
6508             and must not silently collapse onto one scalar"
6509        );
6510    }
6511
6512    #[test]
6513    fn layout_dir_servicos_is_distinct_from_layout_missing_entry_kind_servico() {
6514        // Cross-axis distinctness pin: [`crate::render::LAYOUT_DIR_SERVICOS`]
6515        // (`"servicos"`, the Portuguese-*plural* on-disk directory
6516        // name) is *deliberately* distinct from
6517        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`]
6518        // (`"servico"`, the singular leaf-kind label) — the on-disk
6519        // sub-tree houses one-or-more ComputeUnit YAML descriptors per
6520        // caixa (hence the plural), the diagnostic label names the
6521        // caixa's own kind (singular). A future accidental collapse
6522        // onto one scalar (a per-consumer disambiguation aligning the
6523        // two, a hypothetical English-uniformity pass renaming
6524        // `"servicos"` → `"services"` while retaining `"servico"` on
6525        // the diagnostic arm — or vice versa) would silently reroute
6526        // either consumer onto the other's byte-string. Peer of the
6527        // sibling [`layout_dir_bib_is_distinct_from_layout_missing_entry_kind_bib`]
6528        // pin on the M0 `:kind Biblioteca` split axis; two of the three
6529        // per-kind on-disk / leaf-kind splits carry a distinctness
6530        // pin here, the third ([`crate::render::LAYOUT_DIR_EXE`] vs
6531        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`]) carries an
6532        // equality pin under
6533        // [`layout_dir_exe_matches_layout_missing_entry_kind_exe`].
6534        assert_ne!(
6535            crate::render::LAYOUT_DIR_SERVICOS,
6536            crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
6537            "LAYOUT_DIR_SERVICOS (`\"servicos\"`, plural on-disk sub-tree) \
6538             and LAYOUT_MISSING_ENTRY_KIND_SERVICO (`\"servico\"`, singular \
6539             leaf-kind label) name two distinct axes and must not silently \
6540             collapse onto one scalar"
6541        );
6542    }
6543
6544    #[test]
6545    fn layout_invariants_reconstruct_sandbox_roots_through_lifted_layout_dir_consts() {
6546        // Production-through-const pin: [`LayoutInvariants::verify`]
6547        // routes its three per-kind sandbox-root joins
6548        // (`root.join(LAYOUT_DIR_LIB)` for the `:kind Biblioteca`
6549        // default `lib/<nome>.lisp` reconstruction, `root.join(LAYOUT_DIR_EXE)`
6550        // for the [`LayoutError::ExeOutsideDir`] gate,
6551        // `root.join(LAYOUT_DIR_SERVICOS)` for the
6552        // [`LayoutError::ServicoOutsideDir`] gate) through the three
6553        // lifted consts, not through inline `"lib"` / `"exe"` /
6554        // `"servicos"` `&str` literals. This test drives the
6555        // [`LayoutError::ExeOutsideDir`] arm through a `:kind Binario`
6556        // caixa whose declared `:exe` entry deliberately escapes
6557        // `root.join(LAYOUT_DIR_EXE)` (a sibling `bin/tool` path) —
6558        // if the production emit reads the wrong const (or reverts to
6559        // an inline literal that drifts from the const) the diagnostic
6560        // arm surfaces the wrong variant, catching the drift at build
6561        // time rather than as a per-invocation runtime mismatch.
6562        //
6563        // Mirror of the peer production-through-const pin
6564        // [`crate::dep::tests::validate_no_self_dep_deps_field_routes_through_dep_author_key`]
6565        // (4da6fba) on the sibling M0 `:deps` `list:` diagnostic axis.
6566        use std::path::PathBuf;
6567        let root = PathBuf::from("/tmp/x");
6568        let manifest = root.join("caixa.lisp");
6569        let bin_entry_outside = root.join("bin/tool");
6570        let mut c = caixa(CaixaKind::Binario);
6571        c.exe = vec!["bin/tool".into()];
6572        let manifest_clone = manifest.clone();
6573        let outside_clone = bin_entry_outside.clone();
6574        let layout = StandardLayout::new()
6575            .with_path_exists(move |p| p == manifest_clone || p == outside_clone);
6576        let err = layout.verify(&c, &root).unwrap_err();
6577        match err {
6578            LayoutError::ExeOutsideDir(path) => {
6579                assert_eq!(
6580                    path, bin_entry_outside,
6581                    "ExeOutsideDir must carry the resolved `:exe` entry that \
6582                     escapes `root.join(LAYOUT_DIR_EXE)`"
6583                );
6584                // Byte-identity check: the escape must be against the
6585                // lifted `LAYOUT_DIR_EXE` sub-tree, not a stale inline
6586                // literal — a future const-edit that drifts from `"exe"`
6587                // reroutes `exe_dir` off the sandbox `bin/tool` escapes
6588                // from, and this pattern-arm miss re-surfaces here.
6589                assert!(
6590                    !path.starts_with(root.join(crate::render::LAYOUT_DIR_EXE)),
6591                    "resolved `:exe` entry {path:?} must escape the \
6592                     `root.join(LAYOUT_DIR_EXE)` sub-tree the production \
6593                     emit uses to gate the [`LayoutError::ExeOutsideDir`] arm"
6594                );
6595            }
6596            other => panic!("expected ExeOutsideDir, got {other:?}"),
6597        }
6598    }
6599
6600    #[test]
6601    fn upgrade_state_change_without_behavior_callback_surfaces_as_upgrade_violation() {
6602        // Wiring pin for the cross-slot composition gate
6603        // (`validate_upgrade_from_against_behavior`): a caixa whose
6604        // `:upgrade-from` declares a `(:state-change "lib/m.lisp")`
6605        // instruction but does not declare `:behavior :on-state-change`
6606        // surfaces at `feira build` time as a `LayoutError::UpgradeViolation`
6607        // naming the offending caixa + the entry's `:from` + the
6608        // offending script — not at hot-upgrade dispatch when the
6609        // operator reaches for the missing callback. Mirrors
6610        // `upgrade_from_downgrade_surfaces_as_upgrade_violation` on the
6611        // peer `:from` ↔ `:versao` cross-slot precedence gate.
6612        use crate::{UpgradeFromEntry, UpgradeInstruction};
6613        use std::path::PathBuf;
6614        let root = PathBuf::from("/tmp/x");
6615        let manifest = root.join("caixa.lisp");
6616        let svc = root.join("servicos/demo.computeunit.yaml");
6617        let mut c = caixa(CaixaKind::Servico);
6618        c.versao = "0.2.0".into();
6619        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6620        // `:behavior` is None (the canonical "I added the upgrade path
6621        // but never declared :behavior" footgun the gate closes); a
6622        // peer arm covers the BehaviorSpec-Some-but-on-state-change-
6623        // None shape in `upgrade::tests::behavior_gate_rejects_state_
6624        // change_when_on_state_change_is_none`.
6625        c.upgrade_from = vec![UpgradeFromEntry {
6626            from: "0.1.0".into(),
6627            instructions: vec![
6628                UpgradeInstruction::LoadModule {
6629                    module: "demo".into(),
6630                },
6631                UpgradeInstruction::StateChange {
6632                    script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
6633                },
6634            ],
6635        }];
6636        let manifest_clone = manifest.clone();
6637        let svc_clone = svc.clone();
6638        let layout =
6639            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
6640        let err = layout.verify(&c, &root).unwrap_err();
6641        match err {
6642            LayoutError::UpgradeViolation { caixa, issue } => {
6643                assert_eq!(caixa, "demo", "diagnostic must name the offending caixa");
6644                assert!(
6645                    issue.contains(crate::render::M2_BEHAVIOR_AUTHOR_KEY_ON_STATE_CHANGE),
6646                    "diagnostic must name the missing callback slot for self-locating fix, \
6647                     got {issue:?}"
6648                );
6649                assert!(
6650                    issue.contains("0.1.0"),
6651                    "diagnostic must name the offending entry's :from, got {issue:?}"
6652                );
6653                assert!(
6654                    issue.contains("v01-to-v02.lisp"),
6655                    "diagnostic must name the offending :script for self-locating fix, \
6656                     got {issue:?}"
6657                );
6658            }
6659            other => panic!("expected UpgradeViolation, got {other:?}"),
6660        }
6661    }
6662
6663    #[test]
6664    fn supervisor_must_have_children() {
6665        use crate::RestartStrategy;
6666        let root = PathBuf::from("/tmp/x");
6667        let manifest = root.join("caixa.lisp");
6668        let manifest_clone = manifest.clone();
6669        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6670        let mut c = caixa(CaixaKind::Supervisor);
6671        c.estrategia = Some(RestartStrategy::OneForOne);
6672        c.max_restarts = Some(5);
6673        // No children → should fail
6674        let err = layout.verify(&c, &root).unwrap_err();
6675        assert!(matches!(err, LayoutError::SupervisorViolation { .. }));
6676    }
6677
6678    #[test]
6679    fn supervisor_self_referential_child_is_violation() {
6680        // A Supervisor whose `:children` names its own `:nome` is a
6681        // one-node supervision cycle. The cross-slot gate fires at
6682        // verify time, surfacing as a SupervisorViolation that names the
6683        // offending supervisor — not at the cluster apply far from
6684        // source. The `caixa()` helper's `:nome` is "demo".
6685        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6686        let root = PathBuf::from("/tmp/x");
6687        let manifest = root.join("caixa.lisp");
6688        let manifest_clone = manifest.clone();
6689        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6690        let mut c = caixa(CaixaKind::Supervisor);
6691        c.estrategia = Some(RestartStrategy::OneForOne);
6692        c.max_restarts = Some(5);
6693        c.children = vec![
6694            ChildSpec {
6695                caixa: "worker".into(),
6696                versao: "^0.1".into(),
6697                restart: RestartPolicy::Permanent,
6698            },
6699            ChildSpec {
6700                caixa: "demo".into(),
6701                versao: "^0.1".into(),
6702                restart: RestartPolicy::Permanent,
6703            },
6704        ];
6705        let err = layout.verify(&c, &root).unwrap_err();
6706        let LayoutError::SupervisorViolation { caixa, issue } = err else {
6707            panic!("expected SupervisorViolation for self-referential child, got {err:?}");
6708        };
6709        assert_eq!(caixa, "demo");
6710        assert!(
6711            issue.contains("demo") && issue.contains("itself"),
6712            "issue must name the self-supervising caixa, got {issue:?}"
6713        );
6714    }
6715
6716    #[test]
6717    fn supervisor_distinct_children_pass_self_supervision_gate() {
6718        // Positive control: a Supervisor whose children are all distinct
6719        // from its own `:nome` verifies cleanly.
6720        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6721        let root = PathBuf::from("/tmp/x");
6722        let manifest = root.join("caixa.lisp");
6723        let manifest_clone = manifest.clone();
6724        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6725        let mut c = caixa(CaixaKind::Supervisor);
6726        c.estrategia = Some(RestartStrategy::OneForOne);
6727        c.max_restarts = Some(5);
6728        c.children = vec![ChildSpec {
6729            caixa: "worker".into(),
6730            versao: "^0.1".into(),
6731            restart: RestartPolicy::Permanent,
6732        }];
6733        layout.verify(&c, &root).unwrap();
6734    }
6735
6736    #[test]
6737    fn cross_slot_self_edge_gates_route_parent_nome_through_lifted_accessor() {
6738        // Composition pin: every cross-slot self-edge gate fired from
6739        // `LayoutInvariants::verify` — the supervision-tree arm's
6740        // `crate::supervisor::validate_no_self_supervision` call, the
6741        // Aplicacao arm's `crate::aplicacao::validate_no_self_membership`
6742        // call, and the dep-graph arm's
6743        // `crate::dep::validate_no_self_dep` call — must key its
6744        // `parent_nome` arg off the typed [`Caixa::nome`] accessor, not
6745        // the raw `&caixa.nome` `&String`-borrow of the underlying
6746        // field.
6747        //
6748        // Structurally: a rename of the storage field or a hypothetical
6749        // accessor rebrand (a per-cluster alias table pinned through a
6750        // future `:placement`-scoped slot, the M4 CR materializer's
6751        // per-CR namespace-qualified rewrite, a `:nome-suffix` overlay
6752        // the MESH-COMPOSITION §III.2 roadmap acknowledges) would land
6753        // through the accessor by construction; a raw-borrow bypass
6754        // would silently disagree with every peer consumer that already
6755        // routes through `caixa.nome()` (the caixa-mesh 980c059,
6756        // caixa-helm 22461ef, caixa-flux 162e2e2, caixa-crd 61d3429,
6757        // caixa-feira ef83332 raw-borrow converges), reintroducing the
6758        // drift surface the sibling converges closed. Each arm fires
6759        // its per-kind `LayoutError` variant (`SupervisorViolation` /
6760        // `AplicacaoViolation` / `DepsViolation`) whose `caixa` field
6761        // carries the offending parent name verbatim through
6762        // `caixa.nome().clone()`; asserting the field equals
6763        // `caixa.nome()` on the mutated fixture pins the accessor-
6764        // routed parent-nome projection at every call site — a future
6765        // silent detour that had the gate observe a stale / aliased
6766        // name at the arg boundary would surface here as a
6767        // `caixa != "demo"` inequality.
6768        //
6769        // Peer of the sibling per-caixa-crate `nome`-arg raw-borrow
6770        // convergence pin discipline (54bf2f3 / 22461ef / 162e2e2 on the
6771        // renderer crates; ef83332 on the CLI) — extends the "one typed
6772        // dispatch per `:nome` consumer" discipline onto the substrate's
6773        // own [`LayoutInvariants::verify`] cross-slot self-edge gate
6774        // wire-up on all three typed-name-graph kinds.
6775        use crate::{
6776            ChildSpec, Dep, Membro, Placement, PlacementStrategy, RestartPolicy, RestartStrategy,
6777        };
6778        let root = PathBuf::from("/tmp/x");
6779        let manifest = root.join("caixa.lisp");
6780        let manifest_clone = manifest.clone();
6781        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6782
6783        // Supervisor arm — the `caixa()` helper's `:nome` is "demo",
6784        // and the accessor's return `caixa.nome()` must equal the
6785        // parent-nome that the self-supervision gate observes.
6786        let mut sup = caixa(CaixaKind::Supervisor);
6787        sup.estrategia = Some(RestartStrategy::OneForOne);
6788        sup.max_restarts = Some(5);
6789        sup.children = vec![ChildSpec {
6790            caixa: "demo".into(),
6791            versao: "^0.1".into(),
6792            restart: RestartPolicy::Permanent,
6793        }];
6794        let parent_nome_via_accessor = sup.nome();
6795        assert_eq!(
6796            parent_nome_via_accessor, "demo",
6797            "the caixa() fixture helper's `:nome` must be \"demo\" — \
6798             the accessor's return is the pin's ground truth for the \
6799             cross-slot gate's parent-nome arg",
6800        );
6801        let err = layout.verify(&sup, &root).unwrap_err();
6802        let LayoutError::SupervisorViolation { caixa: c_nome, .. } = err else {
6803            panic!("expected SupervisorViolation for self-referential child, got {err:?}");
6804        };
6805        assert_eq!(
6806            c_nome, parent_nome_via_accessor,
6807            "the SupervisorViolation's `caixa` field must equal \
6808             `sup.nome()` — the cross-slot self-supervision gate's \
6809             `parent_nome` arg must route through the lifted \
6810             [`Caixa::nome`] accessor, not the raw `&caixa.nome` \
6811             `&String`-borrow of the underlying field",
6812        );
6813
6814        // Aplicacao arm — same discipline on the peer typed-name-graph
6815        // kind. Constructed alongside the supervisor arm so any future
6816        // accessor drift lands on both arms in the same pin.
6817        let mut app = caixa(CaixaKind::Aplicacao);
6818        app.placement = Some(Placement {
6819            estrategia: PlacementStrategy::Replicated,
6820            clusters: vec!["rio".into()],
6821            affinity: None,
6822            shard_key: None,
6823        });
6824        app.membros = vec![Membro {
6825            caixa: "demo".into(),
6826            versao: "^0.1".into(),
6827        }];
6828        let parent_nome_via_accessor = app.nome();
6829        assert_eq!(
6830            parent_nome_via_accessor, "demo",
6831            "the caixa() fixture helper's `:nome` must be \"demo\" on \
6832             the Aplicacao arm too — same accessor-ground-truth as the \
6833             sibling supervisor arm above",
6834        );
6835        let err = layout.verify(&app, &root).unwrap_err();
6836        let LayoutError::AplicacaoViolation { caixa: c_nome, .. } = err else {
6837            panic!("expected AplicacaoViolation for self-referential membro, got {err:?}");
6838        };
6839        assert_eq!(
6840            c_nome, parent_nome_via_accessor,
6841            "the AplicacaoViolation's `caixa` field must equal \
6842             `app.nome()` — the cross-slot self-membership gate's \
6843             `parent_nome` arg must route through the lifted \
6844             [`Caixa::nome`] accessor, not the raw `&caixa.nome` \
6845             `&String`-borrow of the underlying field",
6846        );
6847
6848        // Dep-graph arm — third typed-name-graph kind on the
6849        // `parent_nome` arg boundary. Same discipline as the peer
6850        // supervision-tree and Aplicacao-membership arms above.
6851        // Constructed alongside so any future accessor drift lands on
6852        // all three arms in the same pin. Needs a distinct layout
6853        // fixture from the supervisor / aplicacao arms above because
6854        // the `Biblioteca` kind's code-path existence gate demands the
6855        // canonical `lib/<nome>.lisp` path also `exists`, so the shim
6856        // covers both `caixa.lisp` and `lib/demo.lisp`.
6857        let default_lib = root.join("lib").join("demo.lisp");
6858        let manifest_dep = manifest.clone();
6859        let default_lib_clone = default_lib.clone();
6860        let layout_dep = StandardLayout::new()
6861            .with_path_exists(move |p| p == manifest_dep || p == default_lib_clone);
6862        let mut lib = caixa(CaixaKind::Biblioteca);
6863        lib.deps = vec![Dep::simple("demo", "^0.1")];
6864        let parent_nome_via_accessor = lib.nome();
6865        assert_eq!(
6866            parent_nome_via_accessor, "demo",
6867            "the caixa() fixture helper's `:nome` must be \"demo\" on \
6868             the Biblioteca arm too — same accessor-ground-truth as the \
6869             sibling supervisor + Aplicacao arms above",
6870        );
6871        let err = layout_dep.verify(&lib, &root).unwrap_err();
6872        let LayoutError::DepsViolation { caixa: c_nome, .. } = err else {
6873            panic!("expected DepsViolation for self-referential :deps entry, got {err:?}");
6874        };
6875        assert_eq!(
6876            c_nome, parent_nome_via_accessor,
6877            "the DepsViolation's `caixa` field must equal \
6878             `lib.nome()` — the cross-slot self-dep gate's `parent_nome` \
6879             arg must route through the lifted [`Caixa::nome`] accessor, \
6880             not the raw `&caixa.nome` `&String`-borrow of the underlying \
6881             field",
6882        );
6883    }
6884
6885    #[test]
6886    fn upgrade_against_versao_gate_routes_current_versao_through_lifted_accessor() {
6887        // Composition pin: the cross-slot `:upgrade-from :from` ↔
6888        // `:versao` precedence gate fired from
6889        // `LayoutInvariants::verify` — the
6890        // `crate::upgrade::validate_upgrade_from_against_versao` call —
6891        // must key its `versao` arg off the typed [`Caixa::versao`]
6892        // accessor, not the raw `&caixa.versao` `&String`-borrow of
6893        // the underlying field.
6894        //
6895        // Same "arg-boundary reads through the lifted accessor"
6896        // discipline as the sibling
6897        // [`cross_slot_self_edge_gates_route_parent_nome_through_lifted_accessor`]
6898        // pin above on the `:nome`-arg axis of the three typed-name-
6899        // graph self-edge gates — extended here onto the `:versao`-arg
6900        // axis of the substrate's remaining `LayoutInvariants::verify`
6901        // cross-slot arg-carrying call site. Structurally byte-equal
6902        // today (the accessor is `pub fn versao(&self) -> &str { &self.versao }`,
6903        // so both paths coerce to the same `&str`); the pin catches a
6904        // future silent detour (an accessor rebrand that no longer
6905        // shipped the raw slot verbatim — a per-`:edicao` overlay,
6906        // a promotion of `:versao` to a `CaixaVersion` newtype with a
6907        // canonicalizing accessor, an M4 CR-materializer-side pinning
6908        // through a resolver-annotated `:versao-resolved` slot) that
6909        // would silently split the substrate's own precedence gate
6910        // from every peer consumer already routing `:versao` reads
6911        // through the lifted accessor.
6912        //
6913        // The gate fires an `UpgradeViolation { caixa, issue }` when a
6914        // `:upgrade-from` entry's `:from` is not strictly less than
6915        // the top-level `:versao` — the `issue` string names both the
6916        // offending prior version and the current version verbatim,
6917        // so asserting the substring `caixa.versao()` appears in the
6918        // fired diagnostic pins the accessor-routed current-versao
6919        // projection at the arg boundary. A raw-borrow bypass would
6920        // still surface the same bytes today, but the presence of
6921        // this pin makes any future divergence between the accessor's
6922        // return and the raw slot's contents a build-time failure at
6923        // this call site.
6924        use crate::{UpgradeFromEntry, UpgradeInstruction};
6925        let root = PathBuf::from("/tmp/x");
6926        let manifest = root.join("caixa.lisp");
6927        let servico_path = root.join("servicos").join("demo.computeunit.yaml");
6928        let manifest_clone = manifest.clone();
6929        let servico_clone = servico_path.clone();
6930        let layout = StandardLayout::new()
6931            .with_path_exists(move |p| p == manifest_clone || p == servico_clone);
6932        let mut svc = caixa(CaixaKind::Servico);
6933        svc.versao = "0.1.0".into();
6934        svc.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6935        // `:from` >= current `:versao` — trips the precedence gate the
6936        // `validate_upgrade_from_against_versao` cross-slot call
6937        // enforces. `:load-module` carries no on-disk path so the
6938        // path-existence gate downstream stays inert; the precedence
6939        // gate is what fires. No `:on-state-change` needed because the
6940        // instruction list carries no `:state-change` entry, so the
6941        // sibling `validate_upgrade_from_against_behavior` gate is
6942        // inert too.
6943        svc.upgrade_from = vec![UpgradeFromEntry {
6944            from: "0.2.0".into(),
6945            instructions: vec![UpgradeInstruction::LoadModule {
6946                module: "demo".into(),
6947            }],
6948        }];
6949        let current_versao_via_accessor = svc.versao().to_string();
6950        assert_eq!(
6951            current_versao_via_accessor, "0.1.0",
6952            "the mutated fixture's `:versao` must be observable through \
6953             the accessor before layout verification fires — a drift on \
6954             `Caixa::versao` would surface here as a `!= \"0.1.0\"` \
6955             inequality",
6956        );
6957        let err = layout.verify(&svc, &root).unwrap_err();
6958        let LayoutError::UpgradeViolation {
6959            caixa: c_nome,
6960            issue,
6961        } = err
6962        else {
6963            panic!("expected UpgradeViolation for :from >= :versao, got {err:?}");
6964        };
6965        assert_eq!(c_nome, svc.nome(), "wrap envelope names the caixa");
6966        assert!(
6967            issue.contains(&current_versao_via_accessor),
6968            "the UpgradeViolation's `issue` must quote the current \
6969             `:versao` byte-string verbatim — the cross-slot precedence \
6970             gate's `versao` arg must route through the lifted \
6971             [`Caixa::versao`] accessor, not the raw `&caixa.versao` \
6972             `&String`-borrow of the underlying field. issue: {issue}",
6973        );
6974    }
6975
6976    #[test]
6977    fn layout_violation_envelopes_carry_caixa_nome_through_lifted_accessor() {
6978        // Wrap-envelope drift-detection pin: every per-axis
6979        // `LayoutError::*Violation { caixa, issue }` envelope fired
6980        // from `LayoutInvariants::verify` must key its offending-caixa
6981        // field off the typed [`Caixa::nome`] accessor's
6982        // `.to_string()` extension, not the raw
6983        // `caixa.nome.clone()` `String::clone()` of the underlying
6984        // field. Structurally byte-equal today (each accessor is
6985        // `pub fn nome(&self) -> &str { &self.nome }`, so
6986        // `caixa.nome().to_string()` and `caixa.nome.clone()` produce
6987        // the same bytes); the pin catches a future silent detour
6988        // (an accessor rebrand that no longer shipped the raw slot
6989        // verbatim — a per-cluster alias table pinned through a
6990        // future `:placement`-scoped slot, the M4 CR materializer's
6991        // per-CR namespace-qualified rewrite, a `:nome-suffix`
6992        // overlay the MESH-COMPOSITION §III.2 roadmap acknowledges)
6993        // that would silently split the substrate's own layout
6994        // invariant verifier's diagnostic surface from every peer
6995        // caixa-crate consumer that already routes `:nome` reads
6996        // through the lifted accessor (the caixa-mesh 980c059,
6997        // caixa-helm 22461ef, caixa-flux 162e2e2, caixa-crd 61d3429,
6998        // caixa-feira ef83332 raw-borrow converges).
6999        //
7000        // Exercises a representative variant on each of the three
7001        // wrap-envelope arm shapes the substrate's per-axis fan-out
7002        // carries: (1) `LayoutError::NomeViolation` (the leading arm
7003        // in the `verify` order — the `:nome` axis's DNS-1123 shape
7004        // gate fires immediately after the manifest-existence gate),
7005        // (2) `LayoutError::BinarioWithoutExe` (a tuple-variant on
7006        // the kind-coherence family — different envelope shape than
7007        // the struct-variant `*Violation { caixa, issue }` family
7008        // but the same converge target on the `caixa.nome().to_string()`
7009        // arg), and (3) `LayoutError::ServicoWithoutServicos` (the
7010        // sibling tuple-variant on the same kind-coherence family).
7011        // Together they cover the two `LayoutError` envelope shapes
7012        // (struct-variant + tuple-variant) the layout invariants file
7013        // emits on `:nome`-carrying arms.
7014        //
7015        // Peer of the sibling
7016        // [`cross_slot_self_edge_gates_route_parent_nome_through_lifted_accessor`]
7017        // pin above — extends the "wrap-envelope `caixa:` field
7018        // reads through the lifted accessor" discipline from the
7019        // cross-slot self-edge gates' `parent_nome` arg boundary
7020        // onto the per-axis `LayoutError::*Violation` envelope's
7021        // `caixa:` field boundary.
7022
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
7028        // (1) `NomeViolation` on the struct-variant envelope: force a
7029        // DNS-1123-invalid `:nome` (uppercase byte — `is_dns_1123_label`
7030        // rejects) and assert the fired envelope's `caixa:` field
7031        // byte-equals `c.nome().to_string()`.
7032        let mut c = caixa(CaixaKind::Biblioteca);
7033        c.nome = "BAD_NAME".into();
7034        c.bibliotecas = vec!["lib/demo.lisp".into()];
7035        let expected_nome_via_accessor = c.nome().to_string();
7036        assert_eq!(
7037            expected_nome_via_accessor, "BAD_NAME",
7038            "the mutated fixture's `:nome` must be observable through \
7039             the accessor before layout verification fires — a drift \
7040             on `Caixa::nome` would surface here as a `!= \"BAD_NAME\"` \
7041             inequality",
7042        );
7043        let err = layout.verify(&c, &root).unwrap_err();
7044        let LayoutError::NomeViolation { caixa: c_nome, .. } = err else {
7045            panic!("expected NomeViolation for DNS-1123-invalid :nome, got {err:?}");
7046        };
7047        assert_eq!(
7048            c_nome, expected_nome_via_accessor,
7049            "the NomeViolation's `caixa` field must equal \
7050             `c.nome().to_string()` — the wrap envelope's per-axis \
7051             projection must route through the lifted [`Caixa::nome`] \
7052             accessor's `.to_string()` extension, not the raw \
7053             `caixa.nome.clone()` `String::clone()` of the underlying \
7054             field",
7055        );
7056
7057        // (2) `BinarioWithoutExe` on the tuple-variant envelope: a
7058        // Binario-kind caixa with an empty `:exe` list fires the
7059        // kind-coherence gate whose payload is a bare `String`, so the
7060        // pattern is `LayoutError::BinarioWithoutExe(String)` rather
7061        // than the struct-variant `{ caixa, issue }` family. The
7062        // converge target is the same — `caixa.nome().to_string()` — but
7063        // the envelope shape is different, so the pin exercises both.
7064        let mut c = caixa(CaixaKind::Binario);
7065        // `:exe` empty is the trigger — the fixture helper defaults
7066        // it to `vec![]`, so no mutation is needed.
7067        c.nome = "binario-demo".into();
7068        let expected_nome_via_accessor = c.nome().to_string();
7069        assert_eq!(
7070            expected_nome_via_accessor, "binario-demo",
7071            "the mutated fixture's `:nome` must be observable through \
7072             the accessor before layout verification fires",
7073        );
7074        let err = layout.verify(&c, &root).unwrap_err();
7075        let LayoutError::BinarioWithoutExe(c_nome) = err else {
7076            panic!("expected BinarioWithoutExe for empty :exe list on Binario kind, got {err:?}");
7077        };
7078        assert_eq!(
7079            c_nome, expected_nome_via_accessor,
7080            "the BinarioWithoutExe's payload must equal \
7081             `c.nome().to_string()` — the tuple-variant envelope's \
7082             per-axis projection must route through the lifted \
7083             [`Caixa::nome`] accessor's `.to_string()` extension, not \
7084             the raw `caixa.nome.clone()` `String::clone()` of the \
7085             underlying field",
7086        );
7087
7088        // (3) `ServicoWithoutServicos` on the sibling tuple-variant
7089        // envelope: same discipline on the peer kind-coherence
7090        // partition arm. Constructed alongside the Binario arm so any
7091        // future accessor drift lands on both arms in the same pin.
7092        let mut c = caixa(CaixaKind::Servico);
7093        // `:servicos` empty is the trigger — the fixture helper
7094        // defaults it to `vec![]`, so no mutation is needed.
7095        c.nome = "servico-demo".into();
7096        let expected_nome_via_accessor = c.nome().to_string();
7097        assert_eq!(
7098            expected_nome_via_accessor, "servico-demo",
7099            "the mutated fixture's `:nome` must be observable through \
7100             the accessor before layout verification fires",
7101        );
7102        let err = layout.verify(&c, &root).unwrap_err();
7103        let LayoutError::ServicoWithoutServicos(c_nome) = err else {
7104            panic!(
7105                "expected ServicoWithoutServicos for empty :servicos list on Servico kind, \
7106                 got {err:?}"
7107            );
7108        };
7109        assert_eq!(
7110            c_nome, expected_nome_via_accessor,
7111            "the ServicoWithoutServicos's payload must equal \
7112             `c.nome().to_string()` — same converge discipline as the \
7113             sibling `BinarioWithoutExe` tuple-variant arm above",
7114        );
7115    }
7116
7117    #[test]
7118    fn supervisor_must_not_have_bibliotecas() {
7119        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
7120        let root = PathBuf::from("/tmp/x");
7121        let manifest = root.join("caixa.lisp");
7122        let manifest_clone = manifest.clone();
7123        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7124        let mut c = caixa(CaixaKind::Supervisor);
7125        c.estrategia = Some(RestartStrategy::OneForOne);
7126        c.max_restarts = Some(5);
7127        c.bibliotecas = vec!["lib/code.lisp".into()];
7128        c.children = vec![ChildSpec {
7129            caixa: "worker".into(),
7130            versao: "^0.1".into(),
7131            restart: RestartPolicy::Permanent,
7132        }];
7133        let err = layout.verify(&c, &root).unwrap_err();
7134        assert!(matches!(err, LayoutError::SupervisorOwnsCode(_)));
7135    }
7136
7137    // ── Caixa::validate_restart_window wired into Supervisor verify ─────
7138    //
7139    // Until this wire-up landed `Caixa::validate_restart_window` lived as
7140    // `pub fn` on `Caixa` with full per-arm unit coverage in
7141    // `manifest::tests` (`validate_restart_window_rejects_*` — fractional,
7142    // decimal-shaped integer, half-unit minute, leading sign, unknown
7143    // unit, garbage, empty-after-trim) but no production path called it;
7144    // `feira build` silently accepted malformed `:restart-window` and
7145    // `Caixa::supervisor_view` soft-swallowed the parse failure as
7146    // `restart_window: None` (the canonical "no reset" sentinel), turning
7147    // every authoring footgun into a never-reset supervisor far from the
7148    // source caixa.lisp. The following pins fence the layout-pipeline
7149    // wire-up: every layout verify on a structurally-invalid `:restart-
7150    // window` axis surfaces the per-axis `RestartWindowViolation { caixa,
7151    // issue }` envelope before the typed `SupervisorSpec::validate` gate
7152    // sees the laundered `None`.
7153
7154    fn supervisor_with_window(window: Option<&str>) -> Caixa {
7155        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
7156        let mut c = caixa(CaixaKind::Supervisor);
7157        c.estrategia = Some(RestartStrategy::OneForOne);
7158        c.max_restarts = Some(5);
7159        c.restart_window = window.map(str::to_string);
7160        c.children = vec![ChildSpec {
7161            caixa: "worker".into(),
7162            versao: "^0.1".into(),
7163            restart: RestartPolicy::Permanent,
7164        }];
7165        c
7166    }
7167
7168    #[test]
7169    fn restart_window_violation_on_fractional_seconds() {
7170        // `"1.5s"` is the canonical fractional-seconds drift footgun the
7171        // shared integer-magnitude codec (1c55a2a) rejects: round-trips
7172        // through `render` as `"1500ms"` on first serialize, breaking
7173        // THEORY.md §V.2.7 render-determinism. Before this wire-up
7174        // `supervisor_view` soft-swallowed the parse error as
7175        // `restart_window: None`, masking the drift as a never-reset
7176        // supervisor.
7177        let root = PathBuf::from("/tmp/x");
7178        let manifest = root.join("caixa.lisp");
7179        let manifest_clone = manifest.clone();
7180        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7181        let c = supervisor_with_window(Some("1.5s"));
7182        let err = layout.verify(&c, &root).unwrap_err();
7183        let LayoutError::RestartWindowViolation { caixa, issue } = err else {
7184            panic!("expected LayoutError::RestartWindowViolation, got {err:?}");
7185        };
7186        assert_eq!(caixa, "demo");
7187        assert!(
7188            issue.contains("1.5s"),
7189            "issue must quote the offending raw value: {issue}",
7190        );
7191    }
7192
7193    #[test]
7194    fn restart_window_violation_on_decimal_shaped_integer() {
7195        // `"1.0s"` — decimal-shaped integer the codec also rejects (a
7196        // canonical authoring form is `"1s"`). Sibling of the fractional
7197        // case; same codec arm.
7198        let root = PathBuf::from("/tmp/x");
7199        let manifest = root.join("caixa.lisp");
7200        let manifest_clone = manifest.clone();
7201        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7202        let c = supervisor_with_window(Some("1.0s"));
7203        let err = layout.verify(&c, &root).unwrap_err();
7204        assert!(
7205            matches!(
7206                err,
7207                LayoutError::RestartWindowViolation { ref caixa, ref issue }
7208                    if caixa == "demo" && issue.contains("1.0s")
7209            ),
7210            "got {err:?}",
7211        );
7212    }
7213
7214    #[test]
7215    fn restart_window_violation_on_leading_sign() {
7216        // `"+30s"` / `"-30s"` — leading-sign drift the codec rejects.
7217        // Canonical form is `"30s"`. Pin both signs separately because
7218        // a future relaxation might accept one but not the other.
7219        for raw in ["+30s", "-30s"] {
7220            let root = PathBuf::from("/tmp/x");
7221            let manifest = root.join("caixa.lisp");
7222            let manifest_clone = manifest.clone();
7223            let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7224            let c = supervisor_with_window(Some(raw));
7225            let err = layout.verify(&c, &root).unwrap_err();
7226            assert!(
7227                matches!(
7228                    err,
7229                    LayoutError::RestartWindowViolation { ref caixa, ref issue }
7230                        if caixa == "demo" && issue.contains(raw)
7231                ),
7232                "leading-sign {raw:?} got {err:?}",
7233            );
7234        }
7235    }
7236
7237    #[test]
7238    fn restart_window_violation_on_unknown_unit() {
7239        // `"30x"` — unknown duration unit. The codec admits only
7240        // `ms`/`s`/`m`/`h`.
7241        let root = PathBuf::from("/tmp/x");
7242        let manifest = root.join("caixa.lisp");
7243        let manifest_clone = manifest.clone();
7244        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7245        let c = supervisor_with_window(Some("30x"));
7246        let err = layout.verify(&c, &root).unwrap_err();
7247        assert!(
7248            matches!(
7249                err,
7250                LayoutError::RestartWindowViolation { ref caixa, .. } if caixa == "demo"
7251            ),
7252            "got {err:?}",
7253        );
7254    }
7255
7256    #[test]
7257    fn restart_window_violation_on_garbage() {
7258        // `"abc"` — pure garbage. The codec's parse fails before the
7259        // unit dispatch; the wrap envelope still surfaces the
7260        // self-locating diagnostic at the source.
7261        let root = PathBuf::from("/tmp/x");
7262        let manifest = root.join("caixa.lisp");
7263        let manifest_clone = manifest.clone();
7264        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7265        let c = supervisor_with_window(Some("abc"));
7266        let err = layout.verify(&c, &root).unwrap_err();
7267        assert!(
7268            matches!(err, LayoutError::RestartWindowViolation { ref caixa, .. } if caixa == "demo"),
7269            "got {err:?}",
7270        );
7271    }
7272
7273    #[test]
7274    fn restart_window_violation_on_empty_string() {
7275        // `""` — empty after trim. The shared codec's digit-only gate
7276        // refuses an empty magnitude. Distinguished here from the
7277        // `None` ("omit the slot") canonical authoring shape: an empty
7278        // string is an authored-but-empty slot, never the author's
7279        // intent.
7280        let root = PathBuf::from("/tmp/x");
7281        let manifest = root.join("caixa.lisp");
7282        let manifest_clone = manifest.clone();
7283        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7284        let c = supervisor_with_window(Some(""));
7285        let err = layout.verify(&c, &root).unwrap_err();
7286        assert!(
7287            matches!(err, LayoutError::RestartWindowViolation { ref caixa, .. } if caixa == "demo"),
7288            "got {err:?}",
7289        );
7290    }
7291
7292    #[test]
7293    fn verify_accepts_supervisor_without_restart_window() {
7294        // `None` is the canonical "omit the slot to express no reset"
7295        // shape — never reaches the codec, validates cleanly.
7296        let root = PathBuf::from("/tmp/x");
7297        let manifest = root.join("caixa.lisp");
7298        let manifest_clone = manifest.clone();
7299        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7300        let c = supervisor_with_window(None);
7301        layout.verify(&c, &root).unwrap();
7302    }
7303
7304    #[test]
7305    fn verify_accepts_supervisor_with_canonical_restart_window() {
7306        // Every canonical form the shared codec round-trips losslessly
7307        // must pass — `"500ms"`, `"30s"`, `"60s"`, `"1m"`, `"2m"`,
7308        // `"1h"`. Pin every form so a future tightening of the codec's
7309        // accepted set surfaces here as a test failure.
7310        for form in ["500ms", "30s", "60s", "1m", "2m", "1h"] {
7311            let root = PathBuf::from("/tmp/x");
7312            let manifest = root.join("caixa.lisp");
7313            let manifest_clone = manifest.clone();
7314            let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7315            let c = supervisor_with_window(Some(form));
7316            layout
7317                .verify(&c, &root)
7318                .unwrap_or_else(|e| panic!("canonical {form:?} must validate, got {e:?}"));
7319        }
7320    }
7321
7322    #[test]
7323    fn restart_window_violation_fires_before_supervisor_view_validate() {
7324        // Diagnostic-precedence pin: a Supervisor with a malformed
7325        // `:restart-window` AND a typed-shape defect on the typed view
7326        // (zero `:max-restarts`, which `SupervisorSpec::validate`'s
7327        // `ZeroMaxRestarts` arm rejects) surfaces the raw-string
7328        // diagnostic first — the narrower self-locating gate wins. Until
7329        // this wire-up landed `supervisor_view` would silently launder
7330        // the malformed `:restart-window` to `None` and then the typed
7331        // view's `ZeroMaxRestarts` gate would surface, masking the
7332        // raw-string footgun.
7333        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
7334        let root = PathBuf::from("/tmp/x");
7335        let manifest = root.join("caixa.lisp");
7336        let manifest_clone = manifest.clone();
7337        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7338        let mut c = caixa(CaixaKind::Supervisor);
7339        c.estrategia = Some(RestartStrategy::OneForOne);
7340        c.max_restarts = Some(0);
7341        c.restart_window = Some("1.5s".into());
7342        c.children = vec![ChildSpec {
7343            caixa: "worker".into(),
7344            versao: "^0.1".into(),
7345            restart: RestartPolicy::Permanent,
7346        }];
7347        let err = layout.verify(&c, &root).unwrap_err();
7348        assert!(
7349            matches!(err, LayoutError::RestartWindowViolation { .. }),
7350            "got {err:?} — RestartWindowViolation must fire before SupervisorViolation",
7351        );
7352    }
7353
7354    #[test]
7355    fn supervisor_slots_on_non_supervisor_fires_before_restart_window_violation() {
7356        // Order pin: a non-Supervisor caixa with a malformed
7357        // `:restart-window` surfaces `SupervisorSlotsOnNonSupervisor`
7358        // (the kind-coherence gate at the top of verify) before the
7359        // raw-string parse gate inside the Supervisor branch — because
7360        // `:restart-window` is foreign to non-Supervisor kinds, the
7361        // kind-coherence diagnostic is the load-bearing one. Mirrors
7362        // the existing `nome_violation_on_*` ordering tests that fence
7363        // the precedence between universal and kind-specific gates.
7364        let root = PathBuf::from("/tmp/x");
7365        let manifest = root.join("caixa.lisp");
7366        let default_lib = root.join("lib").join("demo.lisp");
7367        let layout =
7368            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
7369        let mut c = caixa(CaixaKind::Biblioteca);
7370        c.restart_window = Some("1.5s".into());
7371        let err = layout.verify(&c, &root).unwrap_err();
7372        assert!(
7373            matches!(err, LayoutError::SupervisorSlotsOnNonSupervisor { .. }),
7374            "got {err:?} — kind-coherence must fire before RestartWindowViolation",
7375        );
7376    }
7377
7378    #[test]
7379    fn restart_window_violation_diagnostic_carries_offending_value() {
7380        // Diagnostic-shape pin: the wrap envelope's `issue` carries the
7381        // codec's parser-shaped reason verbatim (which names the
7382        // offending raw value), so the author can grep their caixa.lisp
7383        // for `:restart-window "<value>"` and fix in one edit. Mirrors
7384        // `nome_violation_*_carries_offending_*` shape pins.
7385        let root = PathBuf::from("/tmp/x");
7386        let manifest = root.join("caixa.lisp");
7387        let manifest_clone = manifest.clone();
7388        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7389        let c = supervisor_with_window(Some("0.5m"));
7390        let err = layout.verify(&c, &root).unwrap_err();
7391        let LayoutError::RestartWindowViolation { caixa, issue } = err else {
7392            panic!("expected LayoutError::RestartWindowViolation, got {err:?}");
7393        };
7394        assert_eq!(caixa, "demo");
7395        assert!(
7396            issue.contains("0.5m"),
7397            "issue must quote the offending raw value verbatim: {issue}",
7398        );
7399        assert!(
7400            !issue.is_empty(),
7401            "issue must carry the codec's parser-shaped reason",
7402        );
7403    }
7404
7405    // ── Aplicacao layout tests ──────────────────────────────────────────
7406
7407    #[test]
7408    fn aplicacao_must_have_membros() {
7409        use crate::{Membro, Placement, PlacementStrategy};
7410        let root = PathBuf::from("/tmp/x");
7411        let manifest = root.join("caixa.lisp");
7412        let manifest_clone = manifest.clone();
7413        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7414        let mut c = caixa(CaixaKind::Aplicacao);
7415        c.placement = Some(Placement {
7416            estrategia: PlacementStrategy::Replicated,
7417            clusters: vec!["rio".into()],
7418            affinity: None,
7419            shard_key: None,
7420        });
7421        // No membros → fails
7422        let err = layout.verify(&c, &root).unwrap_err();
7423        assert!(matches!(err, LayoutError::AplicacaoViolation { .. }));
7424
7425        // With membros → passes
7426        c.membros = vec![Membro {
7427            caixa: "service-a".into(),
7428            versao: "^0.1".into(),
7429        }];
7430        layout.verify(&c, &root).unwrap();
7431    }
7432
7433    #[test]
7434    fn aplicacao_self_referential_membro_is_violation() {
7435        // An Aplicacao whose `:membros` names its own `:nome` is a
7436        // one-node lacre-closure recursion. The cross-slot gate fires
7437        // at verify time, surfacing as an AplicacaoViolation that
7438        // names the offending aplicacao — not at lacre-resolve time
7439        // far from source. The `caixa()` helper's `:nome` is "demo".
7440        // Peer of `supervisor_self_referential_child_is_violation`
7441        // on the supervision-tree axis.
7442        use crate::{Membro, Placement, PlacementStrategy};
7443        let root = PathBuf::from("/tmp/x");
7444        let manifest = root.join("caixa.lisp");
7445        let manifest_clone = manifest.clone();
7446        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7447        let mut c = caixa(CaixaKind::Aplicacao);
7448        c.placement = Some(Placement {
7449            estrategia: PlacementStrategy::Replicated,
7450            clusters: vec!["rio".into()],
7451            affinity: None,
7452            shard_key: None,
7453        });
7454        c.membros = vec![
7455            Membro {
7456                caixa: "service-a".into(),
7457                versao: "^0.1".into(),
7458            },
7459            Membro {
7460                caixa: "demo".into(),
7461                versao: "^0.1".into(),
7462            },
7463        ];
7464        let err = layout.verify(&c, &root).unwrap_err();
7465        let LayoutError::AplicacaoViolation { caixa, issue } = err else {
7466            panic!("expected AplicacaoViolation for self-referential membro, got {err:?}");
7467        };
7468        assert_eq!(caixa, "demo");
7469        assert!(
7470            issue.contains("demo") && issue.contains("lists itself"),
7471            "issue must name the self-membering aplicacao, got {issue:?}"
7472        );
7473    }
7474
7475    #[test]
7476    fn aplicacao_distinct_membros_pass_self_membership_gate() {
7477        // Positive control: an Aplicacao whose membros are all distinct
7478        // from its own `:nome` verifies cleanly.
7479        use crate::{Membro, Placement, PlacementStrategy};
7480        let root = PathBuf::from("/tmp/x");
7481        let manifest = root.join("caixa.lisp");
7482        let manifest_clone = manifest.clone();
7483        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7484        let mut c = caixa(CaixaKind::Aplicacao);
7485        c.placement = Some(Placement {
7486            estrategia: PlacementStrategy::Replicated,
7487            clusters: vec!["rio".into()],
7488            affinity: None,
7489            shard_key: None,
7490        });
7491        c.membros = vec![
7492            Membro {
7493                caixa: "service-a".into(),
7494                versao: "^0.1".into(),
7495            },
7496            Membro {
7497                caixa: "service-b".into(),
7498                versao: "^0.1".into(),
7499            },
7500        ];
7501        layout.verify(&c, &root).unwrap();
7502    }
7503
7504    #[test]
7505    fn aplicacao_self_membership_fires_after_view_validate() {
7506        // Diagnostic-precedence pin: a self-referential membro alongside
7507        // a duplicate-:caixa shape surfaces the more-fundamental
7508        // `MembroDuplicate` (from `view.validate()`) first; only when the
7509        // per-membros shape diagnostics pass does the cross-slot
7510        // self-membership gate fire. Mirrors the ordering pin
7511        // `supervisor_self_referential_child_is_violation` carries on
7512        // the peer supervision-tree axis (`view.validate()` runs first,
7513        // then the cross-slot gate). Without this ordering a future
7514        // refactor that swaps the two calls would silently mask the
7515        // narrower per-membro defect.
7516        use crate::{Membro, Placement, PlacementStrategy};
7517        let root = PathBuf::from("/tmp/x");
7518        let manifest = root.join("caixa.lisp");
7519        let manifest_clone = manifest.clone();
7520        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7521        let mut c = caixa(CaixaKind::Aplicacao);
7522        c.placement = Some(Placement {
7523            estrategia: PlacementStrategy::Replicated,
7524            clusters: vec!["rio".into()],
7525            affinity: None,
7526            shard_key: None,
7527        });
7528        c.membros = vec![
7529            Membro {
7530                caixa: "service-a".into(),
7531                versao: "^0.1".into(),
7532            },
7533            Membro {
7534                caixa: "service-a".into(),
7535                versao: "^0.2".into(),
7536            },
7537            Membro {
7538                caixa: "demo".into(),
7539                versao: "^0.1".into(),
7540            },
7541        ];
7542        let err = layout.verify(&c, &root).unwrap_err();
7543        let LayoutError::AplicacaoViolation { issue, .. } = err else {
7544            panic!("expected AplicacaoViolation, got {err:?}");
7545        };
7546        // The per-membros duplicate diagnostic (from view.validate())
7547        // surfaces ahead of the cross-slot self-membership gate, so the
7548        // `service-a` duplicate is named — not the `demo` self-reference.
7549        assert!(
7550            issue.contains("service-a") && issue.contains("more than once"),
7551            "duplicate-:caixa diagnostic must surface before self-membership gate, \
7552             got {issue:?}"
7553        );
7554    }
7555
7556    #[test]
7557    fn mesh_slots_on_servico_rejected() {
7558        // The canonical real-world footgun: an author adds :entrada to a
7559        // :kind Servico expecting it to expose ingress. aplicacao_view
7560        // returns None for Servico, so the slot is the manifest's
7561        // "ignored otherwise" — never validated, never rendered. The
7562        // kind-coherence gate rejects it at build time (before the
7563        // :servicos existence loop), naming the offending slot + kind.
7564        use crate::Entrada;
7565        let root = PathBuf::from("/tmp/x");
7566        let manifest = root.join("caixa.lisp");
7567        let manifest_clone = manifest.clone();
7568        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7569        let mut c = caixa(CaixaKind::Servico);
7570        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7571        c.entrada = Some(Entrada {
7572            host: "demo.example.com".into(),
7573            para: "demo".into(),
7574            paths: vec![],
7575            port: 8080,
7576        });
7577        let err = layout.verify(&c, &root).unwrap_err();
7578        match err {
7579            LayoutError::MeshSlotsOnNonAplicacao { caixa, kind, slots } => {
7580                assert_eq!(caixa, "demo");
7581                assert_eq!(kind, CaixaKind::Servico);
7582                assert_eq!(slots, crate::render::M3_AUTHOR_KEY_ENTRADA);
7583            }
7584            other => panic!("expected MeshSlotsOnNonAplicacao, got {other:?}"),
7585        }
7586    }
7587
7588    #[test]
7589    fn mesh_slots_on_non_aplicacao_lists_slots_in_canonical_order() {
7590        // All five mesh slots declared on a Biblioteca → the diagnostic
7591        // enumerates them in canonical declaration order, deterministic
7592        // across runs. The gate fires on declared-ness only (the values
7593        // need not be a *valid* AplicacaoSpec — aplicacao_view is never
7594        // called for a non-Aplicacao kind).
7595        use crate::{Entrada, Membro, MeshPolicy, Placement, PlacementStrategy, WitContract};
7596        let root = PathBuf::from("/tmp/x");
7597        let manifest = root.join("caixa.lisp");
7598        let manifest_clone = manifest.clone();
7599        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7600        let mut c = caixa(CaixaKind::Biblioteca);
7601        c.membros = vec![Membro {
7602            caixa: "a".into(),
7603            versao: "^0.1".into(),
7604        }];
7605        c.contratos = vec![WitContract {
7606            de: "a".into(),
7607            para: "a".into(),
7608            wit: "wasi:http/proxy".into(),
7609            endpoint: Some("/x".into()),
7610            subject: None,
7611            slot: None,
7612        }];
7613        c.politicas = Some(MeshPolicy::default());
7614        c.placement = Some(Placement {
7615            estrategia: PlacementStrategy::Replicated,
7616            clusters: vec!["rio".into()],
7617            affinity: None,
7618            shard_key: None,
7619        });
7620        c.entrada = Some(Entrada {
7621            host: "x.example.com".into(),
7622            para: "a".into(),
7623            paths: vec![],
7624            port: 8080,
7625        });
7626        let err = layout.verify(&c, &root).unwrap_err();
7627        match err {
7628            LayoutError::MeshSlotsOnNonAplicacao { slots, .. } => {
7629                assert_eq!(
7630                    slots,
7631                    format!(
7632                        "{} {} {} {} {}",
7633                        crate::render::M3_AUTHOR_KEY_MEMBROS,
7634                        crate::render::M3_AUTHOR_KEY_CONTRATOS,
7635                        crate::render::M3_AUTHOR_KEY_POLITICAS,
7636                        crate::render::M3_AUTHOR_KEY_PLACEMENT,
7637                        crate::render::M3_AUTHOR_KEY_ENTRADA,
7638                    )
7639                );
7640            }
7641            other => panic!("expected MeshSlotsOnNonAplicacao, got {other:?}"),
7642        }
7643    }
7644
7645    #[test]
7646    fn servico_without_mesh_slots_still_verifies() {
7647        // Pass-after control: a well-formed Servico carrying no mesh
7648        // slots must remain accepted — the gate keys off declared-ness,
7649        // so it must not over-fire on the common case.
7650        let root = PathBuf::from("/tmp/x");
7651        let servico = root.join("servicos/demo.computeunit.yaml");
7652        let manifest = root.join("caixa.lisp");
7653        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == servico);
7654        let mut c = caixa(CaixaKind::Servico);
7655        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7656        layout.verify(&c, &root).unwrap();
7657    }
7658
7659    #[test]
7660    fn supervisor_slots_on_servico_rejected() {
7661        // Mirror of `mesh_slots_on_servico_rejected` on the
7662        // supervisor-tree slot set: an author adds `:children` to a
7663        // `:kind Servico` expecting it to spawn workers. supervisor_view
7664        // returns None for Servico, so the slot is the manifest's
7665        // "ignored otherwise" — never validated, never reconciled. The
7666        // kind-coherence gate rejects it at build time (before the
7667        // :servicos existence loop), naming the offending slot + kind.
7668        use crate::{ChildSpec, RestartPolicy};
7669        let root = PathBuf::from("/tmp/x");
7670        let manifest = root.join("caixa.lisp");
7671        let manifest_clone = manifest.clone();
7672        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7673        let mut c = caixa(CaixaKind::Servico);
7674        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7675        c.children = vec![ChildSpec {
7676            caixa: "worker".into(),
7677            versao: "^0.1".into(),
7678            restart: RestartPolicy::Permanent,
7679        }];
7680        let err = layout.verify(&c, &root).unwrap_err();
7681        match err {
7682            LayoutError::SupervisorSlotsOnNonSupervisor { caixa, kind, slots } => {
7683                assert_eq!(caixa, "demo");
7684                assert_eq!(kind, CaixaKind::Servico);
7685                assert_eq!(slots, crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN);
7686            }
7687            other => panic!("expected SupervisorSlotsOnNonSupervisor, got {other:?}"),
7688        }
7689    }
7690
7691    #[test]
7692    fn supervisor_slots_on_non_supervisor_lists_slots_in_canonical_order() {
7693        // All four supervisor slots declared on a Biblioteca → the
7694        // diagnostic enumerates them in canonical declaration order
7695        // (`:estrategia` → `:max-restarts` → `:restart-window` →
7696        // `:children`), deterministic across runs. The gate fires on
7697        // declared-ness only (the values need not be a *valid*
7698        // SupervisorSpec — supervisor_view is never called for a
7699        // non-Supervisor kind). Mirror of
7700        // `mesh_slots_on_non_aplicacao_lists_slots_in_canonical_order`.
7701        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
7702        let root = PathBuf::from("/tmp/x");
7703        let manifest = root.join("caixa.lisp");
7704        let manifest_clone = manifest.clone();
7705        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7706        let mut c = caixa(CaixaKind::Biblioteca);
7707        c.estrategia = Some(RestartStrategy::OneForOne);
7708        c.max_restarts = Some(5);
7709        c.restart_window = Some("60s".into());
7710        c.children = vec![ChildSpec {
7711            caixa: "worker".into(),
7712            versao: "^0.1".into(),
7713            restart: RestartPolicy::Permanent,
7714        }];
7715        let err = layout.verify(&c, &root).unwrap_err();
7716        match err {
7717            LayoutError::SupervisorSlotsOnNonSupervisor { slots, .. } => {
7718                assert_eq!(slots, ":estrategia :max-restarts :restart-window :children");
7719            }
7720            other => panic!("expected SupervisorSlotsOnNonSupervisor, got {other:?}"),
7721        }
7722    }
7723
7724    #[test]
7725    fn aplicacao_with_supervisor_slots_rejected() {
7726        // Cross-kind pin: an Aplicacao (the other no-code orchestrator
7727        // kind) that declares a supervisor slot is rejected by the
7728        // supervisor-slot gate, just as a Supervisor declaring a mesh
7729        // slot is rejected by the mesh-slot gate — the two kind ↔ slot
7730        // coherence gates are symmetric and mutually exclusive. The
7731        // gate fires before the Aplicacao typed-graph validation, so
7732        // the diagnostic names the foreign supervisor slot rather than
7733        // a downstream AplicacaoViolation.
7734        use crate::{Membro, Placement, PlacementStrategy, RestartStrategy};
7735        let root = PathBuf::from("/tmp/x");
7736        let manifest = root.join("caixa.lisp");
7737        let manifest_clone = manifest.clone();
7738        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7739        let mut c = caixa(CaixaKind::Aplicacao);
7740        c.membros = vec![Membro {
7741            caixa: "service-a".into(),
7742            versao: "^0.1".into(),
7743        }];
7744        c.placement = Some(Placement {
7745            estrategia: PlacementStrategy::Replicated,
7746            clusters: vec!["rio".into()],
7747            affinity: None,
7748            shard_key: None,
7749        });
7750        c.estrategia = Some(RestartStrategy::OneForAll);
7751        let err = layout.verify(&c, &root).unwrap_err();
7752        match err {
7753            LayoutError::SupervisorSlotsOnNonSupervisor { caixa, kind, slots } => {
7754                assert_eq!(caixa, "demo");
7755                assert_eq!(kind, CaixaKind::Aplicacao);
7756                assert_eq!(slots, crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA);
7757            }
7758            other => panic!("expected SupervisorSlotsOnNonSupervisor, got {other:?}"),
7759        }
7760    }
7761
7762    #[test]
7763    fn servico_without_supervisor_slots_still_verifies() {
7764        // Pass-after control: a well-formed Servico carrying no
7765        // supervisor slots must remain accepted — the gate keys off
7766        // declared-ness, so it must not over-fire on the common case.
7767        let root = PathBuf::from("/tmp/x");
7768        let servico = root.join("servicos/demo.computeunit.yaml");
7769        let manifest = root.join("caixa.lisp");
7770        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == servico);
7771        let mut c = caixa(CaixaKind::Servico);
7772        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7773        layout.verify(&c, &root).unwrap();
7774    }
7775
7776    #[test]
7777    fn servico_slots_on_biblioteca_rejected() {
7778        // Mirror of `mesh_slots_on_servico_rejected` /
7779        // `supervisor_slots_on_servico_rejected` on the M2
7780        // Servico-runtime slot set: an author adds `:limits` to a
7781        // `:kind Biblioteca` expecting per-process sandboxing. The
7782        // caixa-helm / caixa-flux renderers gate on `require_kind(_,
7783        // Servico)`, so the slot is the manifest's "ignored otherwise" —
7784        // never rendered into any artifact. The kind-coherence gate
7785        // rejects it at build time (before the M2 validate blocks),
7786        // naming the offending slot + kind.
7787        use crate::LimitsSpec;
7788        let root = PathBuf::from("/tmp/x");
7789        let manifest = root.join("caixa.lisp");
7790        let lib = root.join("lib").join("demo.lisp");
7791        let manifest_clone = manifest.clone();
7792        let layout =
7793            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == lib);
7794        let mut c = caixa(CaixaKind::Biblioteca);
7795        c.limits = Some(LimitsSpec {
7796            fuel: Some(1_000_000),
7797            ..Default::default()
7798        });
7799        let err = layout.verify(&c, &root).unwrap_err();
7800        match err {
7801            LayoutError::ServicoSlotsOnNonServico { caixa, kind, slots } => {
7802                assert_eq!(caixa, "demo");
7803                assert_eq!(kind, CaixaKind::Biblioteca);
7804                assert_eq!(slots, crate::render::M2_AUTHOR_KEY_LIMITS);
7805            }
7806            other => panic!("expected ServicoSlotsOnNonServico, got {other:?}"),
7807        }
7808    }
7809
7810    #[test]
7811    fn servico_slots_on_non_servico_lists_slots_in_canonical_order() {
7812        // All three M2 slots declared on a Biblioteca → the diagnostic
7813        // enumerates them in canonical declaration order (`:limits` →
7814        // `:behavior` → `:upgrade-from`), deterministic across runs. The
7815        // gate fires on declared-ness only (the values need not pass the
7816        // M2 validate blocks — those run only after the kind-coherence
7817        // gate, and never for a non-Servico declared-slot caixa). Mirror
7818        // of the mesh/supervisor `*_lists_slots_in_canonical_order` pins.
7819        use crate::{BehaviorSpec, LimitsSpec, UpgradeFromEntry, UpgradeInstruction};
7820        let root = PathBuf::from("/tmp/x");
7821        let manifest = root.join("caixa.lisp");
7822        let manifest_clone = manifest.clone();
7823        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7824        let mut c = caixa(CaixaKind::Biblioteca);
7825        c.limits = Some(LimitsSpec {
7826            fuel: Some(1_000_000),
7827            ..Default::default()
7828        });
7829        c.behavior = Some(BehaviorSpec {
7830            on_init: Some(PathBuf::from("lib/init.lisp")),
7831            ..Default::default()
7832        });
7833        c.upgrade_from = vec![UpgradeFromEntry {
7834            from: "0.1.0".into(),
7835            instructions: vec![UpgradeInstruction::Restart],
7836        }];
7837        let err = layout.verify(&c, &root).unwrap_err();
7838        match err {
7839            LayoutError::ServicoSlotsOnNonServico { slots, .. } => {
7840                assert_eq!(
7841                    slots,
7842                    format!(
7843                        "{} {} {}",
7844                        crate::render::M2_AUTHOR_KEY_LIMITS,
7845                        crate::render::M2_AUTHOR_KEY_BEHAVIOR,
7846                        crate::render::M2_AUTHOR_KEY_UPGRADE_FROM,
7847                    )
7848                );
7849            }
7850            other => panic!("expected ServicoSlotsOnNonServico, got {other:?}"),
7851        }
7852    }
7853
7854    #[test]
7855    fn aplicacao_with_servico_slots_rejected() {
7856        // Cross-kind pin (mirror of `aplicacao_with_supervisor_slots_rejected`):
7857        // an Aplicacao that declares an M2 Servico-runtime slot is
7858        // rejected by the Servico-slot gate, just as a Supervisor
7859        // declaring a mesh slot is rejected by the mesh-slot gate — the
7860        // three kind ↔ slot coherence gates are symmetric and mutually
7861        // exclusive. The gate fires before the Aplicacao typed-graph
7862        // validation, so the diagnostic names the foreign M2 slot rather
7863        // than a downstream AplicacaoViolation about missing :membros.
7864        use crate::{Membro, Placement, PlacementStrategy, UpgradeFromEntry, UpgradeInstruction};
7865        let root = PathBuf::from("/tmp/x");
7866        let manifest = root.join("caixa.lisp");
7867        let manifest_clone = manifest.clone();
7868        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7869        let mut c = caixa(CaixaKind::Aplicacao);
7870        c.membros = vec![Membro {
7871            caixa: "service-a".into(),
7872            versao: "^0.1".into(),
7873        }];
7874        c.placement = Some(Placement {
7875            estrategia: PlacementStrategy::Replicated,
7876            clusters: vec!["rio".into()],
7877            affinity: None,
7878            shard_key: None,
7879        });
7880        c.upgrade_from = vec![UpgradeFromEntry {
7881            from: "0.1.0".into(),
7882            instructions: vec![UpgradeInstruction::Restart],
7883        }];
7884        let err = layout.verify(&c, &root).unwrap_err();
7885        match err {
7886            LayoutError::ServicoSlotsOnNonServico { caixa, kind, slots } => {
7887                assert_eq!(caixa, "demo");
7888                assert_eq!(kind, CaixaKind::Aplicacao);
7889                assert_eq!(slots, crate::render::M2_AUTHOR_KEY_UPGRADE_FROM);
7890            }
7891            other => panic!("expected ServicoSlotsOnNonServico, got {other:?}"),
7892        }
7893    }
7894
7895    #[test]
7896    fn servico_with_servico_slots_still_verifies() {
7897        // Pass-after control: a well-formed Servico carrying all three M2
7898        // slots must remain accepted — the gate is guarded by `kind !=
7899        // Servico`, so it must not over-fire on the kind these slots
7900        // exist for. Mirror of `servico_without_{mesh,supervisor}_slots_
7901        // still_verifies` on the legitimate-declaration axis.
7902        use crate::{BehaviorSpec, LimitsSpec, UpgradeFromEntry, UpgradeInstruction};
7903        let root = PathBuf::from("/tmp/x");
7904        let manifest = root.join("caixa.lisp");
7905        let svc = root.join("servicos/demo.computeunit.yaml");
7906        let init = root.join("lib/init.lisp");
7907        let layout =
7908            StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc || p == init);
7909        let mut c = caixa(CaixaKind::Servico);
7910        c.versao = "0.2.0".into();
7911        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7912        c.limits = Some(LimitsSpec {
7913            fuel: Some(1_000_000),
7914            ..Default::default()
7915        });
7916        c.behavior = Some(BehaviorSpec {
7917            on_init: Some(PathBuf::from("lib/init.lisp")),
7918            ..Default::default()
7919        });
7920        c.upgrade_from = vec![UpgradeFromEntry {
7921            from: "0.1.0".into(),
7922            instructions: vec![UpgradeInstruction::Restart],
7923        }];
7924        layout.verify(&c, &root).unwrap();
7925    }
7926
7927    #[test]
7928    fn aplicacao_must_not_have_bibliotecas() {
7929        use crate::{Membro, Placement, PlacementStrategy};
7930        let root = PathBuf::from("/tmp/x");
7931        let manifest = root.join("caixa.lisp");
7932        let manifest_clone = manifest.clone();
7933        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7934        let mut c = caixa(CaixaKind::Aplicacao);
7935        c.bibliotecas = vec!["lib/code.lisp".into()];
7936        c.membros = vec![Membro {
7937            caixa: "x".into(),
7938            versao: "^0.1".into(),
7939        }];
7940        c.placement = Some(Placement {
7941            estrategia: PlacementStrategy::Replicated,
7942            clusters: vec!["rio".into()],
7943            affinity: None,
7944            shard_key: None,
7945        });
7946        let err = layout.verify(&c, &root).unwrap_err();
7947        assert!(matches!(err, LayoutError::AplicacaoOwnsCode(_)));
7948    }
7949
7950    #[test]
7951    fn acao_must_not_have_bibliotecas() {
7952        // Mirror of `supervisor_must_not_have_bibliotecas` /
7953        // `aplicacao_must_not_have_bibliotecas` on the third no-code
7954        // kind. `has_code` fires before the `:ci`-presence gates below
7955        // it, so this must surface `AcaoOwnsCode` even though the
7956        // caixa also lacks a `:ci` slot (which would otherwise surface
7957        // as `MissingCi`) — the more-fundamental "this kind runs no
7958        // code at all" diagnostic wins.
7959        let root = PathBuf::from("/tmp/x");
7960        let manifest = root.join("caixa.lisp");
7961        let manifest_clone = manifest.clone();
7962        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7963        let mut c = caixa(CaixaKind::Acao);
7964        c.bibliotecas = vec!["lib/code.lisp".into()];
7965        let err = layout.verify(&c, &root).unwrap_err();
7966        assert!(matches!(err, LayoutError::AcaoOwnsCode(_)));
7967    }
7968
7969    #[test]
7970    fn acao_without_ci_errors() {
7971        // Mirror of `binario_without_exe_errors` on the fifth required-
7972        // slot axis.
7973        let root = PathBuf::from("/tmp/x");
7974        let manifest = root.join("caixa.lisp");
7975        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
7976        let err = layout.verify(&caixa(CaixaKind::Acao), &root).unwrap_err();
7977        assert!(matches!(err, LayoutError::MissingCi(_)));
7978    }
7979
7980    #[test]
7981    fn acao_with_ci_passes() {
7982        let root = PathBuf::from("/tmp/x");
7983        let manifest = root.join("caixa.lisp");
7984        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
7985        let mut c = caixa(CaixaKind::Acao);
7986        c.ci = Some(canteiro_types::CiRun {
7987            workspace: "pleme-io".into(),
7988            repo: "caixa".into(),
7989            nodes: vec![],
7990        });
7991        layout
7992            .verify(&c, &root)
7993            .expect("an Acao caixa with a declared :ci slot passes layout verify");
7994    }
7995
7996    #[test]
7997    fn acao_with_cyclic_ci_rejected_at_layout() {
7998        // Layout-side wire-up pin on the compound
7999        // [`crate::Caixa::validate_acao_shape`] gate: a `:kind Acao`
8000        // caixa declaring a structurally illegal `:ci` (here — a
8001        // minimal two-node cycle `a → b → a`, one of the three
8002        // `canteiro_types::DecomposeError` arms
8003        // [`crate::decompose_ci`] refuses) surfaces
8004        // [`LayoutError::AcaoViolation`] at `feira build` time rather
8005        // than passing the layout gate silently and deferring the
8006        // diagnostic to [`caixa_actions::validate`] at renderer time.
8007        //
8008        // Pre-lift the layout pipeline only checked `:ci` *presence*
8009        // via [`LayoutError::MissingCi`]; the decompose gate lived
8010        // only wired open-coded at
8011        // [`caixa_actions::validate`] via the substrate-canonical
8012        // [`crate::require_acao_view`] compound helper. This wire-up
8013        // pin locks the new layout-side compound-shape gate in place
8014        // — a future regression that dropped the `if
8015        // caixa.kind().is_acao() { validate_acao_shape() }` block or
8016        // relaxed the diagnostic surface trips here at caixa-core
8017        // build time. Sibling in shape to the peer
8018        // [`aplicacao_must_not_have_bibliotecas`] /
8019        // [`supervisor_must_not_have_bibliotecas`] /
8020        // [`acao_must_not_have_bibliotecas`] layout wire-up pins on
8021        // the sibling per-kind shape gates.
8022        let root = PathBuf::from("/tmp/x");
8023        let manifest = root.join("caixa.lisp");
8024        let manifest_clone = manifest.clone();
8025        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8026        let mut c = caixa(CaixaKind::Acao);
8027        c.ci = Some(canteiro_types::CiRun {
8028            workspace: "pleme-io".into(),
8029            repo: "caixa".into(),
8030            nodes: vec![
8031                canteiro_types::CiNode::new(
8032                    "a",
8033                    canteiro_types::EnvClass::None,
8034                    canteiro_types::ActionRef {
8035                        name: "a".into(),
8036                        command: "true".into(),
8037                        args: vec![],
8038                    },
8039                    vec!["b".into()],
8040                ),
8041                canteiro_types::CiNode::new(
8042                    "b",
8043                    canteiro_types::EnvClass::None,
8044                    canteiro_types::ActionRef {
8045                        name: "b".into(),
8046                        command: "true".into(),
8047                        args: vec![],
8048                    },
8049                    vec!["a".into()],
8050                ),
8051            ],
8052        });
8053        let err = layout.verify(&c, &root).unwrap_err();
8054        match err {
8055            LayoutError::AcaoViolation { caixa, issue } => {
8056                assert_eq!(caixa, "demo");
8057                assert!(
8058                    issue.contains("decompose"),
8059                    "AcaoViolation issue must name the decompose axis (got: {issue:?})",
8060                );
8061                assert!(
8062                    issue.contains("demo"),
8063                    "AcaoViolation issue must name the offending caixa nome via the folded \
8064                     CiDecomposeFailure Display (got: {issue:?})",
8065                );
8066            }
8067            other => panic!("expected AcaoViolation on a cyclic :ci, got {other:?}"),
8068        }
8069    }
8070
8071    #[test]
8072    fn ci_on_non_acao_errors() {
8073        // Mirror of `mesh_slots_on_non_aplicacao_lists_slots_in_canonical_order`
8074        // on the Acao-only `:ci` axis — declaring `:ci` on any other
8075        // kind is the same "silently ignored" footgun the sibling
8076        // mesh-/supervisor-/servico-slot gates already close.
8077        let root = PathBuf::from("/tmp/x");
8078        let manifest = root.join("caixa.lisp");
8079        let manifest_clone = manifest.clone();
8080        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8081        let mut c = caixa(CaixaKind::Biblioteca);
8082        c.ci = Some(canteiro_types::CiRun {
8083            workspace: "pleme-io".into(),
8084            repo: "caixa".into(),
8085            nodes: vec![],
8086        });
8087        let err = layout.verify(&c, &root).unwrap_err();
8088        match err {
8089            LayoutError::CiOnNonAcao { caixa, kind } => {
8090                assert_eq!(caixa, "demo");
8091                assert_eq!(kind, CaixaKind::Biblioteca);
8092            }
8093            other => panic!("expected CiOnNonAcao, got {other:?}"),
8094        }
8095    }
8096
8097    // ── ForeignCodeSlot — kind ↔ code-surface coherence ────────────────
8098
8099    #[test]
8100    fn biblioteca_with_exe_rejected() {
8101        // Fail-before-pass-after pin: a `:kind Biblioteca` declaring
8102        // `:exe` is the "I added a CLI to my library" footgun — the nix
8103        // flake renderer for Binario gates on `require_kind(_, Binario)`,
8104        // so on a Biblioteca the `:exe` path is silently dropped past
8105        // the layout's path-existence check (no executable target is
8106        // ever generated). The diagnostic names the offending kind +
8107        // slot verbatim so the author can grep their caixa.lisp for
8108        // `:exe` and fix in one edit (drop the slot or change
8109        // `:kind Biblioteca` → `:kind Binario`).
8110        let root = PathBuf::from("/tmp/x");
8111        let manifest = root.join("caixa.lisp");
8112        let lib = root.join("lib").join("demo.lisp");
8113        let exe_path = root.join("exe").join("tool");
8114        let layout = StandardLayout::new()
8115            .with_path_exists(move |p| p == manifest || p == lib || p == exe_path);
8116        let mut c = caixa(CaixaKind::Biblioteca);
8117        c.exe = vec!["exe/tool".into()];
8118        let err = layout.verify(&c, &root).unwrap_err();
8119        match err {
8120            LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
8121                assert_eq!(caixa, "demo");
8122                assert_eq!(kind, CaixaKind::Biblioteca);
8123                assert_eq!(slots, ":exe");
8124            }
8125            other => panic!("expected ForeignCodeSlot, got {other:?}"),
8126        }
8127    }
8128
8129    #[test]
8130    fn biblioteca_with_servicos_rejected() {
8131        // Symmetric to `biblioteca_with_exe_rejected` on the
8132        // `:servicos` axis: a `:kind Biblioteca` declaring a Servico
8133        // computeunit silently passed validate and the daemon's
8134        // ComputeUnit / lareira chart never materialized (caixa-helm /
8135        // caixa-flux gate emission on `require_kind(_, Servico)`).
8136        let root = PathBuf::from("/tmp/x");
8137        let manifest = root.join("caixa.lisp");
8138        let lib = root.join("lib").join("demo.lisp");
8139        let svc = root.join("servicos").join("demo.computeunit.yaml");
8140        let layout =
8141            StandardLayout::new().with_path_exists(move |p| p == manifest || p == lib || p == svc);
8142        let mut c = caixa(CaixaKind::Biblioteca);
8143        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8144        let err = layout.verify(&c, &root).unwrap_err();
8145        match err {
8146            LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
8147                assert_eq!(caixa, "demo");
8148                assert_eq!(kind, CaixaKind::Biblioteca);
8149                assert_eq!(slots, ":servicos");
8150            }
8151            other => panic!("expected ForeignCodeSlot, got {other:?}"),
8152        }
8153    }
8154
8155    #[test]
8156    fn biblioteca_with_exe_and_servicos_lists_slots_in_canonical_order() {
8157        // Both foreign code slots declared on a Biblioteca → the
8158        // diagnostic enumerates them in canonical declaration order
8159        // (`:exe` → `:servicos`), deterministic across runs. Mirrors the
8160        // mesh/supervisor/servico-slot `*_lists_slots_in_canonical_order`
8161        // pins on the peer kind ↔ slot algebra axes; drift in the
8162        // [`Caixa::declared_foreign_code_slots`] iteration order surfaces
8163        // here.
8164        let root = PathBuf::from("/tmp/x");
8165        let manifest = root.join("caixa.lisp");
8166        let manifest_clone = manifest.clone();
8167        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8168        let mut c = caixa(CaixaKind::Biblioteca);
8169        c.exe = vec!["exe/tool".into()];
8170        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8171        let err = layout.verify(&c, &root).unwrap_err();
8172        match err {
8173            LayoutError::ForeignCodeSlot { slots, .. } => {
8174                assert_eq!(slots, ":exe :servicos");
8175            }
8176            other => panic!("expected ForeignCodeSlot, got {other:?}"),
8177        }
8178    }
8179
8180    #[test]
8181    fn binario_with_servicos_rejected() {
8182        // The peer footgun on the Binario kind: declaring a Servico
8183        // computeunit on a `:kind Binario` caixa. The caixa-helm /
8184        // caixa-flux renderers gate on `require_kind(_, Servico)`, so
8185        // the `:servicos` slot vanishes past the layout's path-
8186        // existence check — no ComputeUnit, no Helm chart. `:exe` stays
8187        // valid (Binario's native code surface), so the kind-coherence
8188        // diagnostic targets only `:servicos`.
8189        let root = PathBuf::from("/tmp/x");
8190        let manifest = root.join("caixa.lisp");
8191        let exe_path = root.join("exe").join("tool");
8192        let svc = root.join("servicos").join("demo.computeunit.yaml");
8193        let layout = StandardLayout::new()
8194            .with_path_exists(move |p| p == manifest || p == exe_path || p == svc);
8195        let mut c = caixa(CaixaKind::Binario);
8196        c.exe = vec!["exe/tool".into()];
8197        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8198        let err = layout.verify(&c, &root).unwrap_err();
8199        match err {
8200            LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
8201                assert_eq!(caixa, "demo");
8202                assert_eq!(kind, CaixaKind::Binario);
8203                assert_eq!(slots, ":servicos");
8204            }
8205            other => panic!("expected ForeignCodeSlot, got {other:?}"),
8206        }
8207    }
8208
8209    #[test]
8210    fn servico_with_exe_rejected() {
8211        // Symmetric to `binario_with_servicos_rejected` on the other
8212        // code-running peer: a `:kind Servico` declaring an `:exe` is
8213        // the "I added a host-side CLI to my wasm component" footgun —
8214        // the nix flake's Binario target gates on `require_kind(_,
8215        // Binario)`, so the `:exe` path vanishes past the layout's
8216        // path-existence check.
8217        let root = PathBuf::from("/tmp/x");
8218        let manifest = root.join("caixa.lisp");
8219        let svc = root.join("servicos").join("demo.computeunit.yaml");
8220        let exe_path = root.join("exe").join("tool");
8221        let layout = StandardLayout::new()
8222            .with_path_exists(move |p| p == manifest || p == svc || p == exe_path);
8223        let mut c = caixa(CaixaKind::Servico);
8224        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8225        c.exe = vec!["exe/tool".into()];
8226        let err = layout.verify(&c, &root).unwrap_err();
8227        match err {
8228            LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
8229                assert_eq!(caixa, "demo");
8230                assert_eq!(kind, CaixaKind::Servico);
8231                assert_eq!(slots, ":exe");
8232            }
8233            other => panic!("expected ForeignCodeSlot, got {other:?}"),
8234        }
8235    }
8236
8237    #[test]
8238    fn binario_without_servicos_still_verifies() {
8239        // Pass-after control: a well-formed Binario carrying only its
8240        // native `:exe` surface must remain accepted — the gate keys off
8241        // declared-ness of the *foreign* slots, so it must not over-fire
8242        // on the legitimate same-kind case. Mirror of
8243        // `servico_with_servico_slots_still_verifies` on the peer axis.
8244        let root = PathBuf::from("/tmp/x");
8245        let manifest = root.join("caixa.lisp");
8246        let exe_path = root.join("exe").join("tool");
8247        let layout =
8248            StandardLayout::new().with_path_exists(move |p| p == manifest || p == exe_path);
8249        let mut c = caixa(CaixaKind::Binario);
8250        c.exe = vec!["exe/tool".into()];
8251        layout.verify(&c, &root).unwrap();
8252    }
8253
8254    #[test]
8255    fn biblioteca_with_only_bibliotecas_still_verifies() {
8256        // Pass-after control: a well-formed Biblioteca carrying only
8257        // its native `:bibliotecas` surface (or the default
8258        // `lib/<nome>.lisp`) must remain accepted. The gate keys off
8259        // declared-ness of `:exe` + `:servicos` only — `:bibliotecas`
8260        // is deliberately excluded from the foreign-set on every
8261        // code-running kind (`declared_foreign_code_slots` doc), so a
8262        // Biblioteca with the canonical lib surface alone passes.
8263        let root = PathBuf::from("/tmp/x");
8264        let manifest = root.join("caixa.lisp");
8265        let lib = root.join("lib").join("demo.lisp");
8266        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == lib);
8267        layout
8268            .verify(&caixa(CaixaKind::Biblioteca), &root)
8269            .expect("Biblioteca with default lib must verify");
8270    }
8271
8272    #[test]
8273    fn binario_with_bibliotecas_helper_still_verifies() {
8274        // Pass-after control on the deliberate `:bibliotecas`-as-helper
8275        // shape: a `:kind Binario` may legitimately bundle a `lib/`
8276        // helper its nix flake build consumes (the same shape a
8277        // `:kind Servico` may bundle for its wasm-component source).
8278        // The foreign-code-slot gate must NOT fire on `:bibliotecas` for
8279        // either code-running kind; pinned here so a future tightening
8280        // that adds `:bibliotecas` to the foreign set on Binario /
8281        // Servico surfaces as a test failure rather than as a silent
8282        // over-reach.
8283        let root = PathBuf::from("/tmp/x");
8284        let manifest = root.join("caixa.lisp");
8285        let exe_path = root.join("exe").join("tool");
8286        let lib = root.join("lib").join("helper.lisp");
8287        let layout = StandardLayout::new()
8288            .with_path_exists(move |p| p == manifest || p == exe_path || p == lib);
8289        let mut c = caixa(CaixaKind::Binario);
8290        c.exe = vec!["exe/tool".into()];
8291        c.bibliotecas = vec!["lib/helper.lisp".into()];
8292        layout.verify(&c, &root).unwrap();
8293    }
8294
8295    #[test]
8296    fn supervisor_with_exe_still_surfaces_owns_code() {
8297        // Diagnostic-precedence pin: a `:kind Supervisor` declaring
8298        // `:exe` is *both* "Supervisor with code" and "foreign code
8299        // slot". The more-fundamental `SupervisorOwnsCode` must win
8300        // (Supervisor doesn't run code at all — the foreign-slot
8301        // diagnostic would mislead the author toward changing `:kind`
8302        // when the underlying defect is that supervisors orchestrate
8303        // children, not code). Guards the call order in `verify`
8304        // against silent reordering.
8305        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
8306        let root = PathBuf::from("/tmp/x");
8307        let manifest = root.join("caixa.lisp");
8308        let manifest_clone = manifest.clone();
8309        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8310        let mut c = caixa(CaixaKind::Supervisor);
8311        c.estrategia = Some(RestartStrategy::OneForOne);
8312        c.max_restarts = Some(5);
8313        c.exe = vec!["exe/tool".into()];
8314        c.children = vec![ChildSpec {
8315            caixa: "worker".into(),
8316            versao: "^0.1".into(),
8317            restart: RestartPolicy::Permanent,
8318        }];
8319        let err = layout.verify(&c, &root).unwrap_err();
8320        assert!(
8321            matches!(err, LayoutError::SupervisorOwnsCode(_)),
8322            "Supervisor-with-:exe must surface as SupervisorOwnsCode (the more-fundamental \
8323             no-code-at-all diagnostic), got {err:?}"
8324        );
8325    }
8326
8327    #[test]
8328    fn declared_foreign_code_slots_returns_canonical_order() {
8329        // Unit-level pin for the lifted method: the canonical iteration
8330        // order is `:exe` → `:servicos`, independent of which subset is
8331        // populated. Empty input + each single-slot subset + the full
8332        // pair are all checked so a future axis added to the method
8333        // (a hypothetical fifth code-surface slot) is one extension
8334        // point + one assertion update here, not a coordinated rewrite
8335        // across the layout-test sites that reach for the canonical
8336        // order.
8337        let mut c = caixa(CaixaKind::Biblioteca);
8338        assert!(c.declared_foreign_code_slots().is_empty());
8339        c.exe = vec!["exe/tool".into()];
8340        assert_eq!(c.declared_foreign_code_slots(), vec![":exe"]);
8341        c.exe.clear();
8342        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8343        assert_eq!(c.declared_foreign_code_slots(), vec![":servicos"]);
8344        c.exe = vec!["exe/tool".into()];
8345        assert_eq!(c.declared_foreign_code_slots(), vec![":exe", ":servicos"]);
8346    }
8347
8348    #[test]
8349    fn aplicacao_with_unknown_contrato_member_fails() {
8350        use crate::{Membro, Placement, PlacementStrategy, WitContract};
8351        let root = PathBuf::from("/tmp/x");
8352        let manifest = root.join("caixa.lisp");
8353        let manifest_clone = manifest.clone();
8354        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8355        let mut c = caixa(CaixaKind::Aplicacao);
8356        c.membros = vec![Membro {
8357            caixa: "service-a".into(),
8358            versao: "^0.1".into(),
8359        }];
8360        c.contratos = vec![WitContract {
8361            de: "service-a".into(),
8362            para: "phantom".into(),
8363            wit: "wasi:http/proxy".into(),
8364            endpoint: Some("/x".into()),
8365            subject: None,
8366            slot: None,
8367        }];
8368        c.placement = Some(Placement {
8369            estrategia: PlacementStrategy::Replicated,
8370            clusters: vec!["rio".into()],
8371            affinity: None,
8372            shard_key: None,
8373        });
8374        let err = layout.verify(&c, &root).unwrap_err();
8375        assert!(matches!(err, LayoutError::AplicacaoViolation { .. }));
8376    }
8377
8378    #[test]
8379    fn limits_zero_axis_surfaces_as_layout_violation() {
8380        use crate::LimitsSpec;
8381        let root = PathBuf::from("/tmp/x");
8382        let manifest = root.join("caixa.lisp");
8383        let svc = root.join("servicos/demo.computeunit.yaml");
8384        let mut c = caixa(CaixaKind::Servico);
8385        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8386        c.limits = Some(LimitsSpec {
8387            fuel: Some(0),
8388            ..Default::default()
8389        });
8390        let manifest_clone = manifest.clone();
8391        let svc_clone = svc.clone();
8392        let layout =
8393            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8394        let err = layout.verify(&c, &root).unwrap_err();
8395        let LayoutError::LimitsViolation { caixa, issue } = err else {
8396            panic!("expected LimitsViolation, got {err:?}");
8397        };
8398        assert_eq!(caixa, "demo");
8399        assert!(issue.contains(":fuel"), "issue must name the axis: {issue}");
8400    }
8401
8402    #[test]
8403    fn limits_well_formed_passes_layout() {
8404        use crate::LimitsSpec;
8405        use std::time::Duration;
8406        let root = PathBuf::from("/tmp/x");
8407        let manifest = root.join("caixa.lisp");
8408        let svc = root.join("servicos/demo.computeunit.yaml");
8409        let mut c = caixa(CaixaKind::Servico);
8410        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8411        c.limits = Some(LimitsSpec {
8412            memory: Some(64 * 1024 * 1024),
8413            fuel: Some(1_000_000),
8414            wall_clock: Some(Duration::from_secs(30)),
8415            cpu: Some(500),
8416        });
8417        let manifest_clone = manifest.clone();
8418        let svc_clone = svc.clone();
8419        let layout =
8420            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8421        layout.verify(&c, &root).unwrap();
8422    }
8423
8424    #[test]
8425    fn supervisor_with_valid_children_passes() {
8426        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
8427        let root = PathBuf::from("/tmp/x");
8428        let manifest = root.join("caixa.lisp");
8429        let manifest_clone = manifest.clone();
8430        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8431        let mut c = caixa(CaixaKind::Supervisor);
8432        c.estrategia = Some(RestartStrategy::OneForOne);
8433        c.max_restarts = Some(5);
8434        c.children = vec![
8435            ChildSpec {
8436                caixa: "worker".into(),
8437                versao: "^0.1".into(),
8438                restart: RestartPolicy::Permanent,
8439            },
8440            ChildSpec {
8441                caixa: "cache".into(),
8442                versao: "^0.1".into(),
8443                restart: RestartPolicy::Transient,
8444            },
8445        ];
8446        layout.verify(&c, &root).unwrap();
8447    }
8448
8449    // ── :upgrade-from entry validation pipes through layout ─────────────
8450
8451    #[test]
8452    fn upgrade_invalid_module_surfaces_as_layout_violation() {
8453        // End-to-end pin that
8454        // [`crate::UpgradeFromEntry::validate`] runs *inside*
8455        // `LayoutInvariants::verify` and surfaces value-shape
8456        // violations through the new `UpgradeViolation` arm
8457        // (parallel to `BehaviorViolation`, `LimitsViolation`,
8458        // `SupervisorViolation`, `AplicacaoViolation`). Until this
8459        // wiring landed the entry validator was unreachable from any
8460        // build-pipeline caller — an `:upgrade-from
8461        // ((:from "0.1.0" :instructions ((:load-module "Hello")))` (uppercase
8462        // module name the K8s apiserver would reject on the per-
8463        // ComputeUnit `metadata.name` axis) silently passed
8464        // `feira lint` / `feira build` and surfaced only at wasm-engine
8465        // hot-upgrade time as a per-backend "module not found" /
8466        // `code:load_module/1` `badarg` runtime error, far from the
8467        // source caixa.lisp. Pinning the wiring here so a future
8468        // refactor that drops the `entry.validate()` call surfaces as
8469        // a build-pipeline regression at this test, not as a runtime
8470        // surprise per consumer.
8471        use crate::{UpgradeFromEntry, UpgradeInstruction};
8472        use std::path::PathBuf;
8473        let root = PathBuf::from("/tmp/x");
8474        let manifest = root.join("caixa.lisp");
8475        let svc = root.join("servicos/demo.computeunit.yaml");
8476        let mut c = caixa(CaixaKind::Servico);
8477        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8478        c.upgrade_from = vec![UpgradeFromEntry {
8479            from: "0.1.0".into(),
8480            instructions: vec![UpgradeInstruction::LoadModule {
8481                module: "Hello".into(), // uppercase — not DNS-1123
8482            }],
8483        }];
8484        let manifest_clone = manifest.clone();
8485        let svc_clone = svc.clone();
8486        let layout =
8487            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8488        let err = layout.verify(&c, &root).unwrap_err();
8489        let LayoutError::UpgradeViolation { caixa, issue } = err else {
8490            panic!("expected UpgradeViolation, got {err:?}");
8491        };
8492        assert_eq!(caixa, "demo");
8493        assert!(
8494            issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_LOAD_MODULE),
8495            "issue must name the lisp-form of the offending instruction: {issue}"
8496        );
8497        assert!(
8498            issue.contains("Hello"),
8499            "issue must name the offending :module verbatim: {issue}"
8500        );
8501    }
8502
8503    #[test]
8504    fn upgrade_empty_module_surfaces_as_layout_violation() {
8505        // Companion to the DNS-1123 footgun above on the narrower
8506        // empty arm. Every Module-bearing variant's empty value
8507        // reaches the layout pipeline through the kind-tagged
8508        // `ModuleEmpty` diagnostic naming its lisp-form.
8509        use crate::{UpgradeFromEntry, UpgradeInstruction};
8510        use std::path::PathBuf;
8511        let root = PathBuf::from("/tmp/x");
8512        let manifest = root.join("caixa.lisp");
8513        let svc = root.join("servicos/demo.computeunit.yaml");
8514        let mut c = caixa(CaixaKind::Servico);
8515        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8516        c.upgrade_from = vec![UpgradeFromEntry {
8517            from: "0.1.0".into(),
8518            instructions: vec![UpgradeInstruction::SoftPurge {
8519                module: String::new(),
8520            }],
8521        }];
8522        let manifest_clone = manifest.clone();
8523        let svc_clone = svc.clone();
8524        let layout =
8525            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8526        let err = layout.verify(&c, &root).unwrap_err();
8527        let LayoutError::UpgradeViolation { caixa, issue } = err else {
8528            panic!("expected UpgradeViolation, got {err:?}");
8529        };
8530        assert_eq!(caixa, "demo");
8531        assert!(
8532            issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_SOFT_PURGE),
8533            "issue must name the lisp-form of the empty instruction: {issue}"
8534        );
8535    }
8536
8537    #[test]
8538    fn upgrade_invalid_state_change_script_surfaces_as_layout_violation() {
8539        // Pins that the b0c8389 script value-shape gates
8540        // (AbsoluteScript / ParentEscapeScript) — previously
8541        // unreachable from any build-pipeline caller — now fire
8542        // through the same `UpgradeViolation` arm before the path-
8543        // existence pass would otherwise emit the less-helpful
8544        // "missing upgrade-script" (or, worse, *succeed* against
8545        // /etc/passwd, proving the sandbox bypass — same defect
8546        // the b0c8389 BehaviorSpec wiring closed on the peer M2
8547        // slot).
8548        use crate::{UpgradeFromEntry, UpgradeInstruction};
8549        use std::path::PathBuf;
8550        let root = PathBuf::from("/tmp/x");
8551        let manifest = root.join("caixa.lisp");
8552        let svc = root.join("servicos/demo.computeunit.yaml");
8553        let etc_passwd = PathBuf::from("/etc/passwd");
8554        let mut c = caixa(CaixaKind::Servico);
8555        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8556        c.upgrade_from = vec![UpgradeFromEntry {
8557            from: "0.1.0".into(),
8558            instructions: vec![UpgradeInstruction::StateChange {
8559                script: PathBuf::from("/etc/passwd"),
8560            }],
8561        }];
8562        let manifest_clone = manifest.clone();
8563        let svc_clone = svc.clone();
8564        let etc_passwd_clone = etc_passwd.clone();
8565        // Critically: /etc/passwd "exists" in our mock — without the
8566        // value-shape pre-check, the existence loop would *succeed*
8567        // and the path-traversal exit from the project sandbox would
8568        // pass `feira build` silently.
8569        let layout = StandardLayout::new().with_path_exists(move |p| {
8570            p == manifest_clone || p == svc_clone || p == etc_passwd_clone
8571        });
8572        let err = layout.verify(&c, &root).unwrap_err();
8573        let LayoutError::UpgradeViolation { caixa, issue } = err else {
8574            panic!("expected UpgradeViolation, got {err:?}");
8575        };
8576        assert_eq!(caixa, "demo");
8577        assert!(
8578            issue.contains("absolute") || issue.contains("Absolute"),
8579            "issue must name the violation kind (absolute): {issue}"
8580        );
8581    }
8582
8583    #[test]
8584    fn upgrade_well_formed_passes_layout() {
8585        // Positive control — every documented authoring shape
8586        // (`:load-module`, `:state-change` with a relative path,
8587        // `:soft-purge`, `:purge`, sole `:restart`) passes the wired
8588        // gate. The typed sequence (`:load-module` → `:state-change`
8589        // → `:soft-purge` → `:purge`) lives in one entry; the sole
8590        // `:restart` fallback lives in a *separate* entry on a
8591        // different `:from` (the within-entry restart-exclusivity
8592        // gate added in this commit rejects mixing the fallback with
8593        // the typed sequence — per the UpgradeInstruction::Restart
8594        // doc, `:restart` is terminal and any other instructions in
8595        // the same entry are dead code). Drift here = a future
8596        // tighten that rejects any canonical shape surfaces as a
8597        // regression at this layout-level pin, not piecemeal across
8598        // per-renderer call sites.
8599        //
8600        // `:soft-purge` and `:purge` target *distinct* old-version
8601        // modules (`hello-rio-old` and `hello-rio-oldest`) so the
8602        // within-entry cleanup-singularity gate
8603        // (`UpgradeError::DuplicateCleanup`) passes — that gate
8604        // rejects more than one cleanup per module per entry (one
8605        // semantic per old version; mixing drain + discard on one
8606        // module is the soft-then-hard fallback footgun the author
8607        // shouldn't write because the operator handles cleanup
8608        // failure escalation itself). The two distinct names cover
8609        // the legitimate "drain a recent old, hard-discard an
8610        // older-still" shape — both authoring forms remain load-
8611        // bearing in this positive-control enumeration.
8612        use crate::{BehaviorSpec, UpgradeFromEntry, UpgradeInstruction};
8613        use std::path::PathBuf;
8614        let root = PathBuf::from("/tmp/x");
8615        let manifest = root.join("caixa.lisp");
8616        let svc = root.join("servicos/demo.computeunit.yaml");
8617        let migration = root.join("lib/migrations/v01-to-v02.lisp");
8618        let on_state_change = root.join("lib/migrations.lisp");
8619        let mut c = caixa(CaixaKind::Servico);
8620        // `:versao` past both entries' `:from` so the cross-slot
8621        // precedence gate (`FromNotBeforeVersao`) lets this canonical
8622        // authoring shape through to the positive-control assertion.
8623        c.versao = "0.2.0".into();
8624        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8625        // `:on-state-change` declared alongside the `(:state-change …)`
8626        // instruction below — the cross-slot composition gate
8627        // (`validate_upgrade_from_against_behavior`) rejects a
8628        // `:state-change` without the callback, so the canonical
8629        // authoring shape this positive control pins now includes the
8630        // runtime delivery hook (the `gen_server:code_change/3` analog
8631        // that the per-version script is invoked through during hot
8632        // upgrade per the upgrade.rs module doc "Composes with"
8633        // promise).
8634        c.behavior = Some(BehaviorSpec {
8635            on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
8636            ..Default::default()
8637        });
8638        c.upgrade_from = vec![
8639            UpgradeFromEntry {
8640                from: "0.1.0".into(),
8641                instructions: vec![
8642                    UpgradeInstruction::LoadModule {
8643                        module: "hello-rio".into(),
8644                    },
8645                    UpgradeInstruction::StateChange {
8646                        script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
8647                    },
8648                    UpgradeInstruction::SoftPurge {
8649                        module: "hello-rio-old".into(),
8650                    },
8651                    UpgradeInstruction::Purge {
8652                        module: "hello-rio-oldest".into(),
8653                    },
8654                ],
8655            },
8656            UpgradeFromEntry {
8657                from: "0.0.9".into(),
8658                instructions: vec![UpgradeInstruction::Restart],
8659            },
8660        ];
8661        let manifest_clone = manifest.clone();
8662        let svc_clone = svc.clone();
8663        let migration_clone = migration.clone();
8664        let on_state_change_clone = on_state_change.clone();
8665        let layout = StandardLayout::new().with_path_exists(move |p| {
8666            p == manifest_clone
8667                || p == svc_clone
8668                || p == migration_clone
8669                || p == on_state_change_clone
8670        });
8671        layout.verify(&c, &root).unwrap();
8672    }
8673
8674    #[test]
8675    fn upgrade_from_restart_mixed_surfaces_as_upgrade_violation() {
8676        // Wiring pin: the within-entry `(:restart)`-exclusivity gate
8677        // (`UpgradeFromEntry::validate_restart_exclusive`) lands on
8678        // the same `LayoutError::UpgradeViolation` axis the per-entry
8679        // shape gate (26da2c7), the cross-entry duplicate-`:from`
8680        // gate (7c6aef2), and the cross-slot `:from < :versao`
8681        // precedence gate (de7ab1a) already do. A caixa.lisp whose
8682        // `:upgrade-from` entry mixes `(:restart)` with a typed
8683        // instruction surfaces at `feira build` time naming the
8684        // offending caixa + the entry's `:from` rather than silently
8685        // passing into the wasm-operator with semantically dead code
8686        // in the operator's dispatch table. Mirrors
8687        // `upgrade_from_duplicate_surfaces_as_upgrade_violation` on
8688        // the peer cross-entry gate.
8689        use crate::{UpgradeFromEntry, UpgradeInstruction};
8690        let root = PathBuf::from("/tmp/x");
8691        let manifest = root.join("caixa.lisp");
8692        let svc = root.join("servicos/demo.computeunit.yaml");
8693        let mut c = caixa(CaixaKind::Servico);
8694        c.versao = "0.2.0".into();
8695        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8696        c.upgrade_from = vec![UpgradeFromEntry {
8697            from: "0.1.0".into(),
8698            instructions: vec![
8699                UpgradeInstruction::LoadModule {
8700                    module: "hello-rio".into(),
8701                },
8702                UpgradeInstruction::Restart,
8703            ],
8704        }];
8705        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
8706        let err = layout.verify(&c, &root).unwrap_err();
8707        let LayoutError::UpgradeViolation { caixa, issue } = err else {
8708            panic!("expected LayoutError::UpgradeViolation for restart-mixed entry, got {err:?}");
8709        };
8710        assert_eq!(caixa, "demo");
8711        assert!(
8712            issue.contains("0.1.0"),
8713            "UpgradeViolation issue must name the offending entry's `:from` verbatim, got \
8714             {issue:?}"
8715        );
8716        assert!(
8717            issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_RESTART),
8718            "UpgradeViolation issue must name the `:restart` axis verbatim, got {issue:?}"
8719        );
8720        assert!(
8721            issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_LOAD_MODULE),
8722            "UpgradeViolation issue must name the non-:restart peer instruction's lisp-form \
8723             verbatim, got {issue:?}"
8724        );
8725    }
8726
8727    #[test]
8728    fn upgrade_from_restart_duplicated_surfaces_as_upgrade_violation() {
8729        // Companion arm: the duplicate-`(:restart)` mode of
8730        // `RestartNotExclusive` (no typed peers, just multiple
8731        // `Restart` variants) surfaces through the same wiring as the
8732        // mixed-with-typed mode above. The diagnostic still names the
8733        // offending entry's `:from` verbatim even when `other_kinds`
8734        // is empty.
8735        use crate::{UpgradeFromEntry, UpgradeInstruction};
8736        let root = PathBuf::from("/tmp/x");
8737        let manifest = root.join("caixa.lisp");
8738        let svc = root.join("servicos/demo.computeunit.yaml");
8739        let mut c = caixa(CaixaKind::Servico);
8740        c.versao = "0.2.0".into();
8741        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8742        c.upgrade_from = vec![UpgradeFromEntry {
8743            from: "0.1.0".into(),
8744            instructions: vec![UpgradeInstruction::Restart, UpgradeInstruction::Restart],
8745        }];
8746        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
8747        let err = layout.verify(&c, &root).unwrap_err();
8748        let LayoutError::UpgradeViolation { caixa, issue } = err else {
8749            panic!(
8750                "expected LayoutError::UpgradeViolation for duplicate-restart entry, got \
8751                 {err:?}"
8752            );
8753        };
8754        assert_eq!(caixa, "demo");
8755        assert!(
8756            issue.contains("0.1.0"),
8757            "UpgradeViolation issue must name the offending entry's `:from` verbatim, got \
8758             {issue:?}"
8759        );
8760        assert!(
8761            issue.contains("(:restart)")
8762                || issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_RESTART),
8763            "UpgradeViolation issue must name the `:restart` axis verbatim, got {issue:?}"
8764        );
8765    }
8766
8767    #[test]
8768    fn upgrade_from_invalid_surfaces_as_layout_violation() {
8769        // The `:from` semver gate (`UpgradeError::FromInvalid`)
8770        // was likewise unreachable before this wiring landed — a
8771        // typo-shaped `:from "v0.1.0"` (git-tag-shape leaking into
8772        // the semver slot) silently passed `feira build` and
8773        // surfaced only when the operator's hot-upgrade decision
8774        // engine tried to match against the version key it couldn't
8775        // parse. Now wired through `UpgradeViolation` with the
8776        // peer-shaped `{ from, reason }` payload — the
8777        // parser-shaped `reason` flows through `Display` so the
8778        // wrapped issue string carries both the offending value
8779        // *and* the SemVer-2 parser's wording (peer with the
8780        // `VersaoInvalid` / `MembroVersaoInvalid` envelopes on the
8781        // sibling SemVer-2 axes).
8782        use crate::{UpgradeFromEntry, UpgradeInstruction};
8783        use std::path::PathBuf;
8784        let root = PathBuf::from("/tmp/x");
8785        let manifest = root.join("caixa.lisp");
8786        let svc = root.join("servicos/demo.computeunit.yaml");
8787        let mut c = caixa(CaixaKind::Servico);
8788        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8789        c.upgrade_from = vec![UpgradeFromEntry {
8790            from: "v0.1.0".into(), // git-tag-shape, not semver
8791            instructions: vec![UpgradeInstruction::Restart],
8792        }];
8793        let manifest_clone = manifest.clone();
8794        let svc_clone = svc.clone();
8795        let layout =
8796            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8797        let err = layout.verify(&c, &root).unwrap_err();
8798        let LayoutError::UpgradeViolation { caixa, issue } = err else {
8799            panic!("expected UpgradeViolation, got {err:?}");
8800        };
8801        assert_eq!(caixa, "demo");
8802        assert!(
8803            issue.contains("v0.1.0"),
8804            "UpgradeViolation issue must name the offending :from value verbatim, got {issue:?}"
8805        );
8806        assert!(
8807            issue.contains(":from"),
8808            "UpgradeViolation issue must name the :from slot verbatim, got {issue:?}"
8809        );
8810        // Pin the parser-shaped reason flow-through: the renamed
8811        // `FromInvalid { from, reason }` carries the SemVer-2 parser's
8812        // wording verbatim, and the [`UpgradeError`] Display routes it
8813        // into the wrapped `issue` string so the layout envelope
8814        // surfaces both the offending value *and* the parser's
8815        // diagnosis. Mirrors the peer flow-through on
8816        // `ManifestError::VersaoInvalid` (top-level `:versao`) and
8817        // `AplicacaoError::MembroVersaoInvalid` (`:membros :versao`).
8818        assert!(
8819            issue.contains("SemVer-2"),
8820            "UpgradeViolation issue must carry the parser-shaped reason (\"SemVer-2\"), got {issue:?}"
8821        );
8822    }
8823
8824    #[test]
8825    fn missing_lib_gate_routes_through_kind_requires_lib_and_caixa_nome() {
8826        // Fail-before-pass-after pin on the two-part converge landed
8827        // at layout.rs:844-847:
8828        //   (a) `caixa.kind().is_biblioteca()` →
8829        //       `caixa.kind().requires_lib()` — routes the biblioteca
8830        //       required-slot gate onto the same `requires_*()`
8831        //       predicate family the three sibling required-slot
8832        //       gates (`requires_exe()` at :856, `requires_servicos()`
8833        //       at :860, `requires_ci()` at :874) already key off.
8834        //       All four gates in the block now share one convention;
8835        //       a future kind that gains its own required-slot gate
8836        //       (an M4/M5 typed arm the CAIXA-SDLC §I six-kind roster
8837        //       may grow) reaches for the same predicate family and
8838        //       inherits the accessor discipline for free.
8839        //   (b) raw `caixa.nome` → `caixa.nome()` — routes the
8840        //       `expected` path composition through the typed
8841        //       [`crate::Caixa::nome`] accessor, closing the last
8842        //       unlifted raw `caixa.nome` production field-access
8843        //       site in `caixa-core/src/layout.rs` (every peer
8844        //       diagnostic in the file already routes through
8845        //       `caixa.nome().to_string()`).
8846        //
8847        // The behavioral pin: for a Biblioteca kind with no fallback
8848        // `lib/<nome>.lisp` file, MissingLib fires and its `expected`
8849        // path composes through `Caixa::nome()`; for every other
8850        // kind, MissingLib does NOT fire (the gate short-circuits on
8851        // kinds where `requires_lib()` returns false), even when the
8852        // fallback file is likewise absent. A future regression that
8853        // reroutes the gate off `requires_lib()` (e.g. onto
8854        // `is_biblioteca()` again, or onto a hand-authored
8855        // `matches!(caixa.kind(), CaixaKind::Biblioteca)`) that
8856        // *happens* to agree byte-for-byte on today's arm-set trips
8857        // this test the moment a future kind's `requires_lib()`
8858        // returns true for a non-`Biblioteca` arm (or the sibling
8859        // required-slot gates diverge from the same convention).
8860        let root = PathBuf::from("/tmp/x");
8861        let manifest = root.join("caixa.lisp");
8862        let manifest_only = manifest.clone();
8863        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_only);
8864
8865        // Biblioteca kind + no lib fallback → MissingLib fires with
8866        // the expected path composed through `Caixa::nome()`.
8867        let bib = caixa(CaixaKind::Biblioteca);
8868        assert!(
8869            bib.kind().requires_lib(),
8870            "requires_lib() must return true for Biblioteca — the four-required-\
8871             slot-gate family's routing depends on this arm's assignment"
8872        );
8873        let err = layout.verify(&bib, &root).unwrap_err();
8874        let LayoutError::MissingLib {
8875            caixa: cname,
8876            expected,
8877        } = err
8878        else {
8879            panic!("expected MissingLib for Biblioteca kind with no lib fallback, got {err:?}");
8880        };
8881        assert_eq!(
8882            cname,
8883            bib.nome(),
8884            "MissingLib `caixa:` carrier must byte-equal Caixa::nome()"
8885        );
8886        assert_eq!(
8887            expected,
8888            root.join(crate::render::LAYOUT_DIR_LIB)
8889                .join(format!("{}.lisp", bib.nome())),
8890            "MissingLib `expected:` path must compose through Caixa::nome() \
8891             verbatim — a raw-field-access regression would silently drift \
8892             the composed path on any future `:nome` axis extension \
8893             (namespace-qualified rewrite, per-cluster alias overlay)"
8894        );
8895
8896        // Non-Biblioteca kinds → the MissingLib gate short-circuits.
8897        // Different kinds fail on their own required-slot gate
8898        // (BinarioWithoutExe, ServicoWithoutServicos, MissingCi) or
8899        // on downstream M2/M3 invariants; none of them may surface as
8900        // MissingLib, because `requires_lib()` returns false for each.
8901        for kind in [
8902            CaixaKind::Binario,
8903            CaixaKind::Servico,
8904            CaixaKind::Supervisor,
8905            CaixaKind::Aplicacao,
8906            CaixaKind::Acao,
8907        ] {
8908            assert!(
8909                !kind.requires_lib(),
8910                "requires_lib() must return false for {kind:?} — the \
8911                 four-required-slot-gate family's arm assignment pins \
8912                 exactly one kind (Biblioteca) as the arm that requires \
8913                 a `lib/` entry"
8914            );
8915            let c = caixa(kind);
8916            let result = layout.verify(&c, &root);
8917            assert!(
8918                !matches!(result, Err(LayoutError::MissingLib { .. })),
8919                "MissingLib gate at layout.rs:844 must short-circuit for \
8920                 kinds where requires_lib() returns false; unexpectedly \
8921                 fired for {kind:?}: {result:?}"
8922            );
8923        }
8924    }
8925
8926    #[test]
8927    fn missing_lib_ctor_matches_struct_literal_wrap() {
8928        // Equivalence pin locking [`LayoutError::missing_lib`] to its
8929        // struct-literal peer under PartialEq. The pre-lift wire-up at
8930        // layout.rs:1010 read `LayoutError::MissingLib { caixa:
8931        // caixa.nome().to_string(), expected }`; the post-lift
8932        // dispatch reads `LayoutError::missing_lib(caixa, expected)`.
8933        // Both must produce byte-equal variants — a silent divergence
8934        // (a `to_lowercase()`, a `trim()`, a lost `.to_string()` copy,
8935        // an accidental `.clone()` of the wrong side of the `expected`
8936        // path) surfaces here rather than at a downstream diagnostic-
8937        // shape drift.
8938        let bib = caixa(CaixaKind::Biblioteca);
8939        let expected = PathBuf::from("/tmp/x")
8940            .join(crate::render::LAYOUT_DIR_LIB)
8941            .join(format!("{}.lisp", bib.nome()));
8942        let struct_lit = LayoutError::MissingLib {
8943            caixa: bib.nome().to_string(),
8944            expected: expected.clone(),
8945        };
8946        let ctor = LayoutError::missing_lib(&bib, expected);
8947        assert_eq!(
8948            struct_lit, ctor,
8949            "missing_lib ctor must byte-equal the pre-lift struct-literal"
8950        );
8951    }
8952
8953    #[test]
8954    fn missing_lib_ctor_projects_nome_through_accessor() {
8955        // Accessor-fidelity pin: any future `:nome` axis extension
8956        // (namespace-qualified rewrite `pleme-io/<nome>`, per-cluster
8957        // alias overlay, case-normalization pass) that lands on
8958        // [`crate::Caixa::nome`] must reach the `caixa:` carrier
8959        // through this projection rather than a raw field access.
8960        // The neighbour required-slot ctor family
8961        // ([`layout_nome_only_ctors!`]) projects the same way; this
8962        // pin locks `missing_lib` onto the same discipline so the
8963        // whole `LayoutError` family stays coherent under any future
8964        // `:nome` rewrite.
8965        //
8966        // Deliberately uses a byte-distinctive nome ("named-lib") so
8967        // a regression that hard-codes a fixture literal at the ctor
8968        // body (rather than projecting through the accessor) drops
8969        // the bytes and trips the assertion.
8970        let mut bib = caixa(CaixaKind::Biblioteca);
8971        bib.nome = "named-lib".into();
8972        let expected = PathBuf::from("/srv")
8973            .join(crate::render::LAYOUT_DIR_LIB)
8974            .join(format!("{}.lisp", bib.nome()));
8975        let err = LayoutError::missing_lib(&bib, expected.clone());
8976        let LayoutError::MissingLib {
8977            caixa: cname,
8978            expected: got,
8979        } = err
8980        else {
8981            panic!("missing_lib ctor must construct the MissingLib variant, got a foreign arm");
8982        };
8983        assert_eq!(
8984            cname,
8985            bib.nome(),
8986            "missing_lib `caixa:` carrier must project through Caixa::nome()"
8987        );
8988        assert_eq!(
8989            got, expected,
8990            "missing_lib `expected:` path must pass through verbatim"
8991        );
8992    }
8993
8994    #[test]
8995    fn missing_lib_verify_wire_up_routes_through_ctor() {
8996        // Behavioural pin: [`StandardLayout::verify`] must reach the
8997        // `MissingLib` variant through the newly lifted ctor rather
8998        // than a residual struct-literal block. The end-to-end
8999        // observable — a Biblioteca with no lib fallback — must
9000        // surface a `MissingLib` whose `caixa:` and `expected:`
9001        // carriers are byte-equal to what the ctor would produce
9002        // when called directly.
9003        let root = PathBuf::from("/opt/pkg");
9004        let manifest = root.join("caixa.lisp");
9005        let manifest_only = manifest.clone();
9006        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_only);
9007        let bib = caixa(CaixaKind::Biblioteca);
9008        let expected = root
9009            .join(crate::render::LAYOUT_DIR_LIB)
9010            .join(format!("{}.lisp", bib.nome()));
9011
9012        let observed = layout.verify(&bib, &root).unwrap_err();
9013        let synthesized = LayoutError::missing_lib(&bib, expected);
9014        assert_eq!(
9015            observed, synthesized,
9016            "StandardLayout::verify must reach MissingLib through the missing_lib ctor \
9017             — a residual struct-literal block would silently diverge on any future \
9018             accessor projection change"
9019        );
9020    }
9021
9022    #[test]
9023    fn ci_on_non_acao_ctor_matches_struct_literal_wrap() {
9024        // Equivalence pin locking [`LayoutError::ci_on_non_acao`] to
9025        // its struct-literal peer under `PartialEq`. The pre-lift
9026        // wire-up at `caixa-core/src/manifest.rs:5586` read
9027        // `LayoutError::CiOnNonAcao { caixa: self.nome().to_string(),
9028        // kind: self.kind() }`; the post-lift dispatch reads
9029        // `LayoutError::ci_on_non_acao(self)`. Both must produce
9030        // byte-equal variants — a silent divergence (a `to_lowercase()`,
9031        // a lost `.to_string()` copy, a swapped kind-copy) surfaces
9032        // here rather than at a downstream diagnostic-shape drift.
9033        let bib = caixa(CaixaKind::Biblioteca);
9034        let struct_lit = LayoutError::CiOnNonAcao {
9035            caixa: bib.nome().to_string(),
9036            kind: bib.kind(),
9037        };
9038        let ctor = LayoutError::ci_on_non_acao(&bib);
9039        assert_eq!(
9040            struct_lit, ctor,
9041            "ci_on_non_acao ctor must byte-equal the pre-lift struct-literal"
9042        );
9043    }
9044
9045    #[test]
9046    fn ci_on_non_acao_ctor_projects_nome_and_kind_through_accessors() {
9047        // Accessor-fidelity pin: any future `:nome` axis extension
9048        // (namespace-qualified rewrite `pleme-io/<nome>`, per-cluster
9049        // alias overlay, case-normalization pass) that lands on
9050        // [`crate::Caixa::nome`] and any future `:kind` re-projection
9051        // (an overlay pass returning a different `CaixaKind` copy)
9052        // that lands on [`crate::Caixa::kind`] must reach the
9053        // `caixa:` / `kind:` carriers through these projections
9054        // rather than raw field accesses. The neighbour
9055        // [`LayoutError::missing_lib`] ctor (b4d5a49) projects nome
9056        // the same way; this pin locks the `:ci` axis onto the same
9057        // discipline so the whole `LayoutError` family stays coherent
9058        // under any future rewrite.
9059        //
9060        // Deliberately uses a byte-distinctive nome ("ci-on-binario")
9061        // and a non-Acao kind (`Binario`) so a regression that
9062        // hard-codes a fixture literal at the ctor body (rather than
9063        // projecting through the accessors) drops the bytes or the
9064        // kind and trips the assertion.
9065        let mut bin = caixa(CaixaKind::Binario);
9066        bin.nome = "ci-on-binario".into();
9067        let err = LayoutError::ci_on_non_acao(&bin);
9068        let LayoutError::CiOnNonAcao {
9069            caixa: cname,
9070            kind: cknd,
9071        } = err
9072        else {
9073            panic!("ci_on_non_acao ctor must construct the CiOnNonAcao variant, got a foreign arm");
9074        };
9075        assert_eq!(
9076            cname,
9077            bin.nome(),
9078            "ci_on_non_acao `caixa:` carrier must project through Caixa::nome()"
9079        );
9080        assert_eq!(
9081            cknd,
9082            bin.kind(),
9083            "ci_on_non_acao `kind:` carrier must project through Caixa::kind()"
9084        );
9085    }
9086
9087    #[test]
9088    fn ci_on_non_acao_verify_wire_up_routes_through_ctor() {
9089        // Behavioural pin: [`StandardLayout::verify`] must reach the
9090        // `CiOnNonAcao` variant through the newly lifted ctor rather
9091        // than a residual struct-literal block. The end-to-end
9092        // observable — a non-Acao caixa with `:ci` declared — must
9093        // surface a `CiOnNonAcao` whose `caixa:` and `kind:` carriers
9094        // are byte-equal to what the ctor would produce when called
9095        // directly.
9096        let root = PathBuf::from("/tmp/x");
9097        let manifest = root.join("caixa.lisp");
9098        let manifest_only = manifest.clone();
9099        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_only);
9100        let mut bib = caixa(CaixaKind::Biblioteca);
9101        bib.ci = Some(canteiro_types::CiRun {
9102            workspace: "pleme-io".into(),
9103            repo: "caixa".into(),
9104            nodes: vec![],
9105        });
9106
9107        let observed = layout.verify(&bib, &root).unwrap_err();
9108        let synthesized = LayoutError::ci_on_non_acao(&bib);
9109        assert_eq!(
9110            observed, synthesized,
9111            "Caixa::validate_ci_kind_coherence must reach CiOnNonAcao through the \
9112             ci_on_non_acao ctor — a residual struct-literal block would silently \
9113             diverge on any future accessor projection change"
9114        );
9115    }
9116}