Skip to main content

caixa_core/
layout.rs

1//! Layout invariants — the Rust-enforced package structure.
2//!
3//! This is the caixa analog of Cargo's implicit `src/lib.rs` vs `src/main.rs`
4//! rule: the Rust type system dictates the package shape, and the invariant
5//! checker runs before any build step. [`StandardLayout`] encodes the
6//! canonical layout:
7//!
8//! - `caixa.lisp`           — always required
9//! - `lib/<nome>.lisp`      — required when `:kind Biblioteca` and
10//!                            `:bibliotecas` is empty
11//! - each `:bibliotecas`    — must resolve on disk
12//! - each `:exe`            — must resolve on disk, under `exe/`
13//! - each `:servicos`       — must resolve on disk, under `servicos/`
14//!
15//! Filesystem I/O is injected through [`StandardLayout::with_path_exists`]
16//! so tests can run without touching disk.
17
18use std::path::{Path, PathBuf};
19use std::sync::Arc;
20
21use thiserror::Error;
22
23use crate::{Caixa, CaixaKind};
24
25/// Contract — a caixa layout checker.
26pub trait LayoutInvariants {
27    /// Verify every declared path resolves + kind-specific invariants hold.
28    fn verify(&self, caixa: &Caixa, root: &Path) -> Result<(), LayoutError>;
29}
30
31type ExistsFn = Arc<dyn Fn(&Path) -> bool + Send + Sync>;
32
33/// The default layout contract.
34#[derive(Default, Clone)]
35pub struct StandardLayout {
36    path_exists: Option<ExistsFn>,
37}
38
39impl StandardLayout {
40    #[must_use]
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// Override how file existence is tested. Useful for in-memory tests.
46    #[must_use]
47    pub fn with_path_exists<F>(mut self, f: F) -> Self
48    where
49        F: Fn(&Path) -> bool + Send + Sync + 'static,
50    {
51        self.path_exists = Some(Arc::new(f));
52        self
53    }
54
55    fn exists(&self, p: &Path) -> bool {
56        self.path_exists
57            .as_ref()
58            .map_or_else(|| p.exists(), |f| f(p))
59    }
60
61    /// Probe an authored path's resolved on-disk location under `root`
62    /// and, on absence, return the paired [`LayoutError::MissingEntry`]
63    /// naming the missing entry at that resolved location with the
64    /// caller-provided canonical `kind` label (from the
65    /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] const family).
66    /// Returns the resolved [`PathBuf`] on hit so callers that need to
67    /// run a follow-up per-path gate (the `:exe` /
68    /// `:servicos` sandbox-directory-containment check the peer
69    /// wire-up sites carry) reach it without re-computing
70    /// `root.join(path)`.
71    ///
72    /// Folds the five self-similar
73    /// `let full = root.join(p); if !self.exists(&full) { return
74    /// Err(LayoutError::missing_entry(<kind>, full)); }` existence-probe
75    /// blocks at [`Self::verify`] (`:bibliotecas` iteration, `:exe`
76    /// iteration, `:servicos` iteration, `:behavior` on-disk callback-
77    /// path iteration, `:upgrade-from` per-instruction script-path
78    /// iteration) onto one substrate primitive on [`StandardLayout`].
79    /// Every future consumer that wants to probe a declared path
80    /// against an out-of-band filesystem oracle (a per-slot admission
81    /// webhook, a `feira validate --paths` per-caixa admission verb,
82    /// the deferred `caixa.pleme.io/v1alpha1/Caixa` CR materializer's
83    /// admission-webhook floor, a per-cluster overlay resolver
84    /// re-probing a declared path against a cluster-local filesystem
85    /// snapshot) reaches the two-step `root.join` + `exists` + wrap
86    /// dispatch through one call rather than re-inlining the three-
87    /// line block in lockstep with the five wire-up sites.
88    fn probe_declared_entry<P: AsRef<Path>>(
89        &self,
90        path: P,
91        root: &Path,
92        kind: &'static str,
93    ) -> Result<PathBuf, LayoutError> {
94        let full = root.join(path.as_ref());
95        if !self.exists(&full) {
96            return Err(LayoutError::missing_entry(kind, full));
97        }
98        Ok(full)
99    }
100
101    /// Probe an authored path's resolved on-disk location under `root`
102    /// and, on presence, verify the resolved [`PathBuf`] lives inside
103    /// the paired `sandbox_dir` sub-tree; on absence return the paired
104    /// [`LayoutError::MissingEntry`] (via [`Self::probe_declared_entry`])
105    /// and on sandbox escape return `outside_ctor(full)` naming the
106    /// offending resolved path.
107    ///
108    /// Folds the two self-similar `let full = self.probe_declared_entry(
109    /// p, root, <kind>)?; if !full.starts_with(&<slot>_dir) { return
110    /// Err(LayoutError::<Slot>OutsideDir(full)); }` blocks at
111    /// [`Self::verify`] (`:exe` iteration, `:servicos` iteration) onto
112    /// one substrate primitive on [`StandardLayout`]. The two wire-ups
113    /// used the identical two-arm cascade around the peer
114    /// [`Self::probe_declared_entry`] hit-arm hand-off, differing only
115    /// in the three names bound at each site — the `kind` label, the
116    /// resolved `sandbox_dir`, and the paired outside-dir
117    /// tuple-variant constructor (`LayoutError::ExeOutsideDir` /
118    /// `LayoutError::ServicoOutsideDir`) — exactly the shape the PRIME
119    /// DIRECTIVE names as a bug.
120    ///
121    /// Diagnostic order preserved: [`LayoutError::MissingEntry`] fires
122    /// before `outside_ctor` on the same iteration (the pre-lift order
123    /// this primitive's sibling [`Self::probe_declared_entry`]
124    /// docstring documents). Every future consumer that wants to probe
125    /// a declared path against an out-of-band filesystem oracle *and*
126    /// gate the resolved path on a sandbox sub-tree — a per-slot
127    /// admission webhook probing a declared `:exe` / `:servicos` entry
128    /// against a mounted cluster-local filesystem snapshot, a
129    /// per-cluster overlay resolver rejecting a sandbox-escape patch,
130    /// the deferred `caixa.pleme.io/v1alpha1/Caixa` CR materializer's
131    /// admission-webhook floor — reaches the two-arm probe + sandbox
132    /// dispatch through one call rather than re-inlining the three-
133    /// line block in lockstep with the two `verify` wire-up sites.
134    fn probe_sandboxed_declared_entry<P: AsRef<Path>>(
135        &self,
136        path: P,
137        root: &Path,
138        kind: &'static str,
139        sandbox_dir: &Path,
140        outside_ctor: fn(PathBuf) -> LayoutError,
141    ) -> Result<PathBuf, LayoutError> {
142        let full = self.probe_declared_entry(path, root, kind)?;
143        if !full.starts_with(sandbox_dir) {
144            return Err(outside_ctor(full));
145        }
146        Ok(full)
147    }
148
149    /// Probe every path yielded by `paths` against the injected oracle
150    /// under `root`; on the first miss return the paired
151    /// [`LayoutError::MissingEntry`] naming the offending entry with the
152    /// caller-provided canonical `kind` label (via the sibling
153    /// [`Self::probe_declared_entry`] primitive), and on empty / all-hit
154    /// arms return `Ok(())`.
155    ///
156    /// Folds the three self-similar per-slot existence-probe *loop*
157    /// blocks at [`Self::verify`] onto one substrate primitive on
158    /// [`StandardLayout`]:
159    ///
160    /// - `:bibliotecas` iteration — `for p in caixa.bibliotecas() { self.
161    ///   probe_declared_entry(p, root, LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA)?; }`,
162    /// - `:behavior` on-disk callback-path iteration — `for p in
163    ///   b.declared_paths() { self.probe_declared_entry(p, root,
164    ///   LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK)?; }`,
165    /// - `:upgrade-from` per-instruction script-path iteration — the
166    ///   `for entry in caixa.upgrade_from() { for instr in entry.
167    ///   instructions() { if let Some(p) = instr.declared_path() { self.
168    ///   probe_declared_entry(p, root, LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT)?;
169    ///   } } }` nested cascade, flattened at the wire-up site through
170    ///   `.iter().flat_map(_::instructions).filter_map(_::declared_path)`.
171    ///
172    /// Three consumers, three identical `for … { self.probe_declared_entry
173    /// (…, kind)?; }` loop shapes, one substrate primitive on
174    /// [`StandardLayout`] closing the duplication the PRIME DIRECTIVE
175    /// names as a bug — sibling to the peer per-arm-probe
176    /// [`Self::probe_declared_entry`] (fda1e35) and the two-arm-sandboxed
177    /// [`Self::probe_sandboxed_declared_entry`] (4940d55) primitives on
178    /// the same layout-pipeline existence-probe axis. Diagnostic order
179    /// preserved: iteration proceeds in the caller-supplied iterator
180    /// order and short-circuits on the first miss, byte-equal to the
181    /// pre-lift `for … { … ? }` loop's first-error return semantics.
182    /// Every future consumer that wants to sweep a per-slot declared-
183    /// path list against an out-of-band filesystem oracle (a per-slot
184    /// admission webhook probing an entire `:bibliotecas` / `:behavior`
185    /// / `:upgrade-from` slot as a unit against a mounted cluster-local
186    /// filesystem snapshot, a `feira validate --exists` per-caixa
187    /// admission verb, the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
188    /// materializer's admission-webhook floor, a per-cluster overlay
189    /// resolver re-probing a slot-of-paths after a patch) reaches the
190    /// per-slot batch through one call rather than re-inlining the
191    /// three-line loop body in lockstep with the three `verify` wire-up
192    /// sites.
193    fn probe_declared_entries<I, P>(
194        &self,
195        paths: I,
196        root: &Path,
197        kind: &'static str,
198    ) -> Result<(), LayoutError>
199    where
200        I: IntoIterator<Item = P>,
201        P: AsRef<Path>,
202    {
203        for p in paths {
204            self.probe_declared_entry(p, root, kind)?;
205        }
206        Ok(())
207    }
208
209    /// Probe every path yielded by `paths` against the injected oracle
210    /// under `root` and, on presence, verify the resolved path lives
211    /// inside `sandbox_dir` (via the sibling
212    /// [`Self::probe_sandboxed_declared_entry`] primitive); on the first
213    /// miss return the paired [`LayoutError::MissingEntry`] naming the
214    /// offending entry with the caller-provided canonical `kind` label,
215    /// on the first sandbox-escape return `outside_ctor(full)` naming
216    /// the offending resolved path, and on empty / all-hit arms return
217    /// `Ok(())`.
218    ///
219    /// Folds the two self-similar per-slot sandboxed-existence-probe
220    /// *loop* blocks at [`Self::verify`] onto one substrate primitive on
221    /// [`StandardLayout`]:
222    ///
223    /// - `:exe` iteration — `let exe_dir = root.join(LAYOUT_DIR_EXE); for
224    ///   p in caixa.exe() { self.probe_sandboxed_declared_entry(p, root,
225    ///   LAYOUT_MISSING_ENTRY_KIND_EXE, &exe_dir, LayoutError::ExeOutsideDir)?; }`,
226    /// - `:servicos` iteration — `let servicos_dir = root.join(
227    ///   LAYOUT_DIR_SERVICOS); for p in caixa.servicos() {
228    ///   self.probe_sandboxed_declared_entry(p, root,
229    ///   LAYOUT_MISSING_ENTRY_KIND_SERVICO, &servicos_dir,
230    ///   LayoutError::ServicoOutsideDir)?; }`.
231    ///
232    /// Two consumers, two identical `for … { self.probe_sandboxed_declared_entry
233    /// (…, kind, &sandbox_dir, outside_ctor)?; }` loop shapes, one
234    /// substrate primitive on [`StandardLayout`] closing the duplication
235    /// the PRIME DIRECTIVE names as a bug — sibling to the peer per-slot
236    /// batch [`Self::probe_declared_entries`] (d1ccb0b) primitive on the
237    /// non-sandboxed axis and the per-arm [`Self::probe_declared_entry`]
238    /// (fda1e35) / [`Self::probe_sandboxed_declared_entry`] (4940d55)
239    /// primitives on the per-path axis. The four together close the
240    /// layout-pipeline existence-probe algebra: every wire-up on the
241    /// (per-arm | per-slot batch) × (bare | sandboxed) product now
242    /// routes through one substrate primitive rather than N open-coded
243    /// blocks.
244    ///
245    /// Diagnostic order preserved: iteration proceeds in the caller-
246    /// supplied iterator order and short-circuits on the first miss or
247    /// sandbox-escape, byte-equal to the pre-lift `for … { … ? }` loop's
248    /// first-error return semantics. Within each iteration
249    /// [`LayoutError::MissingEntry`] fires before `outside_ctor` (the
250    /// order the peer [`Self::probe_sandboxed_declared_entry`] primitive
251    /// docstring documents). Every future consumer that wants to sweep a
252    /// per-slot declared-path list against an out-of-band filesystem
253    /// oracle *and* gate each resolved path on a sandbox sub-tree (a
254    /// per-slot admission webhook probing an entire `:exe` / `:servicos`
255    /// slot as a unit against a mounted cluster-local filesystem
256    /// snapshot, a `feira validate --exists --sandbox` per-caixa
257    /// admission verb, the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
258    /// materializer's admission-webhook floor, a per-cluster overlay
259    /// resolver re-probing a slot-of-sandboxed-paths after a patch)
260    /// reaches the per-slot batch through one call rather than
261    /// re-inlining the three-line loop body in lockstep with the two
262    /// `verify` wire-up sites.
263    fn probe_sandboxed_declared_entries<I, P>(
264        &self,
265        paths: I,
266        root: &Path,
267        kind: &'static str,
268        sandbox_dir: &Path,
269        outside_ctor: fn(PathBuf) -> LayoutError,
270    ) -> Result<(), LayoutError>
271    where
272        I: IntoIterator<Item = P>,
273        P: AsRef<Path>,
274    {
275        for p in paths {
276            self.probe_sandboxed_declared_entry(p, root, kind, sandbox_dir, outside_ctor)?;
277        }
278        Ok(())
279    }
280}
281
282impl std::fmt::Debug for StandardLayout {
283    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284        f.debug_struct("StandardLayout")
285            .field("custom_exists", &self.path_exists.is_some())
286            .finish()
287    }
288}
289
290impl LayoutInvariants for StandardLayout {
291    fn verify(&self, caixa: &Caixa, root: &Path) -> Result<(), LayoutError> {
292        let manifest = root.join("caixa.lisp");
293        if !self.exists(&manifest) {
294            return Err(LayoutError::MissingManifest(manifest));
295        }
296
297        // Caixa-identity value-shape gates on the two universal axes
298        // (`:nome`, `:versao`) every substrate-side artifact's
299        // `metadata.name` / version derivation flows through. The
300        // [`Caixa::validate_nome`] / [`Caixa::validate_versao`] doc-
301        // comments name the canonical authoring footguns verbatim —
302        // `:nome` "MyApp" / "my_app" / "team.app" / "-app" / "café"
303        // (DNS-1123 violations the K8s apiserver refuses at admission
304        // time on every derived `metadata.name`: `lareira-<nome>`,
305        // programs.yaml entry, CiliumNetworkPolicy / HTTPRoute names,
306        // `LABEL_APLICACAO` value); `:versao` "0.1" / "v0.1.0" / "latest"
307        // / "^0.1" / "0.1.0.0" (SemVer-2 violations Helm / OCI tag /
308        // `feira publish` git tag / lacre `concrete_versao` /
309        // `:upgrade-from :from` peer matching each refuse downstream).
310        // Until this wire-up landed both validators existed as `pub fn`
311        // on [`Caixa`] (with full per-arm test coverage in
312        // `manifest::tests`) but no production code path called them —
313        // `feira build` (the canonical author-time gate) silently
314        // accepted a malformed `:nome` / `:versao` and the failure
315        // surfaced at `helm install` / `kubectl apply` / `feira publish`
316        // time on the *first* downstream consumer to strict-parse the
317        // value, far from the source `caixa.lisp` and without any field
318        // naming the offending Caixa identity axis. The gate runs
319        // *after* [`LayoutError::MissingManifest`] (no caixa to check
320        // when the manifest is missing) and *before* every kind-coherence
321        // gate (each of which carries `caixa.nome` verbatim in its
322        // diagnostic — running them first on a structurally-invalid
323        // identity would surface a "this kind has slot X" diagnostic
324        // against an unrecoverable name). Cross-axis precedence is
325        // `:nome` → `:versao` — the canonical declaration order on
326        // [`Caixa`] and the same author-grep ordering the
327        // [`ManifestError`] family uses. Same per-axis `*Violation
328        // { caixa, issue }` envelope every peer per-axis wrap exposes
329        // ([`LayoutError::CodePathViolation`] b868442,
330        // [`LayoutError::LimitsViolation`] / [`LayoutError::BehaviorViolation`]
331        // / [`LayoutError::UpgradeViolation`] / [`LayoutError::SupervisorViolation`]
332        // / [`LayoutError::AplicacaoViolation`]).
333        caixa.run_layout_gate(Caixa::validate_nome, LayoutError::nome_violation)?;
334        // `:nome`-side joint-length budget on the canonical
335        // `lareira-<nome>` chart-name shape — the second arm on the
336        // shared `:nome` axis after the bare-DNS-1123 gate above. Runs
337        // through the same [`LayoutError::NomeViolation`] envelope so
338        // every per-axis diagnostic on `:nome` carries one wrap shape,
339        // peer with the [`Caixa::validate_nome`] → `NomeInvalid`
340        // routing already at this site. The chart-name budget is the
341        // second-axis ceiling [`Caixa::validate_nome`] cannot see — a
342        // 56-byte DNS-1123-valid `:nome` passes the bare-`:nome` shape
343        // but produces a 64-byte `lareira-<nome>` chart name the
344        // apiserver / `helm lint` rejects at admission, far from the
345        // source `caixa.lisp` and naming none of the joint-length
346        // overflow's three carriers (DNS-1123 cap, prefix, `:nome`
347        // length). Closing it at this wire-up turns the
348        // [`lareira_chart_name`] doc-comment's explicit M4-admission
349        // deferral (caixa-core/src/render.rs:3198) into a build-time
350        // structural property of every emitted artifact.
351        caixa.run_layout_gate(
352            Caixa::validate_nome_chart_name_budget,
353            LayoutError::nome_violation,
354        )?;
355        caixa.run_layout_gate(Caixa::validate_versao, LayoutError::versao_violation)?;
356
357        // `:deps` / `:deps-dev` per-entry shape gate. The third Caixa-
358        // level orphan validator on the universal authoring surface (peer
359        // of [`Caixa::validate_nome`] / [`Caixa::validate_versao`] wired
360        // immediately above): [`Caixa::validate_deps`] walks every
361        // [`Dep::validate`] arm — empty / non-DNS-1123 `:nome`, empty /
362        // unparseable `:versao` requirement, malformed `:fonte` repo /
363        // pin / `:caminho`, malformed `:caracteristicas` Cargo-feature
364        // name (de68c0c) — and then closes the per-list set-not-multiset
365        // duplicate-`:nome` invariant on each of `:deps` and `:deps-dev`
366        // (359fba5). Until this wire-up landed `validate_deps` existed as
367        // `pub fn` on [`Caixa`] with full per-arm unit coverage in
368        // `manifest::tests` + `dep::tests` (validate_deps_rejects_*,
369        // 53 dep-axis tests) but no production code path called it —
370        // `feira build` (the canonical author-time gate;
371        // `caixa-feira/src/cmd/build.rs:29` routes through
372        // `StandardLayout::verify`) silently accepted a malformed `:deps`
373        // entry and the failure surfaced at the *first* downstream
374        // consumer to strict-parse it: at lacre-resolve time as a
375        // `semver::Error` not naming the offending dep (`:versao` per-
376        // entry); at `git clone` time as a fetch failure quoting the
377        // shell-escape `repo` (`:fonte :repo`); at the resolver's
378        // `HashMap<:nome>` collapse as a silent "second-wins" overwrite
379        // (within-list `:nome` duplicate); at `cargo metadata` time as a
380        // feature-name rejection on the *target* caixa rather than the
381        // dep entry referencing it (`:caracteristicas`); at `helm
382        // install` / `kubectl apply` time as an apiserver `metadata.name`
383        // rejection on the rendered `lareira-<nome>` chart's per-dep
384        // derivation (DNS-1123-violating `:deps :nome`) — each far from
385        // the source `caixa.lisp`, none naming the offending `:deps` /
386        // `:deps-dev` axis. Runs *after* the Caixa-identity gates (the
387        // diagnostic carries `caixa.nome().to_string()` verbatim, which the
388        // peer [`Caixa::validate_nome`] gate above has just guaranteed is
389        // a valid DNS-1123 label) and *before* every kind-coherence gate
390        // (the dep surface is universal — every kind has `:deps` /
391        // `:deps-dev` — so its shape diagnostic is more fundamental than
392        // the kind-coherence partitions on `:bibliotecas` / `:exe` /
393        // `:servicos` / `:membros` / `:children` / M2 slots that follow).
394        // Same per-axis `*Violation { caixa, issue }` envelope every peer
395        // per-axis wrap exposes ([`LayoutError::NomeViolation`] /
396        // [`LayoutError::VersaoViolation`] (1f74a5f),
397        // [`LayoutError::CodePathViolation`] (b868442),
398        // [`LayoutError::LimitsViolation`] / [`LayoutError::BehaviorViolation`]
399        // / [`LayoutError::UpgradeViolation`] / [`LayoutError::SupervisorViolation`]
400        // / [`LayoutError::AplicacaoViolation`]). Threads [`DepError`]
401        // Display through verbatim — every per-arm reason already names
402        // the offending dep's `:nome` (e.g. `":deps entry "caixa-teia"
403        // :versao "^bad" is not a valid semver requirement: …"`), so the
404        // wrap envelope's `issue` carries a self-locating "which dep,
405        // which axis, why" without re-shaping the per-arm parser-side
406        // reason. With this wire-up the canonical author-time gate
407        // refuses every ill-formed `:deps` / `:deps-dev` value-shape by
408        // construction — closing the second-to-last orphan-validator gap
409        // on the typed Caixa surface (`validate_restart_window` is the
410        // remaining orphan, Supervisor-axis specific and wired into the
411        // Supervisor branch below alongside `view.validate()`).
412        // Compound per-Caixa entry gate on the dep-graph axis: the
413        // layout pipeline's two-dispatch `:deps` / `:deps-dev` cascade
414        // — the per-entry + within-list duplicate-`:nome` gate (the
415        // [`crate::Dep::validate`] + [`crate::render::insert_first_seen`]
416        // cascade `Caixa::validate_deps` opened on, 359fba5) and the
417        // cross-slot self-edge gate
418        // ([`crate::dep::validate_no_self_dep`], ad4abf1) — folded
419        // onto the [`crate::Caixa::validate_deps`] substrate primitive.
420        // The two arms run in the same canonical order at the primitive
421        // (per-entry + cross-entry duplicate → cross-slot self-edge) so
422        // the fold is byte-for-byte equivalent to the pre-fold
423        // two-block cascade this call site formerly carried, pinned by
424        // the paired
425        // `validate_deps_folds_{per_entry,self_edge}_arm_matches_gate`
426        // equivalence pins and the
427        // `validate_deps_per_entry_arm_fires_before_self_edge_arm`
428        // ordering pin in the [`crate::Caixa::validate_deps`] pin
429        // family (`manifest.rs`).
430        //
431        // Same lift discipline the peer per-slot compound gates
432        // ([`crate::AplicacaoSpec::validate_contratos`] and its
433        // `:membros` / `:entrada` / `:placement` / `:politicas` peers,
434        // [`crate::MeshPolicy::validate`],
435        // [`crate::SupervisorSpec::validate_children`],
436        // [`crate::Caixa::validate_upgrade_from`] d6801df) each carry —
437        // one named substrate-primitive gate per typed slot folds every
438        // structural + cross-slot axis on that slot onto one call, so
439        // every future consumer that wants to re-check the dep-graph
440        // after a per-entry patch (the deferred
441        // `caixa.pleme.io/v1alpha1/Caixa` CR materializer's admission
442        // webhook, a future `feira validate --deps` per-caixa admission
443        // verb, a per-`:deps` overlay resolver) reaches the two-arm
444        // compound gate through one dispatch rather than re-inlining
445        // the two-dispatch cascade in lockstep with this wire-up.
446        caixa.run_layout_gate(Caixa::validate_deps, LayoutError::deps_violation)?;
447
448        // `:etiquetas` per-entry empty + cross-entry duplicate gate. The
449        // fourth universal-axis Caixa-level value-shape gate (peer of
450        // [`Caixa::validate_nome`] / [`Caixa::validate_versao`] /
451        // [`Caixa::validate_deps`] wired immediately above and
452        // [`Caixa::validate_code_paths`] wired below the kind-coherence
453        // gates) on the typed Caixa surface. `:etiquetas` is the
454        // registry-search-tag axis every kind carries (universal
455        // `Vec<String>` slot on [`Caixa`]) and lands verbatim as the
456        // Helm chart `Chart.yaml` `keywords:` array on every Servico
457        // (`caixa-helm/src/lib.rs:236` folds it through a
458        // [`std::collections::BTreeSet`]). Until this wire-up landed
459        // `:etiquetas` had no shape gate at any layer — an empty entry
460        // (`(:etiquetas (""))` — the canonical paste-from-blank-doc
461        // footgun) silently rendered as `keywords: [""]` in `Chart.yaml`,
462        // and duplicate entries (`(:etiquetas ("demo" "demo"))` — the
463        // copy-paste-the-wrong-tag footgun) were silently dedup'd by
464        // the renderer's `BTreeSet` collect — a "second wins / one
465        // silently disappears" shape divergent from every peer typed-
466        // graph set gate (`:membros :caixa`, `:placement :clusters`,
467        // `:entrada :paths`, `:contratos`, `:deps :nome`,
468        // `:upgrade-from :from`, the per-instruction-class singularity
469        // gates on `:upgrade-from :instructions`). Runs *after* the
470        // peer universal `:nome` / `:versao` / `:deps` gates (declaration
471        // order on [`Caixa`] is `:nome` → `:versao` → `:edicao` →
472        // `:descricao` → `:repositorio` → `:licenca` → `:autores` →
473        // `:etiquetas` → `:deps` → `:deps-dev`, but the gate order
474        // follows the same identity-axis-first cascade the peer gates
475        // establish: `:nome` → `:versao` are the load-bearing identity
476        // axes that flow into every diagnostic's caixa prefix, and
477        // `:deps` is the universal dep surface that dominates every
478        // kind-coherence gate; `:etiquetas` runs after this trio so the
479        // diagnostic carries an already-validated `:nome` and the
480        // peer universal axes' narrower diagnostics surface first when
481        // multiple axes are malformed) and *before* the kind-coherence
482        // gates ([`Self::MeshSlotsOnNonAplicacao`] /
483        // [`Self::SupervisorSlotsOnNonSupervisor`] /
484        // [`Self::ServicoSlotsOnNonServico`] / [`Self::ForeignCodeSlot`])
485        // — `:etiquetas` is universal so its shape diagnostic is more
486        // fundamental than the kind-coherence partitions on kind-
487        // exclusive slot sets.
488        //
489        // Same per-axis `*Violation { caixa, issue }` envelope every peer
490        // per-axis wrap exposes ([`Self::NomeViolation`] /
491        // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
492        // aa77d0f, [`Self::CodePathViolation`] b868442,
493        // [`Self::RestartWindowViolation`] 10e321a). Threads
494        // [`ManifestError::EtiquetaEmpty`] / [`ManifestError::EtiquetaDuplicate`]
495        // Display through verbatim — each per-arm reason already names
496        // the offending tag (for the duplicate arm) or the structural
497        // "empty entry" defect (for the empty arm), so the wrap
498        // envelope's `issue` carries a self-locating "which axis, which
499        // entry, why" without re-shaping the per-arm reason.
500        caixa.run_layout_gate(Caixa::validate_etiquetas, LayoutError::etiquetas_violation)?;
501
502        // `:autores` per-entry empty + cross-entry duplicate gate. The
503        // fifth universal-axis Caixa-level value-shape gate (peer of
504        // [`Caixa::validate_nome`] / [`Caixa::validate_versao`] /
505        // [`Caixa::validate_deps`] / [`Caixa::validate_etiquetas`] wired
506        // immediately above and [`Caixa::validate_code_paths`] wired
507        // below the kind-coherence gates) on the typed Caixa surface.
508        // `:autores` is the maintainer-axis every kind carries
509        // (universal `Vec<String>` slot on [`Caixa`]) and lands verbatim
510        // as the Helm chart `Chart.yaml` `maintainers:` array on every
511        // Servico (`caixa-helm/src/lib.rs:251` maps each entry to a
512        // `Maintainer { name, email: None }` without dedup). Until this
513        // wire-up landed `:autores` had no shape gate at any layer — an
514        // empty entry (`(:autores (""))` — the canonical paste-from-
515        // blank-doc footgun) silently rendered as
516        // `maintainers: [{name: "", email: null}]` in `Chart.yaml`, and
517        // duplicate entries (`(:autores ("pleme-io" "pleme-io"))` —
518        // the copy-paste-the-wrong-author footgun) stacked verbatim in
519        // the chart. Unlike the peer `:etiquetas` axis (where the
520        // renderer's `BTreeSet` collect silently dedups the `keywords:`
521        // array at chart render — a "second wins / one silently
522        // disappears" shape), `maintainers:` has *no* renderer-side
523        // dedup, so duplicate `:autores` entries render as two identical
524        // maintainer records by construction — a strictly worse footgun
525        // than the peer `:etiquetas` shape. Runs *after* the peer
526        // universal `:nome` / `:versao` / `:deps` / `:etiquetas` gates
527        // (the gate order follows the canonical identity-axis-first
528        // cascade the peer gates establish; `:autores` and `:etiquetas`
529        // are the two Vec-shaped universal metadata axes — they sit
530        // adjacent in the cascade after the load-bearing identity +
531        // dep trio) and *before* the kind-coherence gates
532        // ([`Self::MeshSlotsOnNonAplicacao`] /
533        // [`Self::SupervisorSlotsOnNonSupervisor`] /
534        // [`Self::ServicoSlotsOnNonServico`] / [`Self::ForeignCodeSlot`])
535        // — `:autores` is universal so its shape diagnostic is more
536        // fundamental than the kind-coherence partitions on kind-
537        // exclusive slot sets.
538        //
539        // Same per-axis `*Violation { caixa, issue }` envelope every peer
540        // per-axis wrap exposes ([`Self::NomeViolation`] /
541        // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
542        // aa77d0f, [`Self::EtiquetasViolation`] 360a499,
543        // [`Self::CodePathViolation`] b868442,
544        // [`Self::RestartWindowViolation`] 10e321a). Threads
545        // [`ManifestError::AutorEmpty`] / [`ManifestError::AutorDuplicate`]
546        // Display through verbatim — each per-arm reason already names
547        // the offending author (for the duplicate arm) or the structural
548        // "empty entry" defect (for the empty arm), so the wrap
549        // envelope's `issue` carries a self-locating "which axis, which
550        // entry, why" without re-shaping the per-arm reason.
551        caixa.run_layout_gate(Caixa::validate_autores, LayoutError::autores_violation)?;
552
553        // `:repositorio` git-repo-URL shape gate. The sixth
554        // universal-axis Caixa-level value-shape gate (peer of
555        // [`Caixa::validate_nome`] / [`Caixa::validate_versao`] /
556        // [`Caixa::validate_deps`] / [`Caixa::validate_etiquetas`] /
557        // [`Caixa::validate_autores`] wired immediately above and
558        // [`Caixa::validate_code_paths`] wired below the kind-coherence
559        // gates) on the typed Caixa surface. `:repositorio` is the
560        // universal git-shaped homepage axis every kind carries
561        // (universal `Option<String>` slot on [`Caixa`]) and routes
562        // through two load-bearing substrate consumers:
563        // [`caixa-helm`] folds it verbatim into the rendered
564        // `lareira-<nome>` Helm chart's `Chart.yaml` `home:` field
565        // (`build_chart_yaml` at `caixa-helm/src/lib.rs:268`) and into
566        // the chart `README.md` `repo = …` interpolation
567        // (`caixa-helm/src/lib.rs:359`); [`caixa-flux`] folds it
568        // verbatim into the standalone `ClusterBundleOpts::for_caixa`
569        // `git_url:` field (`caixa-flux/src/lib.rs:293`), which
570        // becomes the FluxCD `GitRepository.spec.url` the cluster's
571        // source-controller polls — the load-bearing deploy-time axis.
572        // Both consumers use `Option::unwrap_or_else(|| <fallback>)`
573        // to substitute a placeholder when the slot is absent (`None`
574        // → the fallback fires); a `Some("")` *skips the fallback*
575        // and silently passes the empty string through to
576        // `Chart.yaml home: ""` / `GitRepository url: ""`. Until this
577        // wire-up landed `:repositorio` had no shape gate at any
578        // layer — empty (`(:repositorio "")` — the canonical
579        // paste-from-blank-doc footgun) and malformed (whitespace,
580        // control char / CRLF, leading `-` CLI-arg-injection,
581        // missing `:` separator) values silently landed in the
582        // rendered artifacts and broke at `helm template` / FluxCD
583        // reconcile time far from the source `caixa.lisp`.
584        //
585        // Runs *after* the peer universal `:nome` / `:versao` /
586        // `:deps` / `:etiquetas` / `:autores` gates (the gate order
587        // follows the canonical identity-axis-first cascade the peer
588        // gates establish; `:repositorio` is the universal git-URL
589        // axis — it sits adjacent to `:autores` in the cascade after
590        // the load-bearing identity + dep trio + the two Vec-shaped
591        // universal metadata axes) and *before* the kind-coherence
592        // gates ([`Self::MeshSlotsOnNonAplicacao`] /
593        // [`Self::SupervisorSlotsOnNonSupervisor`] /
594        // [`Self::ServicoSlotsOnNonServico`] / [`Self::ForeignCodeSlot`])
595        // — `:repositorio` is universal so its shape diagnostic is
596        // more fundamental than the kind-coherence partitions on
597        // kind-exclusive slot sets.
598        //
599        // Same per-axis `*Violation { caixa, issue }` envelope every
600        // peer per-axis wrap exposes ([`Self::NomeViolation`] /
601        // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
602        // aa77d0f, [`Self::EtiquetasViolation`] 360a499,
603        // [`Self::AutoresViolation`] 86c769b, [`Self::CodePathViolation`]
604        // b868442, [`Self::RestartWindowViolation`] 10e321a). Threads
605        // [`ManifestError::RepositorioEmpty`] /
606        // [`ManifestError::RepositorioInvalid`] Display through
607        // verbatim — each per-arm reason already names the offending
608        // `:repositorio` value (for the invalid arm) or the
609        // structural "empty entry" defect (for the empty arm), so
610        // the wrap envelope's `issue` carries a self-locating "which
611        // axis, which value, why" without re-shaping the per-arm
612        // reason. With this gate the two `git URL`-shaped surfaces on
613        // the typed Caixa (`:repositorio` here, `:deps :fonte :repo`
614        // peer routed through the same shared
615        // [`crate::render::is_git_repo_url`] predicate via
616        // [`crate::DepSource::validate`]) are now structurally
617        // equivalent — every value past validate is
618        // guaranteed-acceptable by the shared predicate's constraint
619        // union, by construction.
620        caixa.run_layout_gate(
621            Caixa::validate_repositorio,
622            LayoutError::repositorio_violation,
623        )?;
624
625        // `:descricao` non-empty shape gate. The seventh universal-
626        // axis Caixa-level value-shape gate (peer of
627        // [`Caixa::validate_nome`] / [`Caixa::validate_versao`] /
628        // [`Caixa::validate_deps`] / [`Caixa::validate_etiquetas`] /
629        // [`Caixa::validate_autores`] / [`Caixa::validate_repositorio`]
630        // wired immediately above and [`Caixa::validate_code_paths`]
631        // wired below the kind-coherence gates) on the typed Caixa
632        // surface. `:descricao` is the universal free-form-prose
633        // summary axis every kind carries (universal `Option<String>`
634        // slot on [`Caixa`]) and routes through two load-bearing
635        // [`caixa-helm`] consumers: `build_chart_yaml` folds it
636        // verbatim into the rendered `lareira-<nome>` Helm chart's
637        // `Chart.yaml` `description:` field
638        // (`caixa-helm/src/lib.rs:232-235`), and `build_readme` folds
639        // it verbatim into the chart `README.md` header
640        // (`caixa-helm/src/lib.rs:333-336`). Both consumers use
641        // `Option::unwrap_or_else(|| <fallback>)` to substitute a
642        // `caixa.nome`-derived placeholder when the slot is absent
643        // (`None` → the fallback fires); a `Some("")` *skips the
644        // fallback* and silently passes the empty string through to
645        // `Chart.yaml description: ""` / a blank `README.md` header
646        // — exact same footgun shape as the peer `:repositorio`
647        // surface above. Until this wire-up landed `:descricao` had
648        // no shape gate at any layer — the empty
649        // (`(:descricao "")` — the canonical paste-from-blank-doc
650        // footgun) silently landed in the rendered artifacts and
651        // broke at `helm lint` time (`WARNING [chart.metadata.description]:
652        // description is required` on `apiVersion: v2` charts) far
653        // from the source `caixa.lisp`.
654        //
655        // Runs *after* the peer universal `:nome` / `:versao` /
656        // `:deps` / `:etiquetas` / `:autores` / `:repositorio` gates
657        // (the gate order follows the canonical identity-axis-first
658        // cascade the peer gates establish; `:descricao` is the
659        // universal free-form-prose axis — it sits adjacent to
660        // `:repositorio` in the cascade after the load-bearing
661        // identity + dep trio + the two Vec-shaped universal
662        // metadata axes + the universal git-URL axis) and *before*
663        // the kind-coherence gates ([`Self::MeshSlotsOnNonAplicacao`]
664        // / [`Self::SupervisorSlotsOnNonSupervisor`] /
665        // [`Self::ServicoSlotsOnNonServico`] /
666        // [`Self::ForeignCodeSlot`]) — `:descricao` is universal so
667        // its shape diagnostic is more fundamental than the kind-
668        // coherence partitions on kind-exclusive slot sets.
669        //
670        // Same per-axis `*Violation { caixa, issue }` envelope every
671        // peer per-axis wrap exposes ([`Self::NomeViolation`] /
672        // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
673        // aa77d0f, [`Self::EtiquetasViolation`] 360a499,
674        // [`Self::AutoresViolation`] 86c769b,
675        // [`Self::RepositorioViolation`] 577b0a9,
676        // [`Self::CodePathViolation`] b868442,
677        // [`Self::RestartWindowViolation`] 10e321a). Threads
678        // [`ManifestError::DescricaoEmpty`] Display through verbatim
679        // — the per-arm reason already names the offending
680        // `:descricao` slot + cites the renderer-side footgun, so
681        // the wrap envelope's `issue` carries a self-locating
682        // "which axis, why" without re-shaping the per-arm reason.
683        caixa.run_layout_gate(Caixa::validate_descricao, LayoutError::descricao_violation)?;
684
685        // `:licenca` non-empty shape gate. The eighth universal-axis
686        // Caixa-level value-shape gate (peer of [`Caixa::validate_nome`]
687        // / [`Caixa::validate_versao`] / [`Caixa::validate_deps`] /
688        // [`Caixa::validate_etiquetas`] / [`Caixa::validate_autores`] /
689        // [`Caixa::validate_repositorio`] / [`Caixa::validate_descricao`]
690        // wired immediately above and [`Caixa::validate_code_paths`]
691        // wired below the kind-coherence gates) on the typed Caixa
692        // surface. `:licenca` is the universal SPDX-shaped license-
693        // expression axis every kind carries (universal `Option<String>`
694        // slot on [`Caixa`]) and routes through one load-bearing
695        // [`caixa-helm`] consumer: `build_readme` folds it verbatim into
696        // the rendered `lareira-<nome>` Helm chart's `README.md` `##
697        // License` section (`caixa-helm/src/lib.rs:361`) via
698        // `caixa.licenca.clone().unwrap_or_else(|| "MIT".into())`. The
699        // consumer's fallback only fires when the slot is absent (`None`
700        // → the `MIT` fallback fires); a `Some("")` *skips the
701        // fallback* and silently passes the empty string through to a
702        // chart `README.md` whose `License` section renders as a bare
703        // trailing period — exact same footgun shape as the peer
704        // `:repositorio` (577b0a9) and `:descricao` (4e6db38) surfaces
705        // above. Until this wire-up landed `:licenca` had no shape
706        // gate at any layer — the empty (`(:licenca "")` — the
707        // canonical paste-from-blank-doc footgun) silently landed in
708        // the rendered chart `README.md` far from the source
709        // `caixa.lisp`.
710        //
711        // Runs *after* the peer universal `:nome` / `:versao` /
712        // `:deps` / `:etiquetas` / `:autores` / `:repositorio` /
713        // `:descricao` gates (the gate order follows the canonical
714        // identity-axis-first cascade the peer gates establish;
715        // `:licenca` sits adjacent to `:descricao` in the cascade
716        // after the load-bearing identity + dep trio + the two
717        // Vec-shaped universal metadata axes + the universal
718        // git-URL + free-form-prose axes) and *before* the kind-
719        // coherence gates ([`Self::MeshSlotsOnNonAplicacao`] /
720        // [`Self::SupervisorSlotsOnNonSupervisor`] /
721        // [`Self::ServicoSlotsOnNonServico`] /
722        // [`Self::ForeignCodeSlot`]) — `:licenca` is universal so
723        // its shape diagnostic is more fundamental than the kind-
724        // coherence partitions on kind-exclusive slot sets.
725        //
726        // Same per-axis `*Violation { caixa, issue }` envelope every
727        // peer per-axis wrap exposes ([`Self::NomeViolation`] /
728        // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
729        // aa77d0f, [`Self::EtiquetasViolation`] 360a499,
730        // [`Self::AutoresViolation`] 86c769b,
731        // [`Self::RepositorioViolation`] 577b0a9,
732        // [`Self::DescricaoViolation`] 4e6db38,
733        // [`Self::CodePathViolation`] b868442,
734        // [`Self::RestartWindowViolation`] 10e321a). Threads
735        // [`ManifestError::LicencaEmpty`] Display through verbatim
736        // — the per-arm reason already names the offending
737        // `:licenca` slot + cites the renderer-side footgun, so
738        // the wrap envelope's `issue` carries a self-locating
739        // "which axis, why" without re-shaping the per-arm reason.
740        caixa.run_layout_gate(Caixa::validate_licenca, LayoutError::licenca_violation)?;
741
742        // `:edicao` non-empty shape gate. The ninth (and last
743        // un-gated) universal-axis Caixa-level value-shape gate
744        // (peer of [`Caixa::validate_nome`] / [`Caixa::validate_versao`]
745        // / [`Caixa::validate_deps`] / [`Caixa::validate_etiquetas`] /
746        // [`Caixa::validate_autores`] / [`Caixa::validate_repositorio`]
747        // / [`Caixa::validate_descricao`] / [`Caixa::validate_licenca`]
748        // wired immediately above and [`Caixa::validate_code_paths`]
749        // wired below the kind-coherence gates) on the typed Caixa
750        // surface. `:edicao` is the universal language-edition axis
751        // every kind carries (universal `Option<String>` slot on
752        // [`Caixa`]) that selects the tatara-lisp macro surface +
753        // compatibility flags the substrate applies when building
754        // the caixa. The canonical [`Caixa::template`] scaffold every
755        // `feira init` emits carries `:edicao "2026"` verbatim
756        // (`caixa-core/src/manifest.rs:1193`) and every renderer-side
757        // fixture carries `edicao: Some("2026".into())` by
758        // construction (`caixa-helm/src/lib.rs:375`,
759        // `caixa-flux/src/lib.rs:445`, `caixa-mesh/src/lib.rs:629`,
760        // `caixa-core/src/render.rs:2510`). Until this wire-up landed
761        // `:edicao` had no shape gate at any layer — the empty
762        // (`(:edicao "")` — the canonical paste-from-blank-doc
763        // footgun) silently landed as a bare `(:edicao "")` line
764        // in the rendered caixa.lisp and a future renderer-side
765        // consumer that folds the value through
766        // `Option::unwrap_or_else` would skip its fallback (which
767        // only fires on `None`) and pass the empty edition through
768        // to the substrate's build-time edition selector far from
769        // the source `caixa.lisp` — exact same
770        // `Some("")`-skips-`unwrap_or_else` footgun shape as the
771        // peer `:repositorio` (577b0a9), `:descricao` (4e6db38),
772        // and `:licenca` (3d1e535) surfaces above.
773        //
774        // Runs *after* the peer universal `:nome` / `:versao` /
775        // `:deps` / `:etiquetas` / `:autores` / `:repositorio` /
776        // `:descricao` / `:licenca` gates (the gate order follows
777        // the canonical identity-axis-first cascade the peer gates
778        // establish; `:edicao` sits at the tail of the cascade
779        // after the load-bearing identity + dep trio + the two
780        // Vec-shaped universal metadata axes + the three universal
781        // `Option<String>` chart-metadata axes) and *before* the
782        // kind-coherence gates ([`Self::MeshSlotsOnNonAplicacao`] /
783        // [`Self::SupervisorSlotsOnNonSupervisor`] /
784        // [`Self::ServicoSlotsOnNonServico`] /
785        // [`Self::ForeignCodeSlot`]) — `:edicao` is universal so
786        // its shape diagnostic is more fundamental than the kind-
787        // coherence partitions on kind-exclusive slot sets.
788        //
789        // Same per-axis `*Violation { caixa, issue }` envelope every
790        // peer per-axis wrap exposes ([`Self::NomeViolation`] /
791        // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
792        // aa77d0f, [`Self::EtiquetasViolation`] 360a499,
793        // [`Self::AutoresViolation`] 86c769b,
794        // [`Self::RepositorioViolation`] 577b0a9,
795        // [`Self::DescricaoViolation`] 4e6db38,
796        // [`Self::LicencaViolation`] 3d1e535,
797        // [`Self::CodePathViolation`] b868442,
798        // [`Self::RestartWindowViolation`] 10e321a). Threads
799        // [`ManifestError::EdicaoEmpty`] Display through verbatim
800        // — the per-arm reason already names the offending
801        // `:edicao` slot + cites the renderer-side footgun, so
802        // the wrap envelope's `issue` carries a self-locating
803        // "which axis, why" without re-shaping the per-arm reason.
804        // With this gate every universal-axis `Option<String>`
805        // surface on the typed Caixa (`:repositorio` 577b0a9,
806        // `:descricao` 4e6db38, `:licenca` 3d1e535, `:edicao` here)
807        // now carries the same structural empty-arm gate by
808        // construction.
809        caixa.run_layout_gate(Caixa::validate_edicao, LayoutError::edicao_violation)?;
810
811        // Kind ↔ code-surface coherence on the three no-code kinds
812        // (Supervisor / Aplicacao / Acao) — folded onto one substrate
813        // primitive at [`crate::Caixa::validate_no_code_kind_coherence`].
814        // Pre-lift each of the three arms lived as a self-similar
815        // `if caixa.kind().is_<no-code-kind>() && has_code { return
816        // Err(LayoutError::<kind>_owns_code(caixa)); }` block at this
817        // call site — three consumers, three identical shapes, one
818        // substrate primitive on [`Caixa`] closing the duplication the
819        // PRIME DIRECTIVE names as a bug. Mirror of the sibling
820        // [`crate::Caixa::validate_kind_slot_coherence`] fold f0d286e
821        // on the author-time typed-slot coherence axis: this wire-up
822        // closes the same three-arm cascade on the reciprocal
823        // code-surface axis, so the layout pipeline now routes both
824        // "no-code kind declares typed slots" and "no-code kind
825        // declares code" author-time footgun families through one
826        // substrate primitive per axis rather than six open-coded
827        // blocks. Each of the three inner ctors
828        // ([`crate::LayoutError::supervisor_owns_code`] /
829        // [`crate::LayoutError::aplicacao_owns_code`] /
830        // [`crate::LayoutError::acao_owns_code`]) was already lifted
831        // onto the substrate by the peer [`layout_nome_only_ctors!`]
832        // macro, so the primitive routes through the same
833        // `Self::<variant>(caixa.nome().to_string())` tuple-literal
834        // wrap per arm as the pre-lift open-coded blocks. Runs
835        // BEFORE the path-existence loops below so a no-code kind
836        // that declares code surfaces the self-locating OwnsCode
837        // diagnostic naming the offending kind rather than a
838        // downstream `MissingEntry` / `ExeOutsideDir` /
839        // `ServicoOutsideDir` against the resolved path far from the
840        // source `caixa.lisp`.
841        caixa.validate_no_code_kind_coherence()?;
842
843        // Kind ↔ typed-slot coherence on the M3 mesh / supervisor-tree /
844        // M2 Servico-runtime slot families — folded onto one substrate
845        // primitive at [`crate::Caixa::validate_kind_slot_coherence`].
846        // Pre-lift each of the three arms lived as a self-similar
847        // five-line `if !caixa.kind().is_<owner>() { let slots =
848        // caixa.declared_<family>_slots(); if !slots.is_empty() { return
849        // Err(LayoutError::<family>_on_non_<owner>(caixa, slots)); } }`
850        // block at this call site — three consumers, three identical
851        // shapes, one substrate primitive on [`Caixa`] closing the
852        // duplication the PRIME DIRECTIVE names as a bug. The primitive
853        // preserves the pre-fold canonical diagnostic order — mesh →
854        // supervisor → servico — pinned by the load-bearing
855        // `validate_kind_slot_coherence_mesh_arm_fires_before_supervisor_arm`
856        // / `_supervisor_arm_fires_before_servico_arm` ordering pins at
857        // caixa-core/src/manifest.rs, so this wire-up is byte-for-byte
858        // equivalent to the pre-fold three-block cascade on every fixture
859        // exercising any of the three arms. Peer with the per-slot
860        // compound entry gates the substrate already carries
861        // ([`crate::Caixa::validate_deps`] b5dd55e,
862        // [`crate::Caixa::validate_limits`] baa4688,
863        // [`crate::Caixa::validate_behavior`] 0d2877a,
864        // [`crate::Caixa::validate_upgrade_from`] d6801df,
865        // [`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
866        // [`crate::Caixa::validate_supervisor_shape`] 4c70105,
867        // [`crate::Caixa::validate_acao_shape`] 5d6df54) — the
868        // author-time gate axis on the per-slot algebra now shares one
869        // substrate primitive per compound gate, and this lift closes
870        // the symmetric axis on the cross-family kind ↔ slot coherence
871        // algebra so the layout pipeline routes the three self-similar
872        // gates through one substrate primitive rather than three
873        // open-coded blocks. The sibling
874        // [`LayoutError::ForeignCodeSlot`] (code-surface family) and
875        // [`LayoutError::CiOnNonAcao`] (`:ci` axis) gates stay
876        // open-coded downstream: the former bakes the kind-check into
877        // its declared_*_slots helper by design (so it carries no outer
878        // `if !kind().is_<owner>()` guard), the latter carries a
879        // distinct `{ caixa, kind }` wrap shape (no `slots` field —
880        // `:ci` is a single `Option`, not a `Vec`-of-named-slots) whose
881        // reshape onto the uniform `{ caixa, kind, slots }` envelope a
882        // future symmetry lift can join here.
883        caixa.validate_kind_slot_coherence()?;
884
885        // Kind ↔ `:ci` coherence (mirror of the three arms above on
886        // the M3 mesh / supervisor-tree / M2 Servico-runtime slot
887        // families, CANTEIRO §7.1-C, folded onto one substrate
888        // primitive at [`crate::Caixa::validate_ci_kind_coherence`]):
889        // `:ci` carries a typed CI run — a
890        // [`canteiro_types::CiRun`] — that only the caixa-actions
891        // renderer decomposes + validates, and only for a `:kind
892        // Acao`. On any *other* kind a declared `:ci` is the
893        // manifest field's documented "ignored otherwise": it
894        // silently passes verify and then vanishes (never
895        // decomposed, never rendered), far from the source
896        // `caixa.lisp`. Pre-lift the arm lived as a self-similar
897        // `if caixa.ci().is_some() && !caixa.kind().is_acao() { …
898        // return Err(LayoutError::CiOnNonAcao { … }); }` block at
899        // this call site — one consumer today but every future
900        // consumer that wanted to gate this coherence axis as a
901        // unit was structurally forced to re-inline the two-
902        // condition guard in lockstep with this wire-up (the
903        // duplication the PRIME DIRECTIVE names as a bug).
904        // Post-fold the arm reads through one call, and the
905        // [`crate::LayoutError::CiOnNonAcao`] envelope (with its
906        // distinct `{ caixa, kind }` wrap shape — no `slots`
907        // field, because `:ci` is a single `Option` not a `Vec`-
908        // of-named-slots) surfaces byte-for-byte equivalent to the
909        // pre-fold open-coded block, pinned by the paired
910        // `validate_ci_kind_coherence_folds_arm_matches_gate`
911        // per-arm equivalence pin and the
912        // `validate_ci_kind_coherence_accepts_acao_on_every_ci_shape`
913        // /
914        // `validate_ci_kind_coherence_accepts_absent_ci_on_every_kind`
915        // identity-element pins in the
916        // [`crate::Caixa::validate_ci_kind_coherence`] pin family
917        // (`manifest.rs`). Peer to the sibling three-arm
918        // [`crate::Caixa::validate_kind_slot_coherence`] fold
919        // f0d286e: that primitive carries the M3 / supervisor-tree
920        // / M2 axes under a uniform `{ caixa, kind, slots }`
921        // envelope, this primitive carries the `:ci` axis under
922        // its distinct `{ caixa, kind }` envelope, and every
923        // author-time kind ↔ slot coherence diagnostic at the
924        // layout altitude now routes through one substrate
925        // primitive per envelope shape rather than an open-coded
926        // block.
927        caixa.validate_ci_kind_coherence()?;
928
929        // Kind ↔ slot coherence on the fourth and final axis — the
930        // code-surface slot set (the trio M2/Supervisor/Aplicacao gates
931        // above close on the M2 runtime, supervisor-tree, and M3 mesh
932        // axes; this gate closes the symmetric "kind owns this code
933        // shape" relation on `:exe` + `:servicos`). `:exe` is the nix-
934        // built executable surface owned only by Binario; `:servicos`
935        // is the wasm-component daemon surface owned only by Servico.
936        // The caixa-helm / caixa-flux / caixa-flake renderers gate on
937        // `require_kind(_, <owning-kind>)` and only emit the slot for
938        // its owning kind — so on any *other* code-running kind a
939        // declared `:exe` / `:servicos` is the manifest field's
940        // documented "ignored otherwise" (see the field docs on
941        // `Caixa::exe` + `Caixa::servicos`): the path is validated by
942        // the per-kind path-existence loops below, but the value is
943        // never rendered into a build target or programs.yaml entry —
944        // it silently passes `feira build` and then vanishes, far from
945        // the source caixa.lisp.
946        //
947        // Reject it here — beside the M2/Supervisor/Aplicacao slot
948        // gates, after the `SupervisorOwnsCode` / `AplicacaoOwnsCode`
949        // OwnCode gates which dominate on those two no-code kinds (a
950        // Supervisor / Aplicacao with any of `:bibliotecas` / `:exe` /
951        // `:servicos` surfaces the OwnCode diagnostic first), and
952        // before the path-existence loops which would otherwise spend
953        // a less-helpful `MissingEntry` diagnostic on the foreign
954        // slot's path. `declared_foreign_code_slots` is the single
955        // typed source of the foreign-code-slot set + its canonical
956        // diagnostic order (`:exe` → `:servicos`).
957        //
958        // Mirrors the 9d37f98 / 510c00a / 760a430 kind ↔ slot
959        // coherence trio's "declared-but-inert" footgun closure on the
960        // M2 / supervisor-tree / M3 axes, now extended onto the code-
961        // surface axis — every code-running kind's exclusive code
962        // surface is structurally fenced from every other code-running
963        // kind. `:bibliotecas` is deliberately excluded from the foreign
964        // set on Binario / Servico (a `lib/` helper bundled into the
965        // nix flake's build or the wasm-component's source tree is a
966        // legitimate cross-kind authoring shape); on Biblioteca it is
967        // the native slot, and on Supervisor / Aplicacao the OwnCode
968        // gates above already close it.
969        //
970        // Folded onto [`crate::Caixa::validate_foreign_code_kind_coherence`]
971        // — the fourth and last kind ↔ slot coherence primitive on the
972        // typed [`Caixa`] surface, peer of
973        // [`crate::Caixa::validate_kind_slot_coherence`] (f0d286e,
974        // the cross-family M3/supervisor/M2 fold on the sibling
975        // `{ caixa, kind, slots }` envelope),
976        // [`crate::Caixa::validate_no_code_kind_coherence`] (3bbf6a2,
977        // the reciprocal no-code-kind fold on `SupervisorOwnsCode` /
978        // `AplicacaoOwnsCode` / `AcaoOwnsCode`), and
979        // [`crate::Caixa::validate_ci_kind_coherence`] (9b55beb, the
980        // `:ci` axis fold on the `{ caixa, kind }` envelope). With
981        // this lift every kind-coherence axis at the layout altitude
982        // routes through one substrate primitive per axis rather than
983        // an open-coded block, closing the last open-coded gap the
984        // sibling [`crate::Caixa::validate_kind_slot_coherence`]
985        // doc-comment's "ForeignCodeSlot … stays open-coded downstream"
986        // note flagged.
987        caixa.validate_foreign_code_kind_coherence()?;
988
989        // Per-entry path-shape gate on the three Caixa-level code-surface
990        // path lists (`:bibliotecas`, `:exe`, `:servicos`): each entry must
991        // be non-empty, relative, and free of `..` components — the same
992        // [`crate::render::is_sandboxed_relative_path`] discipline the
993        // peer `:behavior :on-*` (b0c8389) and
994        // `:upgrade-from :state-change :script` (26da2c7) axes already
995        // route through. Runs *after* the kind-coherence gates above (so
996        // a `:exe` on a Servico surfaces ForeignCodeSlot rather than a
997        // per-entry shape diagnostic, and a Supervisor/Aplicacao with any
998        // code surface surfaces OwnCode first) and *before* the existence
999        // loops below (so an empty / absolute / parent-escaping entry
1000        // surfaces its self-locating per-slot diagnostic rather than a
1001        // downstream `MissingEntry` / `ExeOutsideDir` /
1002        // `ServicoOutsideDir` against the resolved sandbox-escape path).
1003        caixa.run_layout_gate(Caixa::validate_code_paths, LayoutError::code_path_violation)?;
1004
1005        if caixa.kind().requires_lib() && caixa.bibliotecas().is_empty() {
1006            let expected = root
1007                .join(crate::render::LAYOUT_DIR_LIB)
1008                .join(format!("{}.lisp", caixa.nome()));
1009            if !self.exists(&expected) {
1010                return Err(LayoutError::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::MissingEntry`] naming the missing
1866    /// declared entry at `path` under the canonical `kind` label from
1867    /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`] /
1868    /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] /
1869    /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] /
1870    /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`] /
1871    /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT`].
1872    /// Folds the uniform `{ kind, path }` two-slot construction onto one
1873    /// substrate primitive so every [`StandardLayout::verify`] wire-up
1874    /// on this variant reads through one dispatch rather than the
1875    /// pre-lift open-coded struct-literal block.
1876    #[must_use]
1877    pub fn missing_entry(kind: &'static str, path: PathBuf) -> Self {
1878        Self::MissingEntry { kind, path }
1879    }
1880
1881    /// Construct a [`LayoutError::MissingLib`] naming the offending
1882    /// `caixa.nome()` and the resolved fallback `expected` path.
1883    ///
1884    /// Folds the uniform `Self::MissingLib { caixa: caixa.nome()
1885    /// .to_string(), expected }` two-slot struct-literal onto one
1886    /// substrate primitive so every [`StandardLayout::verify`] wire-up
1887    /// on this variant reads through one dispatch rather than the
1888    /// pre-lift open-coded struct-literal block, projecting the caixa
1889    /// slot through the paired [`crate::Caixa::nome`] accessor — the
1890    /// same discipline the sibling [`Self::missing_entry`]
1891    /// `{ kind, path }` ctor and the [`layout_nome_only_ctors!`]
1892    /// `{ caixa }`-only tuple-variant ctor family already take on the
1893    /// same envelope.
1894    #[must_use]
1895    pub fn missing_lib(caixa: &crate::Caixa, expected: PathBuf) -> Self {
1896        Self::MissingLib {
1897            caixa: caixa.nome().to_string(),
1898            expected,
1899        }
1900    }
1901
1902    /// Construct a [`LayoutError::CiOnNonAcao`] naming the offending
1903    /// `caixa.nome()` and its declared `:kind`.
1904    ///
1905    /// Folds the uniform `Self::CiOnNonAcao { caixa: caixa.nome()
1906    /// .to_string(), kind: caixa.kind() }` two-slot struct-literal
1907    /// onto one substrate primitive so every wire-up on this variant
1908    /// (today the sole [`crate::Caixa::validate_ci_kind_coherence`]
1909    /// call site at `caixa-core/src/manifest.rs:5586` — the deferred
1910    /// `caixa.pleme.io/v1alpha1/Caixa` CR admission webhook and a
1911    /// future `feira validate --ci-kind` per-caixa verb sit at the
1912    /// same axis) reads through one dispatch rather than the pre-lift
1913    /// open-coded struct-literal block. Both slots project through
1914    /// the paired [`crate::Caixa::nome`] / [`crate::Caixa::kind`]
1915    /// accessors — the same discipline every sibling ctor on this
1916    /// envelope ([`Self::missing_lib`] on `{ caixa, expected }`, the
1917    /// [`layout_nome_only_ctors!`] family on `{ caixa }`-only
1918    /// tuple-variants, the [`layout_slot_kind_ctors!`] family on
1919    /// `{ caixa, kind, slots }`) already takes.
1920    #[must_use]
1921    pub fn ci_on_non_acao(caixa: &crate::Caixa) -> Self {
1922        Self::CiOnNonAcao {
1923            caixa: caixa.nome().to_string(),
1924            kind: caixa.kind(),
1925        }
1926    }
1927}
1928
1929// Fold the six `LayoutError::<Variant>(caixa.nome().to_string())` nome-
1930// only tuple-variant wire-up sites at [`StandardLayout::verify`] onto one
1931// substrate primitive per typed variant — the fourth uniform-shape
1932// envelope on `LayoutError` after the `{ caixa, issue }` family the
1933// [`layout_violation_ctors!`] macro closed (131ca0d), the
1934// `{ caixa, kind, slots }` family the peer [`layout_slot_kind_ctors!`]
1935// macro closed (0419438), and the `{ kind, path }`
1936// [`LayoutError::missing_entry`] one-variant ctor (1b09f9d). Each of the
1937// six wire-up sites on this shape (`SupervisorOwnsCode` /
1938// `AplicacaoOwnsCode` / `AcaoOwnsCode` at the no-code kind-coherence gate;
1939// `BinarioWithoutExe` / `ServicoWithoutServicos` / `MissingCi` at the
1940// required-slot gate) opened the identical one-line
1941// `LayoutError::<Variant>(caixa.nome().to_string())` tuple-literal — the
1942// exact "same block re-inlined at every consumer" shape the PRIME
1943// DIRECTIVE names as a bug, on the same altitude the peer `_violation` /
1944// `_slots_on_non_*` / `missing_entry` families each closed on the
1945// sibling `LayoutError` envelopes.
1946//
1947// The macro below generates one static constructor per variant of shape
1948// `fn <slot>(caixa: &Caixa) -> LayoutError`, so every wire-up site
1949// collapses onto one dispatch:
1950// `return Err(LayoutError::<slot>(caixa));`, byte-equal to the pre-lift
1951// tuple-literal. The uniform one-field construction (`caixa: caixa.nome()
1952// .to_string()`) is spelled once — inside the macro — rather than at
1953// every wire-up site. Every constructor is `#[must_use]` so a caller who
1954// mistakenly discards the constructed error (rather than routing it
1955// through `return Err(…)`) trips a compile warning at the wire-up site.
1956//
1957// Peer with the three prior `LayoutError`-envelope constructor families
1958// — the four together now fold every uniform-shape `LayoutError` variant
1959// carried by [`StandardLayout::verify`] onto one substrate primitive per
1960// typed variant, so every layout-side error-wrap on `LayoutError` reads
1961// through one dispatch per variant rather than N open-coded blocks.
1962// Every future consumer that wants to construct one of these variants
1963// outside the layout pipeline (a per-slot admission webhook probing an
1964// Acao's `:ci` slot, a `feira validate --kind <X>` verb refusing a
1965// no-code kind that declares `:bibliotecas` / `:exe` / `:servicos`, an
1966// overlay resolver rejecting a required-slot omission against a
1967// cluster-local snapshot) reaches its variant through one call, matching
1968// the `_violation` / `_slots_on_non_*` / `missing_entry` families'
1969// substrate-primitive discipline.
1970macro_rules! layout_nome_only_ctors {
1971    ($($ctor:ident => $variant:ident),* $(,)?) => {
1972        impl LayoutError {
1973            $(
1974                #[doc = concat!(
1975                    "Construct a [`LayoutError::",
1976                    stringify!($variant),
1977                    "`] naming the offending `caixa.nome()`. Folds the ",
1978                    "uniform `Self::",
1979                    stringify!($variant),
1980                    "(caixa.nome().to_string())` one-field tuple-",
1981                    "literal onto one substrate primitive so every ",
1982                    "[`StandardLayout::verify`] wire-up on this variant ",
1983                    "reads through one dispatch rather than the pre-lift ",
1984                    "open-coded block."
1985                )]
1986                #[must_use]
1987                pub fn $ctor(caixa: &crate::Caixa) -> Self {
1988                    Self::$variant(caixa.nome().to_string())
1989                }
1990            )*
1991        }
1992    };
1993}
1994
1995layout_nome_only_ctors! {
1996    binario_without_exe => BinarioWithoutExe,
1997    servico_without_servicos => ServicoWithoutServicos,
1998    missing_ci => MissingCi,
1999    supervisor_owns_code => SupervisorOwnsCode,
2000    aplicacao_owns_code => AplicacaoOwnsCode,
2001    acao_owns_code => AcaoOwnsCode,
2002}
2003
2004#[cfg(test)]
2005mod tests {
2006    use super::*;
2007    use crate::{Caixa, CaixaKind};
2008    use std::path::PathBuf;
2009
2010    fn caixa(kind: CaixaKind) -> Caixa {
2011        Caixa {
2012            nome: "demo".into(),
2013            versao: "0.1.0".into(),
2014            kind,
2015            edicao: None,
2016            descricao: None,
2017            repositorio: None,
2018            licenca: None,
2019            autores: vec![],
2020            etiquetas: vec![],
2021            deps: vec![],
2022            deps_dev: vec![],
2023            exe: vec![],
2024            bibliotecas: vec![],
2025            servicos: vec![],
2026            // M2 typed-substrate slots default to absent.
2027            limits: None,
2028            behavior: None,
2029            upgrade_from: vec![],
2030            estrategia: None,
2031            max_restarts: None,
2032            restart_window: None,
2033            children: vec![],
2034            // M3 Aplicacao slots default to absent.
2035            membros: vec![],
2036            contratos: vec![],
2037            politicas: None,
2038            placement: None,
2039            entrada: None,
2040            ci: None,
2041        }
2042    }
2043
2044    #[test]
2045    fn missing_manifest_errors() {
2046        let layout = StandardLayout::new().with_path_exists(|_| false);
2047        let err = layout
2048            .verify(&caixa(CaixaKind::Biblioteca), Path::new("/tmp/x"))
2049            .unwrap_err();
2050        assert!(matches!(err, LayoutError::MissingManifest(_)));
2051    }
2052
2053    // ── LayoutError::*_violation constructor family ──────────────────────
2054    //
2055    // The [`layout_violation_ctors!`] macro (below the `LayoutError` enum
2056    // definition) generates one static constructor per `*Violation { caixa,
2057    // issue }` variant that folds the uniform `{ caixa: caixa.nome()
2058    // .to_string(), issue: err.to_string() }` two-slot construction onto
2059    // one substrate primitive. The per-variant equivalence pins below
2060    // (fail-before-pass-after by construction — a byte-mismatched macro
2061    // arm would trip its equivalence pin first) lock each generated
2062    // constructor to its struct-literal peer under `PartialEq`, so every
2063    // wire-up in [`StandardLayout::verify`] on that variant produces a
2064    // byte-equal `LayoutError` to the pre-lift open-coded block. The
2065    // fixture caixa fires under `caixa("demo")` so the `caixa: "demo"`
2066    // half is pinned; the fixture error fires under a fixed `&str` so the
2067    // `issue: <literal>` half is pinned; the two together pin every field
2068    // of every generated variant.
2069
2070    fn layout_violation_ctor_fixture() -> (Caixa, &'static str) {
2071        (caixa(CaixaKind::Biblioteca), "sample issue text")
2072    }
2073
2074    // `assert_eq!` uses `PartialEq::eq(&self, &other)` under the hood, so
2075    // `actual`/`expected` are only ever read, never moved into anything —
2076    // the ergonomic tradeoff (owned + move-in vs. reference + &-borrow at
2077    // every call site) favors the owned form for a test-only assertion
2078    // helper called from 17 wire-up pins. The lint targets the general
2079    // API-shape case where callers still have downstream uses for the
2080    // moved value; the assertion helper terminates on the equality check.
2081    #[allow(clippy::needless_pass_by_value)]
2082    fn assert_violation_ctor_matches(actual: LayoutError, expected: LayoutError) {
2083        assert_eq!(
2084            actual, expected,
2085            "generated constructor must produce byte-equal LayoutError to open-coded struct-literal wrap",
2086        );
2087    }
2088
2089    #[test]
2090    fn nome_violation_ctor_matches_struct_literal_wrap() {
2091        let (c, issue) = layout_violation_ctor_fixture();
2092        assert_violation_ctor_matches(
2093            LayoutError::nome_violation(&c, issue),
2094            LayoutError::NomeViolation {
2095                caixa: c.nome().to_string(),
2096                issue: issue.to_string(),
2097            },
2098        );
2099    }
2100
2101    #[test]
2102    fn versao_violation_ctor_matches_struct_literal_wrap() {
2103        let (c, issue) = layout_violation_ctor_fixture();
2104        assert_violation_ctor_matches(
2105            LayoutError::versao_violation(&c, issue),
2106            LayoutError::VersaoViolation {
2107                caixa: c.nome().to_string(),
2108                issue: issue.to_string(),
2109            },
2110        );
2111    }
2112
2113    #[test]
2114    fn deps_violation_ctor_matches_struct_literal_wrap() {
2115        let (c, issue) = layout_violation_ctor_fixture();
2116        assert_violation_ctor_matches(
2117            LayoutError::deps_violation(&c, issue),
2118            LayoutError::DepsViolation {
2119                caixa: c.nome().to_string(),
2120                issue: issue.to_string(),
2121            },
2122        );
2123    }
2124
2125    #[test]
2126    fn etiquetas_violation_ctor_matches_struct_literal_wrap() {
2127        let (c, issue) = layout_violation_ctor_fixture();
2128        assert_violation_ctor_matches(
2129            LayoutError::etiquetas_violation(&c, issue),
2130            LayoutError::EtiquetasViolation {
2131                caixa: c.nome().to_string(),
2132                issue: issue.to_string(),
2133            },
2134        );
2135    }
2136
2137    #[test]
2138    fn autores_violation_ctor_matches_struct_literal_wrap() {
2139        let (c, issue) = layout_violation_ctor_fixture();
2140        assert_violation_ctor_matches(
2141            LayoutError::autores_violation(&c, issue),
2142            LayoutError::AutoresViolation {
2143                caixa: c.nome().to_string(),
2144                issue: issue.to_string(),
2145            },
2146        );
2147    }
2148
2149    #[test]
2150    fn repositorio_violation_ctor_matches_struct_literal_wrap() {
2151        let (c, issue) = layout_violation_ctor_fixture();
2152        assert_violation_ctor_matches(
2153            LayoutError::repositorio_violation(&c, issue),
2154            LayoutError::RepositorioViolation {
2155                caixa: c.nome().to_string(),
2156                issue: issue.to_string(),
2157            },
2158        );
2159    }
2160
2161    #[test]
2162    fn descricao_violation_ctor_matches_struct_literal_wrap() {
2163        let (c, issue) = layout_violation_ctor_fixture();
2164        assert_violation_ctor_matches(
2165            LayoutError::descricao_violation(&c, issue),
2166            LayoutError::DescricaoViolation {
2167                caixa: c.nome().to_string(),
2168                issue: issue.to_string(),
2169            },
2170        );
2171    }
2172
2173    #[test]
2174    fn licenca_violation_ctor_matches_struct_literal_wrap() {
2175        let (c, issue) = layout_violation_ctor_fixture();
2176        assert_violation_ctor_matches(
2177            LayoutError::licenca_violation(&c, issue),
2178            LayoutError::LicencaViolation {
2179                caixa: c.nome().to_string(),
2180                issue: issue.to_string(),
2181            },
2182        );
2183    }
2184
2185    #[test]
2186    fn edicao_violation_ctor_matches_struct_literal_wrap() {
2187        let (c, issue) = layout_violation_ctor_fixture();
2188        assert_violation_ctor_matches(
2189            LayoutError::edicao_violation(&c, issue),
2190            LayoutError::EdicaoViolation {
2191                caixa: c.nome().to_string(),
2192                issue: issue.to_string(),
2193            },
2194        );
2195    }
2196
2197    #[test]
2198    fn code_path_violation_ctor_matches_struct_literal_wrap() {
2199        let (c, issue) = layout_violation_ctor_fixture();
2200        assert_violation_ctor_matches(
2201            LayoutError::code_path_violation(&c, issue),
2202            LayoutError::CodePathViolation {
2203                caixa: c.nome().to_string(),
2204                issue: issue.to_string(),
2205            },
2206        );
2207    }
2208
2209    #[test]
2210    fn limits_violation_ctor_matches_struct_literal_wrap() {
2211        let (c, issue) = layout_violation_ctor_fixture();
2212        assert_violation_ctor_matches(
2213            LayoutError::limits_violation(&c, issue),
2214            LayoutError::LimitsViolation {
2215                caixa: c.nome().to_string(),
2216                issue: issue.to_string(),
2217            },
2218        );
2219    }
2220
2221    #[test]
2222    fn behavior_violation_ctor_matches_struct_literal_wrap() {
2223        let (c, issue) = layout_violation_ctor_fixture();
2224        assert_violation_ctor_matches(
2225            LayoutError::behavior_violation(&c, issue),
2226            LayoutError::BehaviorViolation {
2227                caixa: c.nome().to_string(),
2228                issue: issue.to_string(),
2229            },
2230        );
2231    }
2232
2233    #[test]
2234    fn upgrade_violation_ctor_matches_struct_literal_wrap() {
2235        let (c, issue) = layout_violation_ctor_fixture();
2236        assert_violation_ctor_matches(
2237            LayoutError::upgrade_violation(&c, issue),
2238            LayoutError::UpgradeViolation {
2239                caixa: c.nome().to_string(),
2240                issue: issue.to_string(),
2241            },
2242        );
2243    }
2244
2245    #[test]
2246    fn restart_window_violation_ctor_matches_struct_literal_wrap() {
2247        let (c, issue) = layout_violation_ctor_fixture();
2248        assert_violation_ctor_matches(
2249            LayoutError::restart_window_violation(&c, issue),
2250            LayoutError::RestartWindowViolation {
2251                caixa: c.nome().to_string(),
2252                issue: issue.to_string(),
2253            },
2254        );
2255    }
2256
2257    #[test]
2258    fn supervisor_violation_ctor_matches_struct_literal_wrap() {
2259        let (c, issue) = layout_violation_ctor_fixture();
2260        assert_violation_ctor_matches(
2261            LayoutError::supervisor_violation(&c, issue),
2262            LayoutError::SupervisorViolation {
2263                caixa: c.nome().to_string(),
2264                issue: issue.to_string(),
2265            },
2266        );
2267    }
2268
2269    #[test]
2270    fn aplicacao_violation_ctor_matches_struct_literal_wrap() {
2271        let (c, issue) = layout_violation_ctor_fixture();
2272        assert_violation_ctor_matches(
2273            LayoutError::aplicacao_violation(&c, issue),
2274            LayoutError::AplicacaoViolation {
2275                caixa: c.nome().to_string(),
2276                issue: issue.to_string(),
2277            },
2278        );
2279    }
2280
2281    #[test]
2282    fn acao_violation_ctor_matches_struct_literal_wrap() {
2283        // Sibling of [`aplicacao_violation_ctor_matches_struct_literal_wrap`]
2284        // / [`supervisor_violation_ctor_matches_struct_literal_wrap`] on
2285        // the third per-kind compound-shape wrap envelope on
2286        // `LayoutError`. Pins the macro-generated `acao_violation`
2287        // constructor to its struct-literal peer under `PartialEq`, so
2288        // every wire-up in [`StandardLayout::verify`] on the
2289        // [`LayoutError::AcaoViolation`] variant produces a byte-equal
2290        // `LayoutError` to the pre-lift open-coded block. Closes the
2291        // pin family the peer per-kind shape wraps already carry.
2292        let (c, issue) = layout_violation_ctor_fixture();
2293        assert_violation_ctor_matches(
2294            LayoutError::acao_violation(&c, issue),
2295            LayoutError::AcaoViolation {
2296                caixa: c.nome().to_string(),
2297                issue: issue.to_string(),
2298            },
2299        );
2300    }
2301
2302    #[test]
2303    fn violation_ctor_routes_issue_through_display_impl() {
2304        // Pin the fold's `issue = err.to_string()` half against any type
2305        // implementing `Display` — a per-arm error type from a foreign
2306        // module (here, `std::io::Error`) threads through byte-equal to
2307        // the struct-literal `.to_string()` construction, so the fold
2308        // does not silently collapse onto `&str`-only inputs.
2309        let c = caixa(CaixaKind::Biblioteca);
2310        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "sample io display source");
2311        let expected_issue = io_err.to_string();
2312        let actual = LayoutError::deps_violation(&c, &io_err);
2313        assert_eq!(
2314            actual,
2315            LayoutError::DepsViolation {
2316                caixa: c.nome().to_string(),
2317                issue: expected_issue,
2318            },
2319        );
2320    }
2321
2322    #[test]
2323    fn violation_ctor_routes_caixa_prefix_through_nome_accessor() {
2324        // Pin the fold's `caixa = caixa.nome().to_string()` half against
2325        // a non-default `:nome` — the accessor threads the caller's
2326        // `:nome` verbatim into the wrap envelope, so the fold does not
2327        // silently collapse onto the default `"demo"` fixture nome.
2328        let mut c = caixa(CaixaKind::Biblioteca);
2329        c.nome = "alt-nome".into();
2330        let actual = LayoutError::behavior_violation(&c, "sample issue");
2331        assert_eq!(
2332            actual,
2333            LayoutError::BehaviorViolation {
2334                caixa: "alt-nome".to_string(),
2335                issue: "sample issue".to_string(),
2336            },
2337        );
2338    }
2339
2340    // ── Caixa::run_layout_gate — per-slot gate + LayoutError wrap fold ───
2341    //
2342    // The [`Caixa::run_layout_gate`] substrate primitive folds the 18
2343    // self-similar `caixa.validate_<slot>().map_err(|err| LayoutError::
2344    // <slot>_violation(caixa, err))?;` wire-up sites at
2345    // [`StandardLayout::verify`] onto one dispatch. The pins below
2346    // (fail-before-pass-after by construction — a silent regression that
2347    // de-folded either arm would trip its own pin first) lock the two
2348    // arms of the fold under `PartialEq`:
2349    //
2350    //   - The `Ok(())` identity-element arm passes through verbatim (no
2351    //     wrap runs, no `caixa` closure capture).
2352    //   - The `Err(E)` arm routes through the caller-supplied `wrap`
2353    //     ctor with `self` bound as the caixa slot, byte-equal to the
2354    //     pre-lift `.map_err(|err| CTOR(caixa, err))` closure.
2355    //
2356    // The third pin (per-arm equivalence) runs the primitive with the
2357    // canonical `Caixa::validate_nome` validator and the paired
2358    // `LayoutError::nome_violation` ctor on a fixture whose `:nome`
2359    // fails `validate_nome`'s DNS-1123 gate ("Bad_Nome" — the uppercase
2360    // + underscore double footgun) and asserts the primitive's
2361    // `Result<(), LayoutError>` matches the open-coded pre-lift cascade
2362    // on the same fixture. A silent regression that de-folded the wrap
2363    // (dropped the `self` binding, threaded a stale caixa nome, swapped
2364    // the wrap ctor) would surface here as a mismatch between the two
2365    // dispatches.
2366
2367    #[test]
2368    fn run_layout_gate_ok_arm_passes_through() {
2369        // Positive control on the identity-element arm: a gate returning
2370        // `Ok(())` short-circuits before the wrap runs, so the caller
2371        // receives `Ok(())` verbatim regardless of what the paired ctor
2372        // would have produced. Pins the fold's `Result::map_err`
2373        // short-circuit semantics — a regression that unconditionally
2374        // wrapped (e.g. always called the ctor) would trip here.
2375        let c = caixa(CaixaKind::Biblioteca);
2376        let result: Result<(), LayoutError> = c.run_layout_gate(
2377            |_c: &Caixa| Ok::<(), &'static str>(()),
2378            |_c: &Caixa, _err: &'static str| {
2379                panic!("wrap must not run on the Ok(()) arm of the fold")
2380            },
2381        );
2382        assert!(
2383            result.is_ok(),
2384            "run_layout_gate must pass Ok(()) through verbatim, got {result:?}"
2385        );
2386    }
2387
2388    #[test]
2389    fn run_layout_gate_err_arm_wraps_via_ctor() {
2390        // Positive control on the err arm: a gate returning `Err(E)`
2391        // threads `E` into the caller-supplied `wrap` ctor with `self`
2392        // bound as the caixa argument. Uses a synthetic `&'static str`
2393        // error and the canonical `LayoutError::deps_violation` ctor so
2394        // the pin covers the fold's two-callable dispatch shape without
2395        // depending on any per-slot validator's specific arm sweep.
2396        let c = caixa(CaixaKind::Biblioteca);
2397        let sample_reason = "sample gate reason";
2398        let result = c.run_layout_gate(
2399            |_c: &Caixa| Err::<(), &'static str>(sample_reason),
2400            LayoutError::deps_violation,
2401        );
2402        let err = result.expect_err("Err arm must reach the caller");
2403        assert_eq!(
2404            err,
2405            LayoutError::DepsViolation {
2406                caixa: c.nome().to_string(),
2407                issue: sample_reason.to_string(),
2408            },
2409            "run_layout_gate must wrap the gate's Err via the caller-supplied \
2410             ctor with `self` bound as the caixa slot"
2411        );
2412    }
2413
2414    #[test]
2415    fn run_layout_gate_folds_arm_matches_gate() {
2416        // Fail-before-pass-after per-arm equivalence pin on the err arm:
2417        // a fixture whose `:nome` fails `validate_nome`'s DNS-1123 gate
2418        // (`"Bad_Nome"` — uppercase + underscore double footgun the
2419        // [`crate::ManifestError::NomeInvalid`] arm rejects) surfaces
2420        // the same `LayoutError::NomeViolation` byte-equal through both
2421        // the primitive `Caixa::run_layout_gate(Caixa::validate_nome,
2422        // LayoutError::nome_violation)` and the open-coded pre-lift
2423        // cascade `Caixa::validate_nome().map_err(|err|
2424        // LayoutError::nome_violation(&c, err))` on the same Caixa
2425        // fixture. Pins the fold — a silent regression that de-folded
2426        // either arm (dropped the `self` binding, threaded a stale
2427        // caixa nome, swapped the wrap ctor) would surface here as a
2428        // mismatch between the two dispatches.
2429        let mut c = caixa(CaixaKind::Biblioteca);
2430        c.nome = "Bad_Nome".into();
2431        let via_primitive = c
2432            .run_layout_gate(Caixa::validate_nome, LayoutError::nome_violation)
2433            .expect_err("Bad_Nome must fail validate_nome");
2434        let via_open_coded = c
2435            .validate_nome()
2436            .map_err(|err| LayoutError::nome_violation(&c, err))
2437            .expect_err("Bad_Nome must fail validate_nome");
2438        assert_eq!(
2439            via_primitive, via_open_coded,
2440            "Caixa::run_layout_gate must surface the err-arm diagnostic \
2441             byte-equal to the open-coded `.map_err(|err| CTOR(caixa, err))` \
2442             cascade on the same Caixa fixture"
2443        );
2444        assert!(
2445            matches!(via_primitive, LayoutError::NomeViolation { .. }),
2446            "expected NomeViolation on Bad_Nome, got {via_primitive:?}"
2447        );
2448    }
2449
2450    // ── LayoutError kind-coherence constructor family ────────────────────
2451    //
2452    // The [`layout_slot_kind_ctors!`] macro (sibling of
2453    // [`layout_violation_ctors!`] beside the `LayoutError` enum definition)
2454    // generates one static constructor per `*SlotsOn*` / `ForeignCodeSlot`
2455    // variant that folds the uniform `{ caixa: caixa.nome().to_string(),
2456    // kind: caixa.kind(), slots: slots.join(" ") }` four-field construction
2457    // onto one substrate primitive. The per-variant equivalence pins below
2458    // (fail-before-pass-after by construction — a byte-mismatched macro arm
2459    // would trip its equivalence pin first) lock each generated constructor
2460    // to its struct-literal peer under `PartialEq`, so every wire-up in
2461    // [`StandardLayout::verify`] on that variant produces a byte-equal
2462    // `LayoutError` to the pre-lift open-coded block. The three cross-axis
2463    // pins that follow (non-default `:nome`, non-default kind, non-trivial
2464    // slots list) route each of the three constructor input axes through
2465    // its declared accessor / arg, so the fold does not silently collapse
2466    // onto a fixture default on any axis.
2467
2468    fn layout_slot_kind_ctor_fixture() -> (Caixa, Vec<&'static str>) {
2469        (caixa(CaixaKind::Biblioteca), vec![":membros", ":contratos"])
2470    }
2471
2472    // Same rationale as `assert_violation_ctor_matches` above: the helper
2473    // terminates on the equality check, so the owned-arg lint's general
2474    // API-shape target does not apply.
2475    #[allow(clippy::needless_pass_by_value)]
2476    fn assert_slot_kind_ctor_matches(actual: LayoutError, expected: LayoutError) {
2477        assert_eq!(
2478            actual, expected,
2479            "generated constructor must produce byte-equal LayoutError to open-coded struct-literal wrap",
2480        );
2481    }
2482
2483    #[test]
2484    fn mesh_slots_on_non_aplicacao_ctor_matches_struct_literal_wrap() {
2485        let (c, slots) = layout_slot_kind_ctor_fixture();
2486        assert_slot_kind_ctor_matches(
2487            LayoutError::mesh_slots_on_non_aplicacao(&c, slots.clone()),
2488            LayoutError::MeshSlotsOnNonAplicacao {
2489                caixa: c.nome().to_string(),
2490                kind: c.kind(),
2491                slots: slots.join(" "),
2492            },
2493        );
2494    }
2495
2496    #[test]
2497    fn supervisor_slots_on_non_supervisor_ctor_matches_struct_literal_wrap() {
2498        let (c, slots) = layout_slot_kind_ctor_fixture();
2499        assert_slot_kind_ctor_matches(
2500            LayoutError::supervisor_slots_on_non_supervisor(&c, slots.clone()),
2501            LayoutError::SupervisorSlotsOnNonSupervisor {
2502                caixa: c.nome().to_string(),
2503                kind: c.kind(),
2504                slots: slots.join(" "),
2505            },
2506        );
2507    }
2508
2509    #[test]
2510    fn servico_slots_on_non_servico_ctor_matches_struct_literal_wrap() {
2511        let (c, slots) = layout_slot_kind_ctor_fixture();
2512        assert_slot_kind_ctor_matches(
2513            LayoutError::servico_slots_on_non_servico(&c, slots.clone()),
2514            LayoutError::ServicoSlotsOnNonServico {
2515                caixa: c.nome().to_string(),
2516                kind: c.kind(),
2517                slots: slots.join(" "),
2518            },
2519        );
2520    }
2521
2522    #[test]
2523    fn foreign_code_slot_ctor_matches_struct_literal_wrap() {
2524        let (c, slots) = layout_slot_kind_ctor_fixture();
2525        assert_slot_kind_ctor_matches(
2526            LayoutError::foreign_code_slot(&c, slots.clone()),
2527            LayoutError::ForeignCodeSlot {
2528                caixa: c.nome().to_string(),
2529                kind: c.kind(),
2530                slots: slots.join(" "),
2531            },
2532        );
2533    }
2534
2535    #[test]
2536    fn slot_kind_ctor_routes_caixa_prefix_through_nome_accessor() {
2537        // Pin the fold's `caixa = caixa.nome().to_string()` half against a
2538        // non-default `:nome` — the accessor threads the caller's `:nome`
2539        // verbatim into the wrap envelope, so the fold does not silently
2540        // collapse onto the default `"demo"` fixture nome. Peer of the
2541        // sibling `violation_ctor_routes_caixa_prefix_through_nome_accessor`
2542        // pin on the `{ caixa, issue }` envelope; extended here onto the
2543        // `{ caixa, kind, slots }` envelope so both `LayoutError`-shape
2544        // constructor families guarantee the `:nome`-derived-caixa slot
2545        // routes through [`Caixa::nome`] rather than a hard-coded string.
2546        let mut c = caixa(CaixaKind::Biblioteca);
2547        c.nome = "alt-nome".into();
2548        let actual = LayoutError::mesh_slots_on_non_aplicacao(&c, vec![":membros"]);
2549        assert_eq!(
2550            actual,
2551            LayoutError::MeshSlotsOnNonAplicacao {
2552                caixa: "alt-nome".to_string(),
2553                kind: CaixaKind::Biblioteca,
2554                slots: ":membros".to_string(),
2555            },
2556        );
2557    }
2558
2559    #[test]
2560    fn slot_kind_ctor_routes_kind_through_caixa_kind_accessor() {
2561        // Pin the fold's `kind = caixa.kind()` half against a non-default
2562        // kind — the accessor threads the caller's `:kind` verbatim into
2563        // the wrap envelope, so the fold does not silently collapse onto
2564        // one hard-coded kind. Sweeps every non-Aplicacao / non-Supervisor
2565        // / non-Servico kind the corresponding gate can fire on so the
2566        // pin covers the kind-derivation axis on every downstream variant.
2567        for kind in [
2568            CaixaKind::Biblioteca,
2569            CaixaKind::Binario,
2570            CaixaKind::Servico,
2571            CaixaKind::Supervisor,
2572            CaixaKind::Aplicacao,
2573            CaixaKind::Acao,
2574        ] {
2575            let c = caixa(kind);
2576            let actual = LayoutError::foreign_code_slot(&c, vec![":exe"]);
2577            assert_eq!(
2578                actual,
2579                LayoutError::ForeignCodeSlot {
2580                    caixa: c.nome().to_string(),
2581                    kind,
2582                    slots: ":exe".to_string(),
2583                },
2584                "foreign_code_slot ctor must thread `caixa.kind()` verbatim on every kind",
2585            );
2586        }
2587    }
2588
2589    #[test]
2590    fn slot_kind_ctor_routes_slots_through_join_separator() {
2591        // Pin the fold's `slots = slots.join(" ")` half against a
2592        // multi-entry slots list — the join threads exactly one ASCII
2593        // space between entries, in caller-supplied order, so the fold
2594        // does not silently collapse onto a fixed separator (`", "`, `";
2595        // "`, `"\n"`), a sorted order, or a single-entry pass-through.
2596        // Uses the M2 servico-slot vocabulary since these are what the
2597        // corresponding `servico_slots_on_non_servico` gate reports.
2598        let c = caixa(CaixaKind::Biblioteca);
2599        let actual = LayoutError::servico_slots_on_non_servico(
2600            &c,
2601            vec![":limits", ":behavior", ":upgrade-from"],
2602        );
2603        assert_eq!(
2604            actual,
2605            LayoutError::ServicoSlotsOnNonServico {
2606                caixa: c.nome().to_string(),
2607                kind: c.kind(),
2608                slots: ":limits :behavior :upgrade-from".to_string(),
2609            },
2610        );
2611    }
2612
2613    // ── LayoutError::missing_entry substrate-primitive constructor ───────
2614    //
2615    // The [`LayoutError::missing_entry`] constructor beside the enum
2616    // definition folds the `{ kind: &'static str, path: PathBuf }`
2617    // uniform-shape envelope onto one substrate primitive — the third
2618    // and last uniform-shape envelope on `LayoutError` after the
2619    // `{ caixa, issue }` family the [`layout_violation_ctors!`] macro
2620    // closed (131ca0d) and the `{ caixa, kind, slots }` family the peer
2621    // [`layout_slot_kind_ctors!`] macro closed (0419438). The pins below
2622    // (fail-before-pass-after by construction — a byte-mismatched
2623    // constructor arm would trip its equivalence pin first) lock the
2624    // constructor to its struct-literal peer under `PartialEq`, so every
2625    // wire-up in [`StandardLayout::verify`] on this variant produces a
2626    // byte-equal `LayoutError` to the pre-lift open-coded block. The two
2627    // cross-axis pins that follow (canonical-kind-label sweep, non-
2628    // default path) route each of the two constructor input axes through
2629    // its arg verbatim, so the fold does not silently collapse onto a
2630    // fixture default on either axis.
2631
2632    #[test]
2633    fn missing_entry_ctor_matches_struct_literal_wrap() {
2634        // Per-envelope equivalence pin — the `missing_entry` constructor
2635        // produces a `LayoutError::MissingEntry` byte-equal under
2636        // `PartialEq` to the open-coded four-line struct-literal wrap on
2637        // the same `(kind, path)` fixture. Peer of the sibling
2638        // `<slot>_violation_ctor_matches_struct_literal_wrap` /
2639        // `<slot>_slots_on_non_<owner>_ctor_matches_struct_literal_wrap`
2640        // pins on the two prior uniform-shape envelopes on the same
2641        // `LayoutError`.
2642        let path = PathBuf::from("/tmp/x/lib/demo.lisp");
2643        assert_eq!(
2644            LayoutError::missing_entry(
2645                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2646                path.clone(),
2647            ),
2648            LayoutError::MissingEntry {
2649                kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2650                path,
2651            },
2652        );
2653    }
2654
2655    #[test]
2656    fn missing_entry_ctor_routes_kind_through_arg_verbatim() {
2657        // Pin the fold's `kind: &'static str` arg through every canonical
2658        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] label the five
2659        // wire-up sites in [`StandardLayout::verify`] pass — so the fold
2660        // does not silently collapse onto one hard-coded label. Sweep
2661        // matches the arm set the peer
2662        // `layout_missing_entry_kind_m2_consts_pin_canonical_kebab_case_labels`
2663        // pin (below) covers on the const-label declarations.
2664        let path = PathBuf::from("/tmp/x/entry");
2665        for kind in [
2666            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2667            crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2668            crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2669            crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
2670            crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
2671        ] {
2672            assert_eq!(
2673                LayoutError::missing_entry(kind, path.clone()),
2674                LayoutError::MissingEntry {
2675                    kind,
2676                    path: path.clone(),
2677                },
2678                "missing_entry ctor must thread `kind` verbatim on every canonical label",
2679            );
2680        }
2681    }
2682
2683    #[test]
2684    fn missing_entry_ctor_routes_path_through_arg_verbatim() {
2685        // Pin the fold's `path: PathBuf` arg against a non-default,
2686        // multi-component `PathBuf` — the ctor threads the caller's
2687        // `PathBuf` verbatim into the wrap envelope, so the fold does
2688        // not silently collapse onto a fixed component prefix, a
2689        // canonicalized form, or a single-component pass-through.
2690        let path = PathBuf::from("/alt/root")
2691            .join("servicos")
2692            .join("hello-rio.computeunit.yaml");
2693        assert_eq!(
2694            LayoutError::missing_entry(
2695                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2696                path.clone(),
2697            ),
2698            LayoutError::MissingEntry {
2699                kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2700                path,
2701            },
2702        );
2703    }
2704
2705    // ── StandardLayout::probe_declared_entry substrate primitive ─────────
2706    //
2707    // The [`StandardLayout::probe_declared_entry`] method
2708    // (`layout.rs`) folds the five self-similar
2709    // `let full = root.join(p); if !self.exists(&full) { return
2710    // Err(LayoutError::missing_entry(<kind>, full)); }` existence-probe
2711    // blocks at [`StandardLayout::verify`] (`:bibliotecas` iteration,
2712    // `:exe` iteration, `:servicos` iteration, `:behavior` on-disk
2713    // callback-path iteration, `:upgrade-from` per-instruction script-
2714    // path iteration) onto one substrate primitive. The pins below
2715    // (fail-before-pass-after by construction — a byte-mismatched
2716    // primitive body would trip its equivalence pin first) lock the
2717    // fold to its pre-lift open-coded shape under `PartialEq`, so every
2718    // wire-up in [`StandardLayout::verify`] on this primitive produces
2719    // a byte-equal `LayoutError` on miss and a byte-equal `PathBuf` on
2720    // hit.
2721
2722    #[test]
2723    fn probe_declared_entry_folds_miss_returns_missing_entry() {
2724        // Per-primitive equivalence pin on the miss arm — a
2725        // [`StandardLayout`] whose oracle returns `false` for every
2726        // path yields a `MissingEntry` byte-equal under `PartialEq` to
2727        // the open-coded `LayoutError::missing_entry(<kind>,
2728        // root.join(path))` wrap the pre-lift block carried at each of
2729        // the five wire-up sites. Peer of the sibling
2730        // `missing_entry_ctor_matches_struct_literal_wrap` pin on the
2731        // constructor's own byte-equal shape.
2732        let layout = StandardLayout::new().with_path_exists(|_| false);
2733        let root = PathBuf::from("/tmp/x");
2734        let path = Path::new("lib/demo.lisp");
2735        let err = layout
2736            .probe_declared_entry(
2737                path,
2738                &root,
2739                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2740            )
2741            .unwrap_err();
2742        assert_eq!(
2743            err,
2744            LayoutError::missing_entry(
2745                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2746                root.join(path),
2747            ),
2748            "probe_declared_entry miss arm must produce byte-equal LayoutError to \
2749             open-coded `missing_entry(<kind>, root.join(path))` wrap",
2750        );
2751    }
2752
2753    #[test]
2754    fn probe_declared_entry_folds_hit_returns_resolved_full() {
2755        // Per-primitive equivalence pin on the hit arm — a
2756        // [`StandardLayout`] whose oracle returns `true` for the probed
2757        // resolved path yields `Ok(root.join(path))` byte-equal under
2758        // `PartialEq`. The pre-lift `:exe` / `:servicos` wire-up sites
2759        // needed the resolved `full` for the follow-up sandbox-directory-
2760        // containment check; the fold preserves that hand-off through
2761        // the primitive's `Ok(PathBuf)` return arm rather than
2762        // re-computing `root.join(path)` at the follow-up gate.
2763        let root = PathBuf::from("/tmp/x");
2764        let path = Path::new("exe/tool.lisp");
2765        let full = root.join(path);
2766        let full_probe = full.clone();
2767        let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
2768        let resolved = layout
2769            .probe_declared_entry(path, &root, crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE)
2770            .expect("probe_declared_entry must return Ok(root.join(path)) when the oracle hits");
2771        assert_eq!(
2772            resolved, full,
2773            "probe_declared_entry hit arm must return the resolved `root.join(path)` \
2774             byte-equal so the `:exe` / `:servicos` sandbox-containment follow-up \
2775             reads it verbatim without re-computing",
2776        );
2777    }
2778
2779    #[test]
2780    fn probe_declared_entry_threads_kind_through_arg_verbatim() {
2781        // Cross-axis pin — sweep every canonical
2782        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] label the five
2783        // wire-up sites in [`StandardLayout::verify`] pass. Each miss
2784        // must return a `MissingEntry` whose `kind:` field byte-equals
2785        // the caller-provided arg, so the fold does not silently
2786        // collapse onto one hard-coded label. Sweep matches the arm
2787        // set the peer
2788        // `missing_entry_ctor_routes_kind_through_arg_verbatim` pin
2789        // covers on the constructor arg.
2790        let layout = StandardLayout::new().with_path_exists(|_| false);
2791        let root = PathBuf::from("/tmp/x");
2792        let path = Path::new("some/entry");
2793        for kind in [
2794            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2795            crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2796            crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2797            crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
2798            crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
2799        ] {
2800            let err = layout.probe_declared_entry(path, &root, kind).unwrap_err();
2801            assert_eq!(
2802                err,
2803                LayoutError::missing_entry(kind, root.join(path)),
2804                "probe_declared_entry must thread `kind` verbatim on every canonical label",
2805            );
2806        }
2807    }
2808
2809    #[test]
2810    fn probe_declared_entry_threads_path_through_arg_verbatim() {
2811        // Cross-axis pin — the primitive must compose the `path` arg
2812        // through `root.join` verbatim on the miss arm's `MissingEntry
2813        // { path: … }` field, so the fold does not silently collapse
2814        // onto a fixed component prefix, a canonicalized form, or a
2815        // hand-authored `root.join(<literal>)`. Sweep two multi-
2816        // component `Path` fixtures (one under `servicos/`, one under
2817        // `lib/`) so a byte-drifted composition on either axis would
2818        // trip.
2819        let layout = StandardLayout::new().with_path_exists(|_| false);
2820        let root = PathBuf::from("/alt/root");
2821        for path in [
2822            Path::new("servicos/hello-rio.computeunit.yaml"),
2823            Path::new("lib/demo.lisp"),
2824        ] {
2825            let err = layout
2826                .probe_declared_entry(
2827                    path,
2828                    &root,
2829                    crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2830                )
2831                .unwrap_err();
2832            assert_eq!(
2833                err,
2834                LayoutError::missing_entry(
2835                    crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2836                    root.join(path),
2837                ),
2838                "probe_declared_entry must compose `path` through `root.join` verbatim",
2839            );
2840        }
2841    }
2842
2843    #[test]
2844    fn probe_declared_entry_routes_through_configurable_exists_oracle() {
2845        // Cross-axis pin — the primitive must consult the injected
2846        // [`StandardLayout::with_path_exists`] oracle, not the ambient
2847        // `Path::exists` filesystem probe. Configure a per-path
2848        // discriminator that returns `true` only for one canonical
2849        // resolved path and assert both arms:
2850        // - the "hit" path resolves to `Ok(full)` byte-equal
2851        // - every other path resolves to `MissingEntry` on the same
2852        //   [`StandardLayout`] instance
2853        // The pin traps a regression that reroutes the primitive off
2854        // the injected oracle onto the ambient `Path::exists` (which
2855        // would silently return `false` for every path in `/tmp/x/…`
2856        // and mask the miss-arm hand-off on the hit fixture, or
2857        // silently return `true` for a real system path and mask the
2858        // hit-arm hand-off on the miss fixture).
2859        let root = PathBuf::from("/tmp/x");
2860        let hit_path = Path::new("servicos/keep.computeunit.yaml");
2861        let miss_path = Path::new("servicos/drop.computeunit.yaml");
2862        let hit_full = root.join(hit_path);
2863        let hit_full_probe = hit_full.clone();
2864        let layout = StandardLayout::new().with_path_exists(move |p| p == hit_full_probe);
2865
2866        let resolved = layout
2867            .probe_declared_entry(
2868                hit_path,
2869                &root,
2870                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2871            )
2872            .expect("probe_declared_entry must consult the injected oracle on the hit arm");
2873        assert_eq!(
2874            resolved, hit_full,
2875            "probe_declared_entry hit arm must return the oracle-approved resolved path",
2876        );
2877
2878        let err = layout
2879            .probe_declared_entry(
2880                miss_path,
2881                &root,
2882                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2883            )
2884            .unwrap_err();
2885        assert_eq!(
2886            err,
2887            LayoutError::missing_entry(
2888                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2889                root.join(miss_path),
2890            ),
2891            "probe_declared_entry miss arm must fire when the injected oracle rejects the path",
2892        );
2893    }
2894
2895    // ── StandardLayout::probe_sandboxed_declared_entry substrate primitive ─
2896    //
2897    // The [`StandardLayout::probe_sandboxed_declared_entry`] method
2898    // (`layout.rs`) folds the two self-similar `let full = self.
2899    // probe_declared_entry(p, root, <kind>)?; if !full.starts_with(&<slot>_dir)
2900    // { return Err(LayoutError::<Slot>OutsideDir(full)); }` blocks at
2901    // [`StandardLayout::verify`] (`:exe` iteration, `:servicos` iteration)
2902    // onto one substrate primitive. The pins below (fail-before-pass-after
2903    // by construction — a byte-mismatched primitive body would trip its
2904    // equivalence pin first) lock the fold to its pre-lift open-coded
2905    // shape under `PartialEq`, so every wire-up in
2906    // [`StandardLayout::verify`] on this primitive produces a byte-equal
2907    // `LayoutError` on miss / sandbox-escape and a byte-equal `PathBuf`
2908    // on hit.
2909
2910    #[test]
2911    fn probe_sandboxed_declared_entry_folds_miss_returns_missing_entry() {
2912        // Per-primitive equivalence pin on the miss arm — a
2913        // [`StandardLayout`] whose oracle returns `false` for every
2914        // path yields a `MissingEntry` byte-equal under `PartialEq` to
2915        // the open-coded `LayoutError::missing_entry(<kind>,
2916        // root.join(path))` wrap the sibling
2917        // [`StandardLayout::probe_declared_entry`] primitive routes
2918        // through. Diagnostic-order pin: `MissingEntry` outranks the
2919        // `outside_ctor` sandbox-escape arm on the same iteration, so
2920        // an entry that is both absent *and* outside the sandbox fires
2921        // the `MissingEntry` diagnostic (the pre-lift order the two
2922        // wire-up sites carried).
2923        let layout = StandardLayout::new().with_path_exists(|_| false);
2924        let root = PathBuf::from("/tmp/x");
2925        let path = Path::new("lib/tool");
2926        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
2927        let err = layout
2928            .probe_sandboxed_declared_entry(
2929                path,
2930                &root,
2931                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2932                &exe_dir,
2933                LayoutError::ExeOutsideDir,
2934            )
2935            .unwrap_err();
2936        assert_eq!(
2937            err,
2938            LayoutError::missing_entry(
2939                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2940                root.join(path),
2941            ),
2942            "probe_sandboxed_declared_entry miss arm must produce byte-equal \
2943             LayoutError to the sibling probe_declared_entry primitive's miss wrap, \
2944             preserving the pre-lift MissingEntry-before-<Slot>OutsideDir diagnostic order",
2945        );
2946    }
2947
2948    #[test]
2949    fn probe_sandboxed_declared_entry_folds_sandbox_escape_via_outside_ctor() {
2950        // Per-primitive equivalence pin on the sandbox-escape arm — a
2951        // [`StandardLayout`] whose oracle admits the probed resolved
2952        // path (so the miss arm passes) but whose resolved path lies
2953        // outside the caller-provided `sandbox_dir` yields the paired
2954        // `outside_ctor(full)` byte-equal under `PartialEq`. The
2955        // primitive threads the resolved `full` through the caller-
2956        // supplied `fn(PathBuf) -> LayoutError` constructor rather
2957        // than a hard-coded variant, so the fold does not silently
2958        // collapse onto one of the two `:exe` / `:servicos` outside-
2959        // dir variants.
2960        let root = PathBuf::from("/tmp/x");
2961        let path = Path::new("lib/tool");
2962        let full = root.join(path);
2963        let full_probe = full.clone();
2964        let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
2965        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
2966        let err = layout
2967            .probe_sandboxed_declared_entry(
2968                path,
2969                &root,
2970                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2971                &exe_dir,
2972                LayoutError::ExeOutsideDir,
2973            )
2974            .unwrap_err();
2975        assert_eq!(
2976            err,
2977            LayoutError::ExeOutsideDir(full),
2978            "probe_sandboxed_declared_entry sandbox-escape arm must route the \
2979             resolved `full` through the caller-supplied outside_ctor byte-equal to \
2980             the pre-lift `LayoutError::ExeOutsideDir(full)` tuple-literal",
2981        );
2982    }
2983
2984    #[test]
2985    fn probe_sandboxed_declared_entry_folds_hit_returns_resolved_full() {
2986        // Per-primitive equivalence pin on the hit arm — a
2987        // [`StandardLayout`] whose oracle admits the probed path *and*
2988        // whose resolved path lives inside `sandbox_dir` yields
2989        // `Ok(root.join(path))` byte-equal under `PartialEq`. The
2990        // pre-lift wire-ups discarded the resolved `Ok(PathBuf)` since
2991        // no follow-up per-path gate consumes it after the sandbox
2992        // check; the fold preserves the same hit-arm hand-off through
2993        // the primitive's `Ok(PathBuf)` return so a future consumer
2994        // that wants to run a per-path successor gate reaches the
2995        // resolved path without re-computing `root.join(path)`.
2996        let root = PathBuf::from("/tmp/x");
2997        let path = Path::new("exe/tool");
2998        let full = root.join(path);
2999        let full_probe = full.clone();
3000        let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3001        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3002        let resolved = layout
3003            .probe_sandboxed_declared_entry(
3004                path,
3005                &root,
3006                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3007                &exe_dir,
3008                LayoutError::ExeOutsideDir,
3009            )
3010            .expect(
3011                "probe_sandboxed_declared_entry must return Ok(root.join(path)) \
3012                 when the oracle admits the path and it lives inside sandbox_dir",
3013            );
3014        assert_eq!(
3015            resolved, full,
3016            "probe_sandboxed_declared_entry hit arm must return the resolved \
3017             `root.join(path)` byte-equal so a future per-path successor gate \
3018             reaches it without re-computing",
3019        );
3020    }
3021
3022    #[test]
3023    fn probe_sandboxed_declared_entry_threads_outside_ctor_through_arg_verbatim() {
3024        // Cross-axis pin — the primitive must thread the caller-
3025        // supplied `outside_ctor` verbatim into the sandbox-escape
3026        // arm's `LayoutError` return, so the fold does not silently
3027        // collapse onto one hard-coded variant. Sweep both
3028        // [`LayoutError`] tuple-variants the two wire-up sites in
3029        // [`StandardLayout::verify`] pass — [`LayoutError::ExeOutsideDir`]
3030        // and [`LayoutError::ServicoOutsideDir`] — so a byte-drifted
3031        // ctor-routing on either axis would trip.
3032        let root = PathBuf::from("/tmp/x");
3033        let outside_dir = root.join(crate::render::LAYOUT_DIR_LIB);
3034        for (kind, sandbox_component, ctor, expected_variant) in [
3035            (
3036                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3037                crate::render::LAYOUT_DIR_EXE,
3038                LayoutError::ExeOutsideDir as fn(PathBuf) -> LayoutError,
3039                LayoutError::ExeOutsideDir as fn(PathBuf) -> LayoutError,
3040            ),
3041            (
3042                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3043                crate::render::LAYOUT_DIR_SERVICOS,
3044                LayoutError::ServicoOutsideDir as fn(PathBuf) -> LayoutError,
3045                LayoutError::ServicoOutsideDir as fn(PathBuf) -> LayoutError,
3046            ),
3047        ] {
3048            let path = outside_dir.strip_prefix(&root).unwrap().join("tool");
3049            let full = root.join(&path);
3050            let full_probe = full.clone();
3051            let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3052            let sandbox_dir = root.join(sandbox_component);
3053            let err = layout
3054                .probe_sandboxed_declared_entry(&path, &root, kind, &sandbox_dir, ctor)
3055                .unwrap_err();
3056            assert_eq!(
3057                err,
3058                expected_variant(full),
3059                "probe_sandboxed_declared_entry must thread `outside_ctor` verbatim \
3060                 on every canonical `<Slot>OutsideDir` variant the two wire-up sites pass",
3061            );
3062        }
3063    }
3064
3065    #[test]
3066    fn probe_sandboxed_declared_entry_routes_miss_arm_through_probe_declared_entry() {
3067        // Cross-primitive pin — the sandboxed probe must route its
3068        // miss arm through the sibling [`StandardLayout::
3069        // probe_declared_entry`] primitive rather than re-inlining the
3070        // `root.join` + `exists` + `missing_entry` cascade, so a
3071        // future edit to the miss-arm shape on either primitive lands
3072        // in exactly one place. Byte-parity assertion: on a fixture
3073        // that misses the oracle, the sandboxed primitive's `Err`
3074        // arm must equal the peer [`StandardLayout::
3075        // probe_declared_entry`] primitive's `Err` arm on the same
3076        // fixture — otherwise the fold has drifted from the substrate
3077        // primitive.
3078        let layout = StandardLayout::new().with_path_exists(|_| false);
3079        let root = PathBuf::from("/tmp/x");
3080        let path = Path::new("servicos/hello-rio.computeunit.yaml");
3081        let sandbox_dir = root.join(crate::render::LAYOUT_DIR_SERVICOS);
3082        let via_sandboxed = layout
3083            .probe_sandboxed_declared_entry(
3084                path,
3085                &root,
3086                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3087                &sandbox_dir,
3088                LayoutError::ServicoOutsideDir,
3089            )
3090            .unwrap_err();
3091        let via_probe = layout
3092            .probe_declared_entry(
3093                path,
3094                &root,
3095                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3096            )
3097            .unwrap_err();
3098        assert_eq!(
3099            via_sandboxed, via_probe,
3100            "probe_sandboxed_declared_entry's miss arm must equal the sibling \
3101             probe_declared_entry primitive's miss arm byte-equal — pins that the \
3102             sandboxed primitive routes through the substrate primitive rather than \
3103             re-inlining the `root.join` + `exists` + `missing_entry` cascade",
3104        );
3105    }
3106
3107    // ── StandardLayout::probe_declared_entries substrate primitive ───────
3108    //
3109    // The [`StandardLayout::probe_declared_entries`] method (`layout.rs`)
3110    // folds the three self-similar per-slot existence-probe *loop* blocks
3111    // at [`StandardLayout::verify`] (`:bibliotecas` iteration,
3112    // `:behavior` on-disk callback-path iteration, `:upgrade-from` per-
3113    // instruction script-path iteration) onto one substrate primitive.
3114    // The pins below (fail-before-pass-after by construction — a byte-
3115    // mismatched primitive body would trip its equivalence pin first)
3116    // lock the fold to its pre-lift open-coded shape under `PartialEq`,
3117    // so every wire-up in [`StandardLayout::verify`] on this primitive
3118    // produces a byte-equal `LayoutError` on miss (via the sibling
3119    // [`StandardLayout::probe_declared_entry`] miss arm) and a byte-
3120    // equal `Ok(())` on the empty / all-hit arms (the fold's identity
3121    // element on an empty slot list; the pre-lift `for … { … }` loop's
3122    // vacuous pass-through).
3123    //
3124    // Sibling of the peer per-arm-probe [`StandardLayout::probe_declared_entry`]
3125    // (fda1e35) and two-arm-sandboxed [`StandardLayout::
3126    // probe_sandboxed_declared_entry`] (4940d55) primitive test blocks
3127    // — same substrate-primitive discipline extended onto the per-slot
3128    // batch axis these two per-path primitives compose under.
3129
3130    #[test]
3131    fn probe_declared_entries_folds_empty_iterator_returns_ok() {
3132        // Per-primitive identity-element pin on the empty-iterator arm —
3133        // the fold's `Ok(())` return on an empty `IntoIterator` is byte-
3134        // equal to the pre-lift `for _ in <empty> { … }` loop's vacuous
3135        // pass-through. Peer of the peer-primitive `Option::None →
3136        // Ok(())` identity elements the sibling per-Caixa compound
3137        // gates ([`crate::Caixa::validate_limits`] baa4688,
3138        // [`crate::Caixa::validate_behavior`] 0d2877a) each carry on
3139        // the sibling per-slot compound-gate axis.
3140        let layout = StandardLayout::new().with_path_exists(|_| false);
3141        let root = PathBuf::from("/tmp/x");
3142        let empty: [&Path; 0] = [];
3143        layout
3144            .probe_declared_entries(
3145                empty,
3146                &root,
3147                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3148            )
3149            .expect(
3150                "probe_declared_entries must return Ok(()) on an empty iterator \
3151                 — the fold's identity element on an empty per-slot batch",
3152            );
3153    }
3154
3155    #[test]
3156    fn probe_declared_entries_folds_all_hits_returns_ok() {
3157        // Per-primitive equivalence pin on the all-hit arm — a
3158        // [`StandardLayout`] whose oracle admits every path in the batch
3159        // yields `Ok(())` byte-equal to the pre-lift `for p in … {
3160        // self.probe_declared_entry(p, root, kind)?; }` loop's full-
3161        // consumption pass-through.
3162        let root = PathBuf::from("/tmp/x");
3163        let a = root.join("lib/a.lisp");
3164        let b = root.join("lib/b.lisp");
3165        let a_probe = a.clone();
3166        let b_probe = b.clone();
3167        let layout = StandardLayout::new().with_path_exists(move |p| p == a_probe || p == b_probe);
3168        layout
3169            .probe_declared_entries(
3170                [Path::new("lib/a.lisp"), Path::new("lib/b.lisp")],
3171                &root,
3172                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3173            )
3174            .expect(
3175                "probe_declared_entries must return Ok(()) when every path in the \
3176                 batch is admitted by the oracle",
3177            );
3178    }
3179
3180    #[test]
3181    fn probe_declared_entries_folds_first_miss_short_circuits_via_probe_declared_entry() {
3182        // Per-primitive equivalence pin on the first-miss arm — a
3183        // [`StandardLayout`] whose oracle admits the first path and
3184        // rejects the second yields a `MissingEntry` byte-equal under
3185        // `PartialEq` to the sibling [`StandardLayout::
3186        // probe_declared_entry`] primitive's miss wrap on the *second*
3187        // path (the pre-lift `for … { … ? }` loop's first-error return
3188        // semantics), *not* on the first (admitted) path.
3189        let root = PathBuf::from("/tmp/x");
3190        let hit = root.join("lib/a.lisp");
3191        let hit_probe = hit.clone();
3192        let layout = StandardLayout::new().with_path_exists(move |p| p == hit_probe);
3193        let miss = Path::new("lib/b.lisp");
3194        let err = layout
3195            .probe_declared_entries(
3196                [Path::new("lib/a.lisp"), miss],
3197                &root,
3198                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3199            )
3200            .unwrap_err();
3201        assert_eq!(
3202            err,
3203            LayoutError::missing_entry(
3204                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3205                root.join(miss),
3206            ),
3207            "probe_declared_entries must short-circuit on the first missing entry \
3208             with a `MissingEntry` byte-equal to the sibling probe_declared_entry \
3209             primitive's miss wrap on that entry — the pre-lift `for … {{ … ? }}` \
3210             loop's first-error return semantics",
3211        );
3212    }
3213
3214    #[test]
3215    fn probe_declared_entries_folds_first_miss_short_circuits_before_later_paths() {
3216        // Diagnostic-order pin — the primitive must return on the *first*
3217        // miss in iterator order rather than probing every path and
3218        // returning the last miss (which would silently drop the pre-
3219        // lift `for … { … ? }` loop's first-error contract). Fixture:
3220        // the oracle rejects both paths, so a byte-equal `MissingEntry`
3221        // on the *first* path in the iterator distinguishes the two
3222        // return-order shapes.
3223        let layout = StandardLayout::new().with_path_exists(|_| false);
3224        let root = PathBuf::from("/tmp/x");
3225        let first = Path::new("lib/first.lisp");
3226        let second = Path::new("lib/second.lisp");
3227        let err = layout
3228            .probe_declared_entries(
3229                [first, second],
3230                &root,
3231                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3232            )
3233            .unwrap_err();
3234        assert_eq!(
3235            err,
3236            LayoutError::missing_entry(
3237                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3238                root.join(first),
3239            ),
3240            "probe_declared_entries must return on the *first* miss in iterator order \
3241             — a `MissingEntry` on the second path would silently drop the pre-lift \
3242             `for … {{ … ? }}` loop's first-error contract",
3243        );
3244    }
3245
3246    #[test]
3247    fn probe_declared_entries_threads_kind_through_arg_verbatim() {
3248        // Cross-axis pin — sweep every canonical
3249        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] label the three
3250        // batch wire-up sites in [`StandardLayout::verify`] pass through
3251        // this primitive (`:bibliotecas`, `:behavior` callback,
3252        // `:upgrade-from` script). Each miss must return a `MissingEntry`
3253        // whose `kind:` field byte-equals the caller-provided arg, so
3254        // the fold does not silently collapse onto one hard-coded label.
3255        // Sibling of the peer `probe_declared_entry_threads_kind_through_arg_verbatim`
3256        // pin's five-label sweep on the per-arm primitive.
3257        let layout = StandardLayout::new().with_path_exists(|_| false);
3258        let root = PathBuf::from("/tmp/x");
3259        let path = Path::new("some/entry");
3260        for kind in [
3261            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3262            crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
3263            crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
3264        ] {
3265            let err = layout
3266                .probe_declared_entries([path], &root, kind)
3267                .unwrap_err();
3268            assert_eq!(
3269                err,
3270                LayoutError::missing_entry(kind, root.join(path)),
3271                "probe_declared_entries must thread `kind` verbatim on every canonical \
3272                 batch label",
3273            );
3274        }
3275    }
3276
3277    #[test]
3278    fn probe_declared_entries_accepts_asref_path_shape_wire_up_sites_pass() {
3279        // Cross-axis pin — the primitive's `P: AsRef<Path>` bound must
3280        // admit every concrete iterator element type the three
3281        // [`StandardLayout::verify`] wire-up sites pass:
3282        // - `&String` (from `caixa.bibliotecas(): &[String]`),
3283        // - `&Path` (from `b.declared_paths(): impl Iterator<Item = &Path>`),
3284        // - `&PathBuf` (from the flattened `.filter_map(_::declared_path)`
3285        //   on `:upgrade-from` instructions, whose `declared_path`
3286        //   returns `Option<&PathBuf>`).
3287        //
3288        // Byte-parity assertion: on a shared `/tmp/x/lib/demo.lisp`
3289        // fixture the miss-arm return must be byte-equal across all
3290        // three element-type flavors, so a future re-shape of the
3291        // bound (a narrower `P: Into<PathBuf>` collapse, a stricter
3292        // `&Path`-only signature) would surface here rather than at
3293        // the caller wire-up site.
3294        let layout = StandardLayout::new().with_path_exists(|_| false);
3295        let root = PathBuf::from("/tmp/x");
3296        let literal = "lib/demo.lisp";
3297        let expected = LayoutError::missing_entry(
3298            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3299            root.join(Path::new(literal)),
3300        );
3301
3302        let via_string_slice: Vec<String> = vec![literal.to_string()];
3303        let via_string_err = layout
3304            .probe_declared_entries(
3305                via_string_slice.iter(),
3306                &root,
3307                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3308            )
3309            .unwrap_err();
3310        assert_eq!(
3311            via_string_err, expected,
3312            "probe_declared_entries must accept `&String` items (the `caixa.bibliotecas() \
3313             : &[String]` wire-up shape)",
3314        );
3315
3316        let via_path_slice: [&Path; 1] = [Path::new(literal)];
3317        let via_path_err = layout
3318            .probe_declared_entries(
3319                via_path_slice,
3320                &root,
3321                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3322            )
3323            .unwrap_err();
3324        assert_eq!(
3325            via_path_err, expected,
3326            "probe_declared_entries must accept `&Path` items (the `b.declared_paths() \
3327             : impl Iterator<Item = &Path>` wire-up shape)",
3328        );
3329
3330        let via_pathbuf_slice: Vec<PathBuf> = vec![PathBuf::from(literal)];
3331        let via_pathbuf_err = layout
3332            .probe_declared_entries(
3333                via_pathbuf_slice.iter(),
3334                &root,
3335                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3336            )
3337            .unwrap_err();
3338        assert_eq!(
3339            via_pathbuf_err, expected,
3340            "probe_declared_entries must accept `&PathBuf` items (the `.filter_map( \
3341             _::declared_path)` on `:upgrade-from` instructions wire-up shape)",
3342        );
3343    }
3344
3345    #[test]
3346    fn probe_declared_entries_routes_miss_arm_through_probe_declared_entry() {
3347        // Cross-primitive pin — the batch primitive must route its miss
3348        // arm through the sibling [`StandardLayout::probe_declared_entry`]
3349        // primitive rather than re-inlining the `root.join` + `exists`
3350        // + `missing_entry` cascade, so a future edit to the miss-arm
3351        // shape on either primitive lands in exactly one place. Byte-
3352        // parity assertion: on a fixture that misses the oracle, the
3353        // batch primitive's `Err` arm must equal the peer per-arm
3354        // [`StandardLayout::probe_declared_entry`] primitive's `Err` arm
3355        // on the same fixture — otherwise the batch fold has drifted
3356        // from the substrate primitive. Same discipline the peer
3357        // `probe_sandboxed_declared_entry_routes_miss_arm_through_probe_declared_entry`
3358        // pin establishes on the sibling sandboxed-primitive axis.
3359        let layout = StandardLayout::new().with_path_exists(|_| false);
3360        let root = PathBuf::from("/tmp/x");
3361        let path = Path::new("lib/demo.lisp");
3362        let via_batch = layout
3363            .probe_declared_entries(
3364                [path],
3365                &root,
3366                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3367            )
3368            .unwrap_err();
3369        let via_probe = layout
3370            .probe_declared_entry(
3371                path,
3372                &root,
3373                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3374            )
3375            .unwrap_err();
3376        assert_eq!(
3377            via_batch, via_probe,
3378            "probe_declared_entries's miss arm must equal the sibling probe_declared_entry \
3379             primitive's miss arm byte-equal — pins that the batch primitive routes \
3380             through the substrate primitive rather than re-inlining the `root.join` + \
3381             `exists` + `missing_entry` cascade",
3382        );
3383    }
3384
3385    // ── StandardLayout::probe_sandboxed_declared_entries substrate primitive ─
3386    //
3387    // The [`StandardLayout::probe_sandboxed_declared_entries`] method
3388    // (`layout.rs`) folds the two self-similar per-slot sandboxed-
3389    // existence-probe *loop* blocks at [`StandardLayout::verify`]
3390    // (`:exe` iteration, `:servicos` iteration) onto one substrate
3391    // primitive. The pins below (fail-before-pass-after by construction
3392    // — a byte-mismatched primitive body would trip its equivalence pin
3393    // first) lock the fold to its pre-lift open-coded shape under
3394    // `PartialEq`, so every wire-up in [`StandardLayout::verify`] on
3395    // this primitive produces a byte-equal `LayoutError` on miss / on
3396    // sandbox-escape (via the sibling
3397    // [`StandardLayout::probe_sandboxed_declared_entry`] arms) and a
3398    // byte-equal `Ok(())` on the empty / all-hit arms (the fold's
3399    // identity element on an empty slot list; the pre-lift `for … {
3400    // … }` loop's vacuous pass-through).
3401    //
3402    // Sibling of the peer per-slot batch bare-probe
3403    // [`StandardLayout::probe_declared_entries`] (d1ccb0b), per-arm
3404    // bare-probe [`StandardLayout::probe_declared_entry`] (fda1e35), and
3405    // per-arm sandboxed-probe
3406    // [`StandardLayout::probe_sandboxed_declared_entry`] (4940d55)
3407    // primitive test blocks — same substrate-primitive discipline
3408    // extended onto the fourth and last quadrant of the
3409    // (per-arm | per-slot batch) × (bare | sandboxed) existence-probe
3410    // algebra.
3411
3412    #[test]
3413    fn probe_sandboxed_declared_entries_folds_empty_iterator_returns_ok() {
3414        // Per-primitive identity-element pin on the empty-iterator arm —
3415        // the fold's `Ok(())` return on an empty `IntoIterator` is byte-
3416        // equal to the pre-lift `for _ in <empty> { … }` loop's vacuous
3417        // pass-through. Peer of the sibling
3418        // `probe_declared_entries_folds_empty_iterator_returns_ok` pin
3419        // on the bare-batch axis and the `Option::None → Ok(())`
3420        // identity elements the per-Caixa compound gates
3421        // ([`crate::Caixa::validate_limits`] baa4688,
3422        // [`crate::Caixa::validate_behavior`] 0d2877a) each carry on the
3423        // per-slot compound-gate axis.
3424        let layout = StandardLayout::new().with_path_exists(|_| false);
3425        let root = PathBuf::from("/tmp/x");
3426        let empty: [&Path; 0] = [];
3427        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3428        layout
3429            .probe_sandboxed_declared_entries(
3430                empty,
3431                &root,
3432                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3433                &exe_dir,
3434                LayoutError::ExeOutsideDir,
3435            )
3436            .expect(
3437                "probe_sandboxed_declared_entries must return Ok(()) on an empty iterator \
3438                 — the fold's identity element on an empty per-slot sandboxed batch",
3439            );
3440    }
3441
3442    #[test]
3443    fn probe_sandboxed_declared_entries_folds_all_hits_returns_ok() {
3444        // Per-primitive equivalence pin on the all-hit arm — a
3445        // [`StandardLayout`] whose oracle admits every path in the batch
3446        // *and* whose resolved paths live inside `sandbox_dir` yields
3447        // `Ok(())` byte-equal to the pre-lift `for p in … {
3448        // self.probe_sandboxed_declared_entry(p, root, kind,
3449        // &sandbox_dir, outside_ctor)?; }` loop's full-consumption
3450        // pass-through.
3451        let root = PathBuf::from("/tmp/x");
3452        let a = root.join("exe/a");
3453        let b = root.join("exe/b");
3454        let a_probe = a.clone();
3455        let b_probe = b.clone();
3456        let layout = StandardLayout::new().with_path_exists(move |p| p == a_probe || p == b_probe);
3457        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3458        layout
3459            .probe_sandboxed_declared_entries(
3460                [Path::new("exe/a"), Path::new("exe/b")],
3461                &root,
3462                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3463                &exe_dir,
3464                LayoutError::ExeOutsideDir,
3465            )
3466            .expect(
3467                "probe_sandboxed_declared_entries must return Ok(()) when every path in \
3468                 the batch is admitted by the oracle and lives inside sandbox_dir",
3469            );
3470    }
3471
3472    #[test]
3473    fn probe_sandboxed_declared_entries_folds_first_miss_short_circuits_via_probe_sandboxed_declared_entry()
3474     {
3475        // Per-primitive equivalence pin on the first-miss arm — a
3476        // [`StandardLayout`] whose oracle admits the first path (inside
3477        // sandbox_dir) and rejects the second yields a `MissingEntry`
3478        // byte-equal under `PartialEq` to the sibling
3479        // [`StandardLayout::probe_sandboxed_declared_entry`] primitive's
3480        // miss wrap on the *second* path (the pre-lift `for … { … ? }`
3481        // loop's first-error return semantics), *not* on the first
3482        // (admitted) path.
3483        let root = PathBuf::from("/tmp/x");
3484        let hit = root.join("exe/a");
3485        let hit_probe = hit.clone();
3486        let layout = StandardLayout::new().with_path_exists(move |p| p == hit_probe);
3487        let miss = Path::new("exe/b");
3488        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3489        let err = layout
3490            .probe_sandboxed_declared_entries(
3491                [Path::new("exe/a"), miss],
3492                &root,
3493                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3494                &exe_dir,
3495                LayoutError::ExeOutsideDir,
3496            )
3497            .unwrap_err();
3498        assert_eq!(
3499            err,
3500            LayoutError::missing_entry(
3501                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3502                root.join(miss),
3503            ),
3504            "probe_sandboxed_declared_entries must short-circuit on the first missing \
3505             entry with a `MissingEntry` byte-equal to the sibling \
3506             probe_sandboxed_declared_entry primitive's miss wrap on that entry — the \
3507             pre-lift `for … {{ … ? }}` loop's first-error return semantics",
3508        );
3509    }
3510
3511    #[test]
3512    fn probe_sandboxed_declared_entries_folds_first_miss_short_circuits_before_later_paths() {
3513        // Diagnostic-order pin — the primitive must return on the *first*
3514        // miss in iterator order rather than probing every path and
3515        // returning the last miss (which would silently drop the pre-
3516        // lift `for … { … ? }` loop's first-error contract). Fixture:
3517        // the oracle rejects both paths, so a byte-equal `MissingEntry`
3518        // on the *first* path in the iterator distinguishes the two
3519        // return-order shapes. Sibling of the peer
3520        // `probe_declared_entries_folds_first_miss_short_circuits_before_later_paths`
3521        // pin on the bare-batch axis.
3522        let layout = StandardLayout::new().with_path_exists(|_| false);
3523        let root = PathBuf::from("/tmp/x");
3524        let first = Path::new("exe/first");
3525        let second = Path::new("exe/second");
3526        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3527        let err = layout
3528            .probe_sandboxed_declared_entries(
3529                [first, second],
3530                &root,
3531                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3532                &exe_dir,
3533                LayoutError::ExeOutsideDir,
3534            )
3535            .unwrap_err();
3536        assert_eq!(
3537            err,
3538            LayoutError::missing_entry(
3539                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3540                root.join(first),
3541            ),
3542            "probe_sandboxed_declared_entries must return on the *first* miss in iterator \
3543             order — a `MissingEntry` on the second path would silently drop the pre-lift \
3544             `for … {{ … ? }}` loop's first-error contract",
3545        );
3546    }
3547
3548    #[test]
3549    fn probe_sandboxed_declared_entries_folds_sandbox_escape_via_outside_ctor() {
3550        // Per-primitive equivalence pin on the sandbox-escape arm — a
3551        // [`StandardLayout`] whose oracle admits the probed resolved
3552        // path (so the miss arm passes) but whose resolved path lies
3553        // outside the caller-provided `sandbox_dir` yields the paired
3554        // `outside_ctor(full)` byte-equal under `PartialEq`. The
3555        // primitive threads the resolved `full` through the caller-
3556        // supplied `fn(PathBuf) -> LayoutError` constructor rather than
3557        // a hard-coded variant, so the fold does not silently collapse
3558        // onto one of the two `:exe` / `:servicos` outside-dir variants.
3559        let root = PathBuf::from("/tmp/x");
3560        let path = Path::new("lib/tool");
3561        let full = root.join(path);
3562        let full_probe = full.clone();
3563        let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3564        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3565        let err = layout
3566            .probe_sandboxed_declared_entries(
3567                [path],
3568                &root,
3569                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3570                &exe_dir,
3571                LayoutError::ExeOutsideDir,
3572            )
3573            .unwrap_err();
3574        assert_eq!(
3575            err,
3576            LayoutError::ExeOutsideDir(full),
3577            "probe_sandboxed_declared_entries sandbox-escape arm must route the resolved \
3578             `full` through the caller-supplied outside_ctor byte-equal to the pre-lift \
3579             `LayoutError::ExeOutsideDir(full)` tuple-literal",
3580        );
3581    }
3582
3583    #[test]
3584    fn probe_sandboxed_declared_entries_threads_outside_ctor_through_arg_verbatim() {
3585        // Cross-axis pin — the primitive must thread the caller-supplied
3586        // `outside_ctor` verbatim into the sandbox-escape arm's
3587        // `LayoutError` return, so the fold does not silently collapse
3588        // onto one hard-coded variant. Sweep both [`LayoutError`]
3589        // tuple-variants the two wire-up sites in
3590        // [`StandardLayout::verify`] pass — [`LayoutError::ExeOutsideDir`]
3591        // and [`LayoutError::ServicoOutsideDir`] — so a byte-drifted
3592        // ctor-routing on either axis would trip. Sibling of the peer
3593        // `probe_sandboxed_declared_entry_threads_outside_ctor_through_arg_verbatim`
3594        // pin on the per-arm sandboxed-primitive axis.
3595        let root = PathBuf::from("/tmp/x");
3596        let outside_dir = root.join(crate::render::LAYOUT_DIR_LIB);
3597        for (kind, sandbox_component, ctor) in [
3598            (
3599                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3600                crate::render::LAYOUT_DIR_EXE,
3601                LayoutError::ExeOutsideDir as fn(PathBuf) -> LayoutError,
3602            ),
3603            (
3604                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3605                crate::render::LAYOUT_DIR_SERVICOS,
3606                LayoutError::ServicoOutsideDir as fn(PathBuf) -> LayoutError,
3607            ),
3608        ] {
3609            let path = outside_dir.strip_prefix(&root).unwrap().join("tool");
3610            let full = root.join(&path);
3611            let full_probe = full.clone();
3612            let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3613            let sandbox_dir = root.join(sandbox_component);
3614            let err = layout
3615                .probe_sandboxed_declared_entries([&path], &root, kind, &sandbox_dir, ctor)
3616                .unwrap_err();
3617            assert_eq!(
3618                err,
3619                ctor(full),
3620                "probe_sandboxed_declared_entries must thread `outside_ctor` verbatim on \
3621                 every canonical `<Slot>OutsideDir` variant the two wire-up sites pass",
3622            );
3623        }
3624    }
3625
3626    #[test]
3627    fn probe_sandboxed_declared_entries_threads_kind_through_arg_verbatim() {
3628        // Cross-axis pin — sweep every canonical
3629        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] label the two
3630        // sandboxed-batch wire-up sites in [`StandardLayout::verify`]
3631        // pass through this primitive (`:exe`, `:servicos`). Each miss
3632        // must return a `MissingEntry` whose `kind:` field byte-equals
3633        // the caller-provided arg, so the fold does not silently
3634        // collapse onto one hard-coded label. Sibling of the peer
3635        // `probe_declared_entries_threads_kind_through_arg_verbatim`
3636        // pin's label sweep on the bare-batch axis.
3637        let layout = StandardLayout::new().with_path_exists(|_| false);
3638        let root = PathBuf::from("/tmp/x");
3639        let path = Path::new("some/entry");
3640        for (kind, sandbox_component, ctor) in [
3641            (
3642                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3643                crate::render::LAYOUT_DIR_EXE,
3644                LayoutError::ExeOutsideDir as fn(PathBuf) -> LayoutError,
3645            ),
3646            (
3647                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3648                crate::render::LAYOUT_DIR_SERVICOS,
3649                LayoutError::ServicoOutsideDir as fn(PathBuf) -> LayoutError,
3650            ),
3651        ] {
3652            let sandbox_dir = root.join(sandbox_component);
3653            let err = layout
3654                .probe_sandboxed_declared_entries([path], &root, kind, &sandbox_dir, ctor)
3655                .unwrap_err();
3656            assert_eq!(
3657                err,
3658                LayoutError::missing_entry(kind, root.join(path)),
3659                "probe_sandboxed_declared_entries must thread `kind` verbatim on every \
3660                 canonical sandboxed-batch label",
3661            );
3662        }
3663    }
3664
3665    #[test]
3666    fn probe_sandboxed_declared_entries_accepts_asref_path_shape_wire_up_sites_pass() {
3667        // Cross-axis pin — the primitive's `P: AsRef<Path>` bound must
3668        // admit every concrete iterator element type the two
3669        // [`StandardLayout::verify`] wire-up sites pass:
3670        // - `&String` (from `caixa.exe(): &[String]`),
3671        // - `&String` (from `caixa.servicos(): &[String]`).
3672        //
3673        // Byte-parity assertion: on a shared `/tmp/x/lib/demo`
3674        // fixture the miss-arm return must be byte-equal across
3675        // `&String` and the ergonomic `&Path` / `&PathBuf` element-type
3676        // flavors, so a future re-shape of the bound (a narrower `P:
3677        // Into<PathBuf>` collapse, a stricter `&Path`-only signature)
3678        // would surface here rather than at the caller wire-up site.
3679        // Sibling of the peer
3680        // `probe_declared_entries_accepts_asref_path_shape_wire_up_sites_pass`
3681        // pin on the bare-batch axis.
3682        let layout = StandardLayout::new().with_path_exists(|_| false);
3683        let root = PathBuf::from("/tmp/x");
3684        let literal = "exe/demo";
3685        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3686        let expected = LayoutError::missing_entry(
3687            crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3688            root.join(Path::new(literal)),
3689        );
3690
3691        let via_string_slice: Vec<String> = vec![literal.to_string()];
3692        let via_string_err = layout
3693            .probe_sandboxed_declared_entries(
3694                via_string_slice.iter(),
3695                &root,
3696                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3697                &exe_dir,
3698                LayoutError::ExeOutsideDir,
3699            )
3700            .unwrap_err();
3701        assert_eq!(
3702            via_string_err, expected,
3703            "probe_sandboxed_declared_entries must accept `&String` items (the `caixa.exe() \
3704             : &[String]` / `caixa.servicos(): &[String]` wire-up shape)",
3705        );
3706
3707        let via_path_slice: [&Path; 1] = [Path::new(literal)];
3708        let via_path_err = layout
3709            .probe_sandboxed_declared_entries(
3710                via_path_slice,
3711                &root,
3712                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3713                &exe_dir,
3714                LayoutError::ExeOutsideDir,
3715            )
3716            .unwrap_err();
3717        assert_eq!(
3718            via_path_err, expected,
3719            "probe_sandboxed_declared_entries must accept `&Path` items",
3720        );
3721
3722        let via_pathbuf_slice: Vec<PathBuf> = vec![PathBuf::from(literal)];
3723        let via_pathbuf_err = layout
3724            .probe_sandboxed_declared_entries(
3725                via_pathbuf_slice.iter(),
3726                &root,
3727                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3728                &exe_dir,
3729                LayoutError::ExeOutsideDir,
3730            )
3731            .unwrap_err();
3732        assert_eq!(
3733            via_pathbuf_err, expected,
3734            "probe_sandboxed_declared_entries must accept `&PathBuf` items",
3735        );
3736    }
3737
3738    #[test]
3739    fn probe_sandboxed_declared_entries_routes_miss_arm_through_probe_sandboxed_declared_entry() {
3740        // Cross-primitive pin — the batch primitive must route its miss
3741        // arm through the sibling
3742        // [`StandardLayout::probe_sandboxed_declared_entry`] primitive
3743        // rather than re-inlining the `probe_declared_entry` +
3744        // `starts_with` cascade, so a future edit to the miss-arm shape
3745        // on either primitive lands in exactly one place. Byte-parity
3746        // assertion: on a fixture that misses the oracle, the batch
3747        // primitive's `Err` arm must equal the peer per-arm
3748        // [`StandardLayout::probe_sandboxed_declared_entry`] primitive's
3749        // `Err` arm on the same fixture — otherwise the batch fold has
3750        // drifted from the substrate primitive. Same discipline the peer
3751        // `probe_declared_entries_routes_miss_arm_through_probe_declared_entry`
3752        // pin establishes on the sibling bare-batch axis.
3753        let layout = StandardLayout::new().with_path_exists(|_| false);
3754        let root = PathBuf::from("/tmp/x");
3755        let path = Path::new("servicos/hello-rio.computeunit.yaml");
3756        let sandbox_dir = root.join(crate::render::LAYOUT_DIR_SERVICOS);
3757        let via_batch = layout
3758            .probe_sandboxed_declared_entries(
3759                [path],
3760                &root,
3761                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3762                &sandbox_dir,
3763                LayoutError::ServicoOutsideDir,
3764            )
3765            .unwrap_err();
3766        let via_probe = layout
3767            .probe_sandboxed_declared_entry(
3768                path,
3769                &root,
3770                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3771                &sandbox_dir,
3772                LayoutError::ServicoOutsideDir,
3773            )
3774            .unwrap_err();
3775        assert_eq!(
3776            via_batch, via_probe,
3777            "probe_sandboxed_declared_entries's miss arm must equal the sibling \
3778             probe_sandboxed_declared_entry primitive's miss arm byte-equal — pins that \
3779             the batch primitive routes through the substrate primitive rather than \
3780             re-inlining the `probe_declared_entry` + `starts_with` cascade",
3781        );
3782    }
3783
3784    #[test]
3785    fn probe_sandboxed_declared_entries_routes_sandbox_escape_arm_through_probe_sandboxed_declared_entry()
3786     {
3787        // Cross-primitive pin — the batch primitive's sandbox-escape arm
3788        // must equal the sibling
3789        // [`StandardLayout::probe_sandboxed_declared_entry`] primitive's
3790        // sandbox-escape arm on the same fixture. Byte-parity assertion:
3791        // on a fixture where the oracle admits the probed path but the
3792        // resolved path lies outside sandbox_dir, both primitives must
3793        // return the same `outside_ctor(full)` byte-equal under
3794        // `PartialEq` — otherwise the batch fold has drifted from the
3795        // per-arm primitive on the second-arm dispatch.
3796        let root = PathBuf::from("/tmp/x");
3797        let path = Path::new("lib/escape");
3798        let full = root.join(path);
3799        let full_probe = full.clone();
3800        let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3801        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3802        let via_batch = layout
3803            .probe_sandboxed_declared_entries(
3804                [path],
3805                &root,
3806                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3807                &exe_dir,
3808                LayoutError::ExeOutsideDir,
3809            )
3810            .unwrap_err();
3811        let via_probe = layout
3812            .probe_sandboxed_declared_entry(
3813                path,
3814                &root,
3815                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3816                &exe_dir,
3817                LayoutError::ExeOutsideDir,
3818            )
3819            .unwrap_err();
3820        assert_eq!(
3821            via_batch, via_probe,
3822            "probe_sandboxed_declared_entries's sandbox-escape arm must equal the sibling \
3823             probe_sandboxed_declared_entry primitive's sandbox-escape arm byte-equal — \
3824             pins that the batch primitive routes through the substrate primitive on both \
3825             the miss and sandbox-escape arms rather than re-inlining either cascade",
3826        );
3827    }
3828
3829    // ── LayoutError::<nome-only> constructor family ──────────────────────
3830    //
3831    // The [`layout_nome_only_ctors!`] macro (below the `LayoutError` enum
3832    // definition) generates one static constructor per `<Variant>(String)`
3833    // tuple-variant that folds the uniform `Self::<Variant>(caixa.nome()
3834    // .to_string())` one-field construction onto one substrate primitive.
3835    // The per-variant equivalence pins below (fail-before-pass-after by
3836    // construction — a byte-mismatched macro arm would trip its
3837    // equivalence pin first) lock each generated constructor to its
3838    // tuple-literal peer under `PartialEq`, so every wire-up in
3839    // [`StandardLayout::verify`] on that variant produces a byte-equal
3840    // `LayoutError` to the pre-lift open-coded tuple-literal. The
3841    // cross-axis pin that follows (non-default `:nome`) routes the sole
3842    // constructor input axis through its declared accessor, so the fold
3843    // does not silently collapse onto the fixture default `:nome`.
3844
3845    fn layout_nome_only_ctor_fixture() -> Caixa {
3846        caixa(CaixaKind::Biblioteca)
3847    }
3848
3849    // Same rationale as `assert_violation_ctor_matches` / `assert_slot_
3850    // kind_ctor_matches` above: the helper terminates on the equality
3851    // check, so the owned-arg lint's general API-shape target does not
3852    // apply.
3853    #[allow(clippy::needless_pass_by_value)]
3854    fn assert_nome_only_ctor_matches(actual: LayoutError, expected: LayoutError) {
3855        assert_eq!(
3856            actual, expected,
3857            "generated constructor must produce byte-equal LayoutError to open-coded tuple-literal wrap",
3858        );
3859    }
3860
3861    #[test]
3862    fn binario_without_exe_ctor_matches_tuple_literal_wrap() {
3863        let c = layout_nome_only_ctor_fixture();
3864        assert_nome_only_ctor_matches(
3865            LayoutError::binario_without_exe(&c),
3866            LayoutError::BinarioWithoutExe(c.nome().to_string()),
3867        );
3868    }
3869
3870    #[test]
3871    fn servico_without_servicos_ctor_matches_tuple_literal_wrap() {
3872        let c = layout_nome_only_ctor_fixture();
3873        assert_nome_only_ctor_matches(
3874            LayoutError::servico_without_servicos(&c),
3875            LayoutError::ServicoWithoutServicos(c.nome().to_string()),
3876        );
3877    }
3878
3879    #[test]
3880    fn missing_ci_ctor_matches_tuple_literal_wrap() {
3881        let c = layout_nome_only_ctor_fixture();
3882        assert_nome_only_ctor_matches(
3883            LayoutError::missing_ci(&c),
3884            LayoutError::MissingCi(c.nome().to_string()),
3885        );
3886    }
3887
3888    #[test]
3889    fn supervisor_owns_code_ctor_matches_tuple_literal_wrap() {
3890        let c = layout_nome_only_ctor_fixture();
3891        assert_nome_only_ctor_matches(
3892            LayoutError::supervisor_owns_code(&c),
3893            LayoutError::SupervisorOwnsCode(c.nome().to_string()),
3894        );
3895    }
3896
3897    #[test]
3898    fn aplicacao_owns_code_ctor_matches_tuple_literal_wrap() {
3899        let c = layout_nome_only_ctor_fixture();
3900        assert_nome_only_ctor_matches(
3901            LayoutError::aplicacao_owns_code(&c),
3902            LayoutError::AplicacaoOwnsCode(c.nome().to_string()),
3903        );
3904    }
3905
3906    #[test]
3907    fn acao_owns_code_ctor_matches_tuple_literal_wrap() {
3908        let c = layout_nome_only_ctor_fixture();
3909        assert_nome_only_ctor_matches(
3910            LayoutError::acao_owns_code(&c),
3911            LayoutError::AcaoOwnsCode(c.nome().to_string()),
3912        );
3913    }
3914
3915    #[test]
3916    fn nome_only_ctor_routes_caixa_through_nome_accessor() {
3917        // Pin the fold's `caixa.nome().to_string()` sole-field construction
3918        // against a non-default `:nome` — the accessor threads the caller's
3919        // `:nome` verbatim into the tuple-variant, so the fold does not
3920        // silently collapse onto the default `"demo"` fixture nome. Peer of
3921        // the sibling `violation_ctor_routes_caixa_prefix_through_nome_
3922        // accessor` / `slot_kind_ctor_routes_caixa_prefix_through_nome_
3923        // accessor` pins on the `{ caixa, issue }` / `{ caixa, kind,
3924        // slots }` envelopes; extended here onto the sixth
3925        // `<Variant>(String)` envelope so every LayoutError-shape ctor
3926        // family guarantees the `:nome`-derived-caixa slot routes through
3927        // [`Caixa::nome`] rather than a hard-coded string. Sweeps the six
3928        // ctors in the [`layout_nome_only_ctors!`] macro so the pin covers
3929        // every generated arm.
3930        let mut c = caixa(CaixaKind::Biblioteca);
3931        c.nome = "alt-nome".into();
3932        assert_eq!(
3933            LayoutError::binario_without_exe(&c),
3934            LayoutError::BinarioWithoutExe("alt-nome".to_string()),
3935        );
3936        assert_eq!(
3937            LayoutError::servico_without_servicos(&c),
3938            LayoutError::ServicoWithoutServicos("alt-nome".to_string()),
3939        );
3940        assert_eq!(
3941            LayoutError::missing_ci(&c),
3942            LayoutError::MissingCi("alt-nome".to_string()),
3943        );
3944        assert_eq!(
3945            LayoutError::supervisor_owns_code(&c),
3946            LayoutError::SupervisorOwnsCode("alt-nome".to_string()),
3947        );
3948        assert_eq!(
3949            LayoutError::aplicacao_owns_code(&c),
3950            LayoutError::AplicacaoOwnsCode("alt-nome".to_string()),
3951        );
3952        assert_eq!(
3953            LayoutError::acao_owns_code(&c),
3954            LayoutError::AcaoOwnsCode("alt-nome".to_string()),
3955        );
3956    }
3957
3958    #[test]
3959    fn biblioteca_needs_default_lib_path() {
3960        let root = PathBuf::from("/tmp/x");
3961        let expect_manifest = root.join("caixa.lisp");
3962        let layout = StandardLayout::new().with_path_exists(move |p| p == expect_manifest);
3963        let err = layout
3964            .verify(&caixa(CaixaKind::Biblioteca), &root)
3965            .unwrap_err();
3966        assert!(matches!(err, LayoutError::MissingLib { .. }));
3967    }
3968
3969    #[test]
3970    fn biblioteca_passes_when_default_lib_exists() {
3971        let root = PathBuf::from("/tmp/x");
3972        let manifest = root.join("caixa.lisp");
3973        let default_lib = root.join("lib").join("demo.lisp");
3974        let layout =
3975            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
3976        layout
3977            .verify(&caixa(CaixaKind::Biblioteca), &root)
3978            .expect("should pass");
3979    }
3980
3981    #[test]
3982    fn binario_without_exe_errors() {
3983        let root = PathBuf::from("/tmp/x");
3984        let manifest = root.join("caixa.lisp");
3985        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3986        let err = layout
3987            .verify(&caixa(CaixaKind::Binario), &root)
3988            .unwrap_err();
3989        assert!(matches!(err, LayoutError::BinarioWithoutExe(_)));
3990    }
3991
3992    #[test]
3993    fn exe_outside_dir_errors() {
3994        // A relative entry that lives under the caixa root but *not*
3995        // under `exe/` — the canonical case the `starts_with(exe_dir)`
3996        // fence catches. The prior parent-escape shape this test used
3997        // (`"../sibling/tool"`) is now caught at validate time by
3998        // [`Caixa::validate_code_paths`] with the narrower
3999        // [`crate::ManifestError::CodePathParentEscape`] diagnostic
4000        // (see the layout-level integration pin
4001        // `code_path_violation_on_parent_escape_fires_before_existence_check`),
4002        // so this fence pin uses a non-`..` non-absolute shape outside
4003        // `exe/` to preserve coverage of the ExeOutsideDir surface.
4004        let root = PathBuf::from("/tmp/x");
4005        let manifest = root.join("caixa.lisp");
4006        let outside = root.join("lib/tool");
4007        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == outside);
4008        let mut c = caixa(CaixaKind::Binario);
4009        c.exe = vec!["lib/tool".into()];
4010        let err = layout.verify(&c, &root).unwrap_err();
4011        assert!(matches!(err, LayoutError::ExeOutsideDir(_)));
4012    }
4013
4014    // ── code-path shape gate (lifted to layout-level verify) ─────────────
4015
4016    #[test]
4017    fn code_path_violation_on_empty_bibliotecas_entry() {
4018        let root = PathBuf::from("/tmp/x");
4019        let manifest = root.join("caixa.lisp");
4020        let default_lib = root.join("lib").join("demo.lisp");
4021        let layout =
4022            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4023        let mut c = caixa(CaixaKind::Biblioteca);
4024        c.bibliotecas = vec![String::new()];
4025        let err = layout.verify(&c, &root).unwrap_err();
4026        // The wire-up wraps `ManifestError` Display into the
4027        // CodePathViolation envelope (peer of LimitsViolation /
4028        // BehaviorViolation / UpgradeViolation), so the issue string
4029        // names the offending slot at the source.
4030        let LayoutError::CodePathViolation { caixa, issue } = err else {
4031            panic!("expected LayoutError::CodePathViolation, got {err:?}");
4032        };
4033        assert_eq!(caixa, "demo");
4034        assert!(
4035            issue.contains(":bibliotecas"),
4036            "issue must name the offending slot: {issue}",
4037        );
4038    }
4039
4040    #[test]
4041    fn code_path_violation_on_absolute_servicos_entry() {
4042        let root = PathBuf::from("/tmp/x");
4043        let manifest = root.join("caixa.lisp");
4044        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
4045        let mut c = caixa(CaixaKind::Servico);
4046        c.servicos = vec!["/etc/servicos/escape.yaml".into()];
4047        let err = layout.verify(&c, &root).unwrap_err();
4048        let LayoutError::CodePathViolation { caixa, issue } = err else {
4049            panic!("expected LayoutError::CodePathViolation, got {err:?}");
4050        };
4051        assert_eq!(caixa, "demo");
4052        assert!(
4053            issue.contains(":servicos"),
4054            "issue must name the offending slot: {issue}",
4055        );
4056        assert!(
4057            issue.contains("/etc/servicos/escape.yaml"),
4058            "issue must quote the offending path: {issue}",
4059        );
4060    }
4061
4062    #[test]
4063    fn code_path_violation_on_parent_escape_fires_before_existence_check() {
4064        // The new gate runs BEFORE the existence loops, so a
4065        // parent-escaping `:exe` entry surfaces CodePathViolation
4066        // (naming `:exe` at the source) rather than the downstream
4067        // ExeOutsideDir / MissingEntry against the resolved sandbox-
4068        // escape path. Even if the resolved escape target exists
4069        // on disk (which we simulate here by claiming it does), the
4070        // shape diagnostic wins.
4071        let root = PathBuf::from("/tmp/x");
4072        let manifest = root.join("caixa.lisp");
4073        let resolved_escape = root.join("exe/../../escape.lisp");
4074        let layout =
4075            StandardLayout::new().with_path_exists(move |p| p == manifest || p == resolved_escape);
4076        let mut c = caixa(CaixaKind::Binario);
4077        c.exe = vec!["exe/../../escape.lisp".into()];
4078        let err = layout.verify(&c, &root).unwrap_err();
4079        let LayoutError::CodePathViolation { caixa, issue } = err else {
4080            panic!("expected LayoutError::CodePathViolation, got {err:?}");
4081        };
4082        assert_eq!(caixa, "demo");
4083        assert!(
4084            issue.contains(":exe"),
4085            "issue must name the offending slot: {issue}",
4086        );
4087    }
4088
4089    // ── etiquetas universal-axis gate wired into verify ─────────────────
4090    //
4091    // Pins the layout-pipeline wire-up of [`Caixa::validate_etiquetas`]:
4092    // the fourth universal-axis Caixa-level value-shape gate (peer of
4093    // `validate_nome` / `validate_versao` / `validate_deps` /
4094    // `validate_code_paths`), wired before the kind-coherence gates so
4095    // a structurally-invalid `:etiquetas` entry on any kind surfaces
4096    // the per-axis `EtiquetasViolation { caixa, issue }` envelope at
4097    // the source rather than silently rendering as `keywords: [""]`
4098    // in `Chart.yaml` (Servico kind, via caixa-helm's `BTreeSet`
4099    // collect) or silently dedup'ing at chart render (every kind).
4100    // Until this wire-up landed `:etiquetas` had no shape gate at any
4101    // layer — the registry-search-tag axis was the largest universal
4102    // authoring surface on the typed Caixa surface with no validate
4103    // discipline.
4104    //
4105    // Same per-axis `*Violation { caixa, issue }` envelope every peer
4106    // per-axis wrap exposes; the wire-up runs after `validate_deps`
4107    // (universal axis ordering: `:nome` → `:versao` → `:deps` →
4108    // `:etiquetas`) and before every kind-coherence gate
4109    // (`:etiquetas` is universal so its shape diagnostic is more
4110    // fundamental than the partition-on-kind diagnostics).
4111
4112    #[test]
4113    fn etiquetas_violation_on_empty_entry() {
4114        // Canonical paste-from-blank-doc footgun on every kind. The
4115        // wrap envelope wraps [`ManifestError::EtiquetaEmpty`]'s
4116        // Display through verbatim, so the issue string names the
4117        // offending `:etiquetas` axis at the source — the author can
4118        // grep their caixa.lisp for `:etiquetas` and fix the empty
4119        // entry in one edit. Mirrors the peer
4120        // `code_path_violation_on_empty_bibliotecas_entry` shape
4121        // (b868442) on the `:bibliotecas` axis.
4122        let root = PathBuf::from("/tmp/x");
4123        let manifest = root.join("caixa.lisp");
4124        let default_lib = root.join("lib").join("demo.lisp");
4125        let layout =
4126            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4127        let mut c = caixa(CaixaKind::Biblioteca);
4128        c.etiquetas = vec![String::new()];
4129        let err = layout.verify(&c, &root).unwrap_err();
4130        let LayoutError::EtiquetasViolation { caixa, issue } = err else {
4131            panic!("expected LayoutError::EtiquetasViolation, got {err:?}");
4132        };
4133        assert_eq!(caixa, "demo");
4134        assert!(
4135            issue.contains(":etiquetas"),
4136            "issue must name the offending slot: {issue}",
4137        );
4138    }
4139
4140    #[test]
4141    fn etiquetas_violation_on_duplicate_entry() {
4142        // Canonical copy-paste-the-wrong-tag footgun. Without the wire-
4143        // up the duplicate was silently dedup'd by caixa-helm's
4144        // `BTreeSet` collect at chart render — a "second wins / one
4145        // silently disappears" shape. The wrap envelope names the
4146        // offending tag verbatim through the inner
4147        // [`ManifestError::EtiquetaDuplicate`]'s Display.
4148        let root = PathBuf::from("/tmp/x");
4149        let manifest = root.join("caixa.lisp");
4150        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
4151        let mut c = caixa(CaixaKind::Servico);
4152        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
4153        c.etiquetas = vec!["demo".into(), "demo".into()];
4154        // The servicos path doesn't exist in this fixture, but the
4155        // `:etiquetas` gate fires before the existence loop (universal
4156        // axis dominates kind-specific existence checks). Wire is
4157        // intact iff the wrap envelope surfaces first.
4158        let err = layout.verify(&c, &root).unwrap_err();
4159        let LayoutError::EtiquetasViolation { caixa, issue } = err else {
4160            panic!("expected LayoutError::EtiquetasViolation, got {err:?}");
4161        };
4162        assert_eq!(caixa, "demo");
4163        assert!(
4164            issue.contains("demo"),
4165            "issue must quote the offending tag: {issue}",
4166        );
4167    }
4168
4169    #[test]
4170    fn etiquetas_violation_fires_before_kind_coherence_mesh_slot() {
4171        // Cross-axis precedence pin: a Biblioteca with malformed
4172        // `:etiquetas` *and* declared mesh slots (`:membros`) surfaces
4173        // the universal `:etiquetas` diagnostic first, not the
4174        // kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4175        // `:etiquetas` is universal (every kind owns the slot), so its
4176        // shape diagnostic is more fundamental than the partition-on-
4177        // kind diagnostic. Mirrors the peer
4178        // `deps_violation_fires_before_*` precedence pins (aa77d0f) on
4179        // the universal `:deps` axis vs the same kind-coherence gates.
4180        let root = PathBuf::from("/tmp/x");
4181        let manifest = root.join("caixa.lisp");
4182        let default_lib = root.join("lib").join("demo.lisp");
4183        let layout =
4184            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4185        let mut c = caixa(CaixaKind::Biblioteca);
4186        c.etiquetas = vec![String::new()];
4187        c.membros = vec![crate::aplicacao::Membro {
4188            caixa: "x".into(),
4189            versao: "^0.1".into(),
4190        }];
4191        let err = layout.verify(&c, &root).unwrap_err();
4192        assert!(
4193            matches!(err, LayoutError::EtiquetasViolation { .. }),
4194            "got {err:?}",
4195        );
4196    }
4197
4198    #[test]
4199    fn etiquetas_violation_fires_after_deps_violation() {
4200        // Cross-axis precedence pin (inside the universal-axis trio):
4201        // a caixa with both a malformed `:deps` entry *and* a malformed
4202        // `:etiquetas` entry surfaces `DepsViolation` first — `:deps`
4203        // is the third universal axis in declaration order
4204        // (`:nome` → `:versao` → `:deps` → `:etiquetas`) and runs first
4205        // in `verify`. Mirrors the peer
4206        // `nome_violation_fires_before_versao_violation` shape on the
4207        // identity-axis pair.
4208        let root = PathBuf::from("/tmp/x");
4209        let manifest = root.join("caixa.lisp");
4210        let default_lib = root.join("lib").join("demo.lisp");
4211        let layout =
4212            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4213        let mut c = caixa(CaixaKind::Biblioteca);
4214        c.deps = vec![crate::Dep::simple("Caixa-Teia", "^0.1")]; // uppercase :nome
4215        c.etiquetas = vec![String::new()];
4216        let err = layout.verify(&c, &root).unwrap_err();
4217        assert!(
4218            matches!(err, LayoutError::DepsViolation { .. }),
4219            "got {err:?}",
4220        );
4221    }
4222
4223    #[test]
4224    fn etiquetas_violation_accepts_canonical_template() {
4225        // Positive control sanity pin: the canonical `Caixa::template`
4226        // shape (`:etiquetas ()` — empty list) passes the gate
4227        // trivially. Mirrors the peer
4228        // `validate_code_paths_accepts_canonical_template` pin.
4229        let root = PathBuf::from("/tmp/x");
4230        let manifest = root.join("caixa.lisp");
4231        let default_lib = root.join("lib").join("demo.lisp");
4232        let layout =
4233            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4234        let c = caixa(CaixaKind::Biblioteca);
4235        layout.verify(&c, &root).expect("template must pass");
4236    }
4237
4238    #[test]
4239    fn etiquetas_violation_on_non_chart_keyword_shape() {
4240        // Canonical CSV-list-separator-confusion footgun: the author
4241        // confused the CSV-style separator with the `:etiquetas` list
4242        // grammar. The shape gate fires past the empty + duplicate
4243        // arms via [`Caixa::validate_etiquetas`]'s new
4244        // `is_chart_keyword_shape` cascade, and the layout envelope
4245        // wraps [`ManifestError::EtiquetaInvalid`]'s Display through
4246        // verbatim — the issue string names both the offending slot
4247        // and the offending value (debug-escaped). Peer with the
4248        // `autores_violation_on_non_chart_maintainer_shape` pin on
4249        // the sibling universal-axis `Vec<String>` surface — the
4250        // second layout pin on the Vec<String> per-entry shape
4251        // cascade.
4252        let root = PathBuf::from("/tmp/x");
4253        let manifest = root.join("caixa.lisp");
4254        let default_lib = root.join("lib").join("demo.lisp");
4255        let layout =
4256            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4257        let mut c = caixa(CaixaKind::Biblioteca);
4258        c.etiquetas = vec!["mesh,http,grpc".into()];
4259        let err = layout.verify(&c, &root).unwrap_err();
4260        let LayoutError::EtiquetasViolation { caixa, issue } = err else {
4261            panic!("expected LayoutError::EtiquetasViolation, got {err:?}");
4262        };
4263        assert_eq!(caixa, "demo");
4264        assert!(
4265            issue.contains(":etiquetas"),
4266            "issue must name the offending slot: {issue}",
4267        );
4268        assert!(
4269            issue.contains("mesh,http,grpc"),
4270            "issue must quote the offending value: {issue}",
4271        );
4272    }
4273
4274    // ── autores universal-axis gate wired into verify ───────────────────
4275    //
4276    // Pins the layout-pipeline wire-up of [`Caixa::validate_autores`]:
4277    // the fifth universal-axis Caixa-level value-shape gate (peer of
4278    // `validate_nome` / `validate_versao` / `validate_deps` /
4279    // `validate_etiquetas` / `validate_code_paths`), wired immediately
4280    // after `validate_etiquetas` so the two Vec-shaped universal
4281    // metadata axes sit adjacent in the cascade. Until this wire-up
4282    // landed `:autores` had no shape gate at any layer — the
4283    // maintainer-axis was the second largest universal authoring
4284    // surface on the typed Caixa surface with no validate discipline,
4285    // and unlike `:etiquetas` (caixa-helm dedups the rendered
4286    // `keywords:` array via `BTreeSet` collect at chart render),
4287    // `maintainers:` has *no* renderer-side dedup, so duplicate
4288    // `:autores` entries render verbatim as two identical
4289    // `Maintainer { name, email: None }` records — a strictly worse
4290    // footgun than the peer `:etiquetas` shape.
4291
4292    #[test]
4293    fn autores_violation_on_empty_entry() {
4294        // Canonical paste-from-blank-doc footgun on every kind. The
4295        // wrap envelope wraps [`ManifestError::AutorEmpty`]'s Display
4296        // through verbatim, so the issue string names the offending
4297        // `:autores` axis at the source — the author can grep their
4298        // caixa.lisp for `:autores` and fix the empty entry in one
4299        // edit. Mirrors the peer `etiquetas_violation_on_empty_entry`
4300        // shape (360a499) on the `:etiquetas` axis.
4301        let root = PathBuf::from("/tmp/x");
4302        let manifest = root.join("caixa.lisp");
4303        let default_lib = root.join("lib").join("demo.lisp");
4304        let layout =
4305            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4306        let mut c = caixa(CaixaKind::Biblioteca);
4307        c.autores = vec![String::new()];
4308        let err = layout.verify(&c, &root).unwrap_err();
4309        let LayoutError::AutoresViolation { caixa, issue } = err else {
4310            panic!("expected LayoutError::AutoresViolation, got {err:?}");
4311        };
4312        assert_eq!(caixa, "demo");
4313        assert!(
4314            issue.contains(":autores"),
4315            "issue must name the offending slot: {issue}",
4316        );
4317    }
4318
4319    #[test]
4320    fn autores_violation_on_duplicate_entry() {
4321        // Canonical copy-paste-the-wrong-author footgun. Unlike the
4322        // peer `:etiquetas` axis (silently dedup'd by caixa-helm's
4323        // `BTreeSet` collect at chart render), `:autores` duplicates
4324        // stack verbatim in the rendered `maintainers:` — the gate
4325        // closes the footgun at validate time before any renderer
4326        // sees it. The wrap envelope names the offending author
4327        // verbatim through the inner [`ManifestError::AutorDuplicate`]'s
4328        // Display.
4329        let root = PathBuf::from("/tmp/x");
4330        let manifest = root.join("caixa.lisp");
4331        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
4332        let mut c = caixa(CaixaKind::Servico);
4333        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
4334        c.autores = vec!["pleme-io".into(), "pleme-io".into()];
4335        // The servicos path doesn't exist in this fixture, but the
4336        // `:autores` gate fires before the existence loop (universal
4337        // axis dominates kind-specific existence checks). Wire is
4338        // intact iff the wrap envelope surfaces first.
4339        let err = layout.verify(&c, &root).unwrap_err();
4340        let LayoutError::AutoresViolation { caixa, issue } = err else {
4341            panic!("expected LayoutError::AutoresViolation, got {err:?}");
4342        };
4343        assert_eq!(caixa, "demo");
4344        assert!(
4345            issue.contains("pleme-io"),
4346            "issue must quote the offending author: {issue}",
4347        );
4348    }
4349
4350    #[test]
4351    fn autores_violation_fires_before_kind_coherence_mesh_slot() {
4352        // Cross-axis precedence pin: a Biblioteca with malformed
4353        // `:autores` *and* declared mesh slots (`:membros`) surfaces
4354        // the universal `:autores` diagnostic first, not the
4355        // kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4356        // `:autores` is universal (every kind owns the slot), so its
4357        // shape diagnostic is more fundamental than the partition-on-
4358        // kind diagnostic. Mirrors the peer
4359        // `etiquetas_violation_fires_before_kind_coherence_mesh_slot`
4360        // pin (360a499) on the `:etiquetas` axis vs the same kind-
4361        // coherence gates.
4362        let root = PathBuf::from("/tmp/x");
4363        let manifest = root.join("caixa.lisp");
4364        let default_lib = root.join("lib").join("demo.lisp");
4365        let layout =
4366            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4367        let mut c = caixa(CaixaKind::Biblioteca);
4368        c.autores = vec![String::new()];
4369        c.membros = vec![crate::aplicacao::Membro {
4370            caixa: "x".into(),
4371            versao: "^0.1".into(),
4372        }];
4373        let err = layout.verify(&c, &root).unwrap_err();
4374        assert!(
4375            matches!(err, LayoutError::AutoresViolation { .. }),
4376            "got {err:?}",
4377        );
4378    }
4379
4380    #[test]
4381    fn autores_violation_fires_after_etiquetas_violation() {
4382        // Cross-axis precedence pin (inside the Vec-shaped universal
4383        // metadata pair): a caixa with both a malformed `:etiquetas`
4384        // entry *and* a malformed `:autores` entry surfaces
4385        // `EtiquetasViolation` first — `:etiquetas` is the fourth
4386        // universal axis in the cascade and runs before `:autores`,
4387        // peer with the canonical identity-axis-first cascade the
4388        // peer gates establish. Mirrors the peer
4389        // `etiquetas_violation_fires_after_deps_violation` precedence
4390        // pin (360a499) on the dep-axis-before-tag-axis pair.
4391        let root = PathBuf::from("/tmp/x");
4392        let manifest = root.join("caixa.lisp");
4393        let default_lib = root.join("lib").join("demo.lisp");
4394        let layout =
4395            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4396        let mut c = caixa(CaixaKind::Biblioteca);
4397        c.etiquetas = vec![String::new()];
4398        c.autores = vec![String::new()];
4399        let err = layout.verify(&c, &root).unwrap_err();
4400        assert!(
4401            matches!(err, LayoutError::EtiquetasViolation { .. }),
4402            "got {err:?}",
4403        );
4404    }
4405
4406    #[test]
4407    fn autores_violation_accepts_canonical_template() {
4408        // Positive control sanity pin: the canonical `Caixa::template`
4409        // shape (`:autores ()` — empty list) passes the gate trivially.
4410        // Mirrors the peer `etiquetas_violation_accepts_canonical_template`
4411        // pin (360a499).
4412        let root = PathBuf::from("/tmp/x");
4413        let manifest = root.join("caixa.lisp");
4414        let default_lib = root.join("lib").join("demo.lisp");
4415        let layout =
4416            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4417        let c = caixa(CaixaKind::Biblioteca);
4418        layout.verify(&c, &root).expect("template must pass");
4419    }
4420
4421    #[test]
4422    fn autores_violation_on_non_chart_maintainer_shape() {
4423        // Canonical paste-from-multiline-doc footgun: the author
4424        // pasted a multi-line block of author records into one
4425        // `:autores` entry instead of splitting into one entry per
4426        // author. The shape gate fires past the empty + duplicate arms
4427        // via [`Caixa::validate_autores`]'s new
4428        // `is_chart_maintainer_name_shape` cascade, and the layout
4429        // envelope wraps [`ManifestError::AutorInvalid`]'s Display
4430        // through verbatim — the issue string names both the offending
4431        // slot and the offending value (debug-escaped). Peer with the
4432        // `descricao_violation_on_non_chart_shape` pin on the sibling
4433        // universal-axis `Option<String>` surface and the
4434        // `licenca_violation_on_non_spdx_shape` /
4435        // `edicao_violation_on_non_year_shape` peers — and the first
4436        // layout pin on the Vec<String> per-entry shape cascade.
4437        let root = PathBuf::from("/tmp/x");
4438        let manifest = root.join("caixa.lisp");
4439        let default_lib = root.join("lib").join("demo.lisp");
4440        let layout =
4441            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4442        let mut c = caixa(CaixaKind::Biblioteca);
4443        c.autores = vec!["alice\nbob".into()];
4444        let err = layout.verify(&c, &root).unwrap_err();
4445        let LayoutError::AutoresViolation { caixa, issue } = err else {
4446            panic!("expected LayoutError::AutoresViolation, got {err:?}");
4447        };
4448        assert_eq!(caixa, "demo");
4449        assert!(
4450            issue.contains(":autores"),
4451            "issue must name the offending slot: {issue}",
4452        );
4453        assert!(
4454            issue.contains("alice\\nbob"),
4455            "issue must quote the offending value (debug-escaped): {issue}",
4456        );
4457    }
4458
4459    // ── repositorio universal-axis gate wired into verify ────────────────
4460    //
4461    // Pins the layout-pipeline wire-up of [`Caixa::validate_repositorio`]:
4462    // the sixth universal-axis Caixa-level value-shape gate (peer of
4463    // `validate_nome` / `validate_versao` / `validate_deps` /
4464    // `validate_etiquetas` / `validate_autores` / `validate_code_paths`),
4465    // wired immediately after `validate_autores` so the universal
4466    // git-URL axis sits adjacent to the two Vec-shaped universal
4467    // metadata axes (`:etiquetas`, `:autores`) in the cascade. Until
4468    // this wire-up landed `:repositorio` had no shape gate at any
4469    // layer — the universal git-shaped homepage axis was the third
4470    // largest universal authoring surface on the typed Caixa with no
4471    // validate discipline, routing the same string through two
4472    // load-bearing substrate consumers (`caixa-helm`'s `Chart.yaml
4473    // home:` field and `caixa-flux`'s FluxCD `GitRepository.spec.url`)
4474    // via `Option::unwrap_or_else` fallbacks that only fire on `None` —
4475    // a `Some("")` silently passed every fallback and rendered as an
4476    // empty URL in both consumers, breaking at `helm template` /
4477    // FluxCD reconcile time far from the source `caixa.lisp`. The
4478    // gate closes the divergence and makes the two `git URL`-shaped
4479    // surfaces on the typed Caixa (`:repositorio` here, `:deps :fonte
4480    // :repo` peer routed through the same shared
4481    // `crate::render::is_git_repo_url` predicate) structurally
4482    // equivalent by construction.
4483
4484    #[test]
4485    fn repositorio_violation_on_empty_some() {
4486        // Canonical paste-from-blank-doc footgun on every kind. The
4487        // wrap envelope wraps [`ManifestError::RepositorioEmpty`]'s
4488        // Display through verbatim, so the issue string names the
4489        // offending `:repositorio` axis at the source — the author
4490        // can grep their caixa.lisp for `:repositorio ""` and fix the
4491        // empty value in one edit. Mirrors the peer
4492        // `autores_violation_on_empty_entry` shape (86c769b) on the
4493        // `:autores` axis.
4494        let root = PathBuf::from("/tmp/x");
4495        let manifest = root.join("caixa.lisp");
4496        let default_lib = root.join("lib").join("demo.lisp");
4497        let layout =
4498            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4499        let mut c = caixa(CaixaKind::Biblioteca);
4500        c.repositorio = Some(String::new());
4501        let err = layout.verify(&c, &root).unwrap_err();
4502        let LayoutError::RepositorioViolation { caixa, issue } = err else {
4503            panic!("expected LayoutError::RepositorioViolation, got {err:?}");
4504        };
4505        assert_eq!(caixa, "demo");
4506        assert!(
4507            issue.contains(":repositorio"),
4508            "issue must name the offending slot: {issue}",
4509        );
4510    }
4511
4512    #[test]
4513    fn repositorio_violation_on_malformed_shape() {
4514        // Canonical CLI-argument-injection footgun: a leading `-`
4515        // value (`-upload-pack=evil`) escapes the `git clone <repo>`
4516        // subprocess argument boundary at clone time. The shared
4517        // `is_git_repo_url` predicate — the same parser the peer
4518        // `:deps :fonte :repo` axis routes through via
4519        // `DepSource::validate` — refuses every leading-`-` shape at
4520        // validate time. The wrap envelope names the offending value
4521        // verbatim through the inner [`ManifestError::RepositorioInvalid`]'s
4522        // Display.
4523        let root = PathBuf::from("/tmp/x");
4524        let manifest = root.join("caixa.lisp");
4525        let default_lib = root.join("lib").join("demo.lisp");
4526        let layout =
4527            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4528        let mut c = caixa(CaixaKind::Biblioteca);
4529        c.repositorio = Some("-upload-pack=evil".into());
4530        let err = layout.verify(&c, &root).unwrap_err();
4531        let LayoutError::RepositorioViolation { caixa, issue } = err else {
4532            panic!("expected LayoutError::RepositorioViolation, got {err:?}");
4533        };
4534        assert_eq!(caixa, "demo");
4535        assert!(
4536            issue.contains("-upload-pack=evil"),
4537            "issue must quote the offending value: {issue}",
4538        );
4539    }
4540
4541    #[test]
4542    fn repositorio_violation_fires_before_kind_coherence_mesh_slot() {
4543        // Cross-axis precedence pin: a Biblioteca with malformed
4544        // `:repositorio` *and* declared mesh slots (`:membros`)
4545        // surfaces the universal `:repositorio` diagnostic first, not
4546        // the kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4547        // `:repositorio` is universal (every kind owns the slot), so
4548        // its shape diagnostic is more fundamental than the
4549        // partition-on-kind diagnostic. Mirrors the peer
4550        // `autores_violation_fires_before_kind_coherence_mesh_slot`
4551        // pin (86c769b) on the `:autores` axis vs the same
4552        // kind-coherence gates.
4553        let root = PathBuf::from("/tmp/x");
4554        let manifest = root.join("caixa.lisp");
4555        let default_lib = root.join("lib").join("demo.lisp");
4556        let layout =
4557            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4558        let mut c = caixa(CaixaKind::Biblioteca);
4559        c.repositorio = Some(String::new());
4560        c.membros = vec![crate::aplicacao::Membro {
4561            caixa: "x".into(),
4562            versao: "^0.1".into(),
4563        }];
4564        let err = layout.verify(&c, &root).unwrap_err();
4565        assert!(
4566            matches!(err, LayoutError::RepositorioViolation { .. }),
4567            "got {err:?}",
4568        );
4569    }
4570
4571    #[test]
4572    fn repositorio_violation_fires_after_autores_violation() {
4573        // Cross-axis precedence pin (inside the universal metadata
4574        // trio): a caixa with both a malformed `:autores` entry *and*
4575        // a malformed `:repositorio` value surfaces `AutoresViolation`
4576        // first — `:autores` is the fifth universal axis in the
4577        // cascade and runs before `:repositorio`, peer with the
4578        // canonical identity-axis-first cascade the peer gates
4579        // establish. Mirrors the peer
4580        // `autores_violation_fires_after_etiquetas_violation`
4581        // precedence pin (86c769b) on the tag-axis-before-author-axis
4582        // pair.
4583        let root = PathBuf::from("/tmp/x");
4584        let manifest = root.join("caixa.lisp");
4585        let default_lib = root.join("lib").join("demo.lisp");
4586        let layout =
4587            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4588        let mut c = caixa(CaixaKind::Biblioteca);
4589        c.autores = vec![String::new()];
4590        c.repositorio = Some(String::new());
4591        let err = layout.verify(&c, &root).unwrap_err();
4592        assert!(
4593            matches!(err, LayoutError::AutoresViolation { .. }),
4594            "got {err:?}",
4595        );
4596    }
4597
4598    #[test]
4599    fn repositorio_violation_accepts_canonical_template() {
4600        // Positive control sanity pin: the canonical `Caixa::template`
4601        // shape (omits `:repositorio` entirely → `None` on the typed
4602        // surface) passes the gate trivially — the gate is a no-op
4603        // when the author didn't author a value. Mirrors the peer
4604        // `autores_violation_accepts_canonical_template` pin (86c769b).
4605        let root = PathBuf::from("/tmp/x");
4606        let manifest = root.join("caixa.lisp");
4607        let default_lib = root.join("lib").join("demo.lisp");
4608        let layout =
4609            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4610        let c = caixa(CaixaKind::Biblioteca);
4611        layout.verify(&c, &root).expect("template must pass");
4612    }
4613
4614    #[test]
4615    fn repositorio_violation_accepts_canonical_github_shorthand() {
4616        // Positive control pin on the canonical pleme-io `:repositorio`
4617        // shape: the `github:org/repo` shorthand the README quickstart
4618        // and the `caixa-helm` / `caixa-mesh` / `caixa-flux` fixtures
4619        // all use passes the gate end-to-end. Closes the structural
4620        // equivalence between this surface and the peer `:deps :fonte
4621        // :repo` axis — both consume `crate::render::is_git_repo_url`
4622        // and both must agree on the same accepted shape set.
4623        let root = PathBuf::from("/tmp/x");
4624        let manifest = root.join("caixa.lisp");
4625        let default_lib = root.join("lib").join("demo.lisp");
4626        let layout =
4627            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4628        let mut c = caixa(CaixaKind::Biblioteca);
4629        c.repositorio = Some("github:pleme-io/hello-rio".into());
4630        layout.verify(&c, &root).expect("canonical shape must pass");
4631    }
4632
4633    // ── descricao universal-axis gate wired into verify ──────────────────
4634    //
4635    // Pins the layout-pipeline wire-up of [`Caixa::validate_descricao`]:
4636    // the seventh universal-axis Caixa-level value-shape gate (peer of
4637    // `validate_nome` / `validate_versao` / `validate_deps` /
4638    // `validate_etiquetas` / `validate_autores` / `validate_repositorio` /
4639    // `validate_code_paths`), wired immediately after `validate_repositorio`
4640    // so the universal free-form-prose axis sits adjacent to the
4641    // universal git-URL axis in the cascade. Until this wire-up landed
4642    // `:descricao` had no shape gate at any layer — the empty
4643    // `Some("")` silently passed both `caixa-helm` consumers'
4644    // `Option::unwrap_or_else(|| <fallback>)` (which only fire on
4645    // `None`) and rendered as `Chart.yaml description: ""` plus a
4646    // blank `README.md` header, breaking at `helm lint` time
4647    // (`WARNING [chart.metadata.description]: description is required`
4648    // on `apiVersion: v2` charts) far from the source `caixa.lisp`.
4649    // Closes the same `Some("")` skips-`unwrap_or_else` footgun the
4650    // peer `:repositorio` gate (577b0a9) closed, on the universal
4651    // free-form-prose summary axis.
4652
4653    #[test]
4654    fn descricao_violation_on_empty_some() {
4655        // Canonical paste-from-blank-doc footgun on every kind. The
4656        // wrap envelope wraps [`ManifestError::DescricaoEmpty`]'s
4657        // Display through verbatim, so the issue string names the
4658        // offending `:descricao` axis at the source — the author can
4659        // grep their caixa.lisp for `:descricao ""` and fix the empty
4660        // value in one edit. Mirrors the peer
4661        // `repositorio_violation_on_empty_some` shape (577b0a9) on
4662        // the sibling `Option<String>` `:repositorio` axis.
4663        let root = PathBuf::from("/tmp/x");
4664        let manifest = root.join("caixa.lisp");
4665        let default_lib = root.join("lib").join("demo.lisp");
4666        let layout =
4667            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4668        let mut c = caixa(CaixaKind::Biblioteca);
4669        c.descricao = Some(String::new());
4670        let err = layout.verify(&c, &root).unwrap_err();
4671        let LayoutError::DescricaoViolation { caixa, issue } = err else {
4672            panic!("expected LayoutError::DescricaoViolation, got {err:?}");
4673        };
4674        assert_eq!(caixa, "demo");
4675        assert!(
4676            issue.contains(":descricao"),
4677            "issue must name the offending slot: {issue}",
4678        );
4679    }
4680
4681    #[test]
4682    fn descricao_violation_fires_before_kind_coherence_mesh_slot() {
4683        // Cross-axis precedence pin: a Biblioteca with empty
4684        // `:descricao` *and* declared mesh slots (`:membros`)
4685        // surfaces the universal `:descricao` diagnostic first, not
4686        // the kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4687        // `:descricao` is universal (every kind owns the slot), so
4688        // its shape diagnostic is more fundamental than the
4689        // partition-on-kind diagnostic. Mirrors the peer
4690        // `repositorio_violation_fires_before_kind_coherence_mesh_slot`
4691        // pin (577b0a9) on the `:repositorio` axis vs the same
4692        // kind-coherence gates.
4693        let root = PathBuf::from("/tmp/x");
4694        let manifest = root.join("caixa.lisp");
4695        let default_lib = root.join("lib").join("demo.lisp");
4696        let layout =
4697            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4698        let mut c = caixa(CaixaKind::Biblioteca);
4699        c.descricao = Some(String::new());
4700        c.membros = vec![crate::aplicacao::Membro {
4701            caixa: "x".into(),
4702            versao: "^0.1".into(),
4703        }];
4704        let err = layout.verify(&c, &root).unwrap_err();
4705        assert!(
4706            matches!(err, LayoutError::DescricaoViolation { .. }),
4707            "got {err:?}",
4708        );
4709    }
4710
4711    #[test]
4712    fn descricao_violation_fires_after_repositorio_violation() {
4713        // Cross-axis precedence pin (inside the universal metadata
4714        // cascade): a caixa with both a malformed `:repositorio` *and*
4715        // an empty `:descricao` surfaces `RepositorioViolation`
4716        // first — `:repositorio` is the sixth universal axis in the
4717        // cascade and runs before `:descricao`, peer with the
4718        // canonical identity-axis-first cascade the peer gates
4719        // establish. Mirrors the peer
4720        // `repositorio_violation_fires_after_autores_violation`
4721        // precedence pin (577b0a9) on the autores-axis-before-
4722        // repositorio-axis pair.
4723        let root = PathBuf::from("/tmp/x");
4724        let manifest = root.join("caixa.lisp");
4725        let default_lib = root.join("lib").join("demo.lisp");
4726        let layout =
4727            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4728        let mut c = caixa(CaixaKind::Biblioteca);
4729        c.repositorio = Some(String::new());
4730        c.descricao = Some(String::new());
4731        let err = layout.verify(&c, &root).unwrap_err();
4732        assert!(
4733            matches!(err, LayoutError::RepositorioViolation { .. }),
4734            "got {err:?}",
4735        );
4736    }
4737
4738    #[test]
4739    fn descricao_violation_accepts_none() {
4740        // Positive control sanity pin: a caixa that omits
4741        // `:descricao` entirely (the canonical `Caixa::template` shape
4742        // carries `Some("FIXME — describe this caixa")`, but the
4743        // layout-test fixture defaults to `None`) passes the gate
4744        // trivially — the gate is a no-op when the author didn't
4745        // author a value. Mirrors the peer
4746        // `repositorio_violation_accepts_canonical_template` pin
4747        // (577b0a9).
4748        let root = PathBuf::from("/tmp/x");
4749        let manifest = root.join("caixa.lisp");
4750        let default_lib = root.join("lib").join("demo.lisp");
4751        let layout =
4752            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4753        let c = caixa(CaixaKind::Biblioteca);
4754        layout.verify(&c, &root).expect("None must pass");
4755    }
4756
4757    #[test]
4758    fn descricao_violation_accepts_canonical_summary() {
4759        // Positive control pin on the canonical pleme-io `:descricao`
4760        // shape: a short free-form prose summary the `caixa-helm` /
4761        // `caixa-flux` / `caixa-mesh` fixtures all carry passes the
4762        // gate end-to-end.
4763        let root = PathBuf::from("/tmp/x");
4764        let manifest = root.join("caixa.lisp");
4765        let default_lib = root.join("lib").join("demo.lisp");
4766        let layout =
4767            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4768        let mut c = caixa(CaixaKind::Biblioteca);
4769        c.descricao = Some("Canonical Rust→wasm32-wasip2 caixa Servico.".into());
4770        layout
4771            .verify(&c, &root)
4772            .expect("canonical summary must pass");
4773    }
4774
4775    #[test]
4776    fn descricao_violation_on_non_chart_shape() {
4777        // Shape-predicate wire-up pin: a malformed `:descricao` value
4778        // that's a non-empty `Some(s)` but carries a paste-from-
4779        // multiline-doc embedded newline surfaces the
4780        // `DescricaoViolation` envelope via the manifest-layer
4781        // `ManifestError::DescricaoInvalid` arm. Mirrors the peer
4782        // `descricao_violation_on_empty_some` shape on the empty arm
4783        // of the same axis and the peer
4784        // `licenca_violation_on_non_spdx_shape` shape on the sibling
4785        // `:licenca` axis. Until this gate landed a value like
4786        // `"Checkout\nflow."` (an embedded newline) or `"Checkout
4787        // flow. "` (a trailing whitespace) silently passed
4788        // `StandardLayout::verify` and landed in the rendered
4789        // Chart.yaml `description:` field as a YAML-illegal
4790        // multi-line scalar or a silently-trimmed whitespace
4791        // round-trip far from the source caixa.lisp.
4792        let root = PathBuf::from("/tmp/x");
4793        let manifest = root.join("caixa.lisp");
4794        let default_lib = root.join("lib").join("demo.lisp");
4795        let layout =
4796            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4797        let mut c = caixa(CaixaKind::Biblioteca);
4798        c.descricao = Some("Checkout\nflow.".into());
4799        let err = layout.verify(&c, &root).unwrap_err();
4800        let LayoutError::DescricaoViolation { caixa, issue } = err else {
4801            panic!("expected LayoutError::DescricaoViolation, got {err:?}");
4802        };
4803        assert_eq!(caixa, "demo");
4804        assert!(
4805            issue.contains(":descricao"),
4806            "issue must name the offending slot: {issue}",
4807        );
4808        // The wrapped `ManifestError::DescricaoInvalid` Display uses
4809        // `{descricao:?}` (Debug) so the embedded newline surfaces
4810        // debug-escaped as `\n` in the issue string.
4811        assert!(
4812            issue.contains("Checkout\\nflow."),
4813            "issue must quote the offending value (debug-escaped): {issue}",
4814        );
4815    }
4816
4817    // ── :licenca empty-Some shape wired into verify (universal axis) ──
4818    //
4819    // Until this wire-up landed `Caixa::validate_licenca` did not
4820    // exist — the universal SPDX-shaped license-expression axis had
4821    // no shape gate at any layer, so an empty `Some("")` silently
4822    // passed `Caixa::from_lisp` and `StandardLayout::verify` and
4823    // landed as a bare trailing period in the rendered
4824    // `lareira-<nome>` chart's `README.md` `## License` section via
4825    // the `caixa-helm` consumer's `caixa.licenca.clone().unwrap_or_else(||
4826    // "MIT".into())` (which only fires on `None`) at
4827    // `caixa-helm/src/lib.rs:361`. Closes the same `Some("")`
4828    // skips-`unwrap_or_else` footgun the peer `:repositorio`
4829    // (577b0a9) and `:descricao` (4e6db38) gates closed, on the
4830    // universal license-expression axis.
4831
4832    #[test]
4833    fn licenca_violation_on_empty_some() {
4834        // Canonical paste-from-blank-doc footgun on every kind. The
4835        // wrap envelope wraps [`ManifestError::LicencaEmpty`]'s
4836        // Display through verbatim, so the issue string names the
4837        // offending `:licenca` axis at the source — the author can
4838        // grep their caixa.lisp for `:licenca ""` and fix the empty
4839        // value in one edit. Mirrors the peer
4840        // `descricao_violation_on_empty_some` shape (4e6db38) on
4841        // the sibling `Option<String>` `:licenca` axis.
4842        let root = PathBuf::from("/tmp/x");
4843        let manifest = root.join("caixa.lisp");
4844        let default_lib = root.join("lib").join("demo.lisp");
4845        let layout =
4846            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4847        let mut c = caixa(CaixaKind::Biblioteca);
4848        c.licenca = Some(String::new());
4849        let err = layout.verify(&c, &root).unwrap_err();
4850        let LayoutError::LicencaViolation { caixa, issue } = err else {
4851            panic!("expected LayoutError::LicencaViolation, got {err:?}");
4852        };
4853        assert_eq!(caixa, "demo");
4854        assert!(
4855            issue.contains(":licenca"),
4856            "issue must name the offending slot: {issue}",
4857        );
4858    }
4859
4860    #[test]
4861    fn licenca_violation_fires_before_kind_coherence_mesh_slot() {
4862        // Cross-axis precedence pin: a Biblioteca with empty
4863        // `:licenca` *and* declared mesh slots (`:membros`)
4864        // surfaces the universal `:licenca` diagnostic first, not
4865        // the kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4866        // `:licenca` is universal (every kind owns the slot), so
4867        // its shape diagnostic is more fundamental than the
4868        // partition-on-kind diagnostic. Mirrors the peer
4869        // `descricao_violation_fires_before_kind_coherence_mesh_slot`
4870        // pin (4e6db38) on the `:descricao` axis vs the same
4871        // kind-coherence gates.
4872        let root = PathBuf::from("/tmp/x");
4873        let manifest = root.join("caixa.lisp");
4874        let default_lib = root.join("lib").join("demo.lisp");
4875        let layout =
4876            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4877        let mut c = caixa(CaixaKind::Biblioteca);
4878        c.licenca = Some(String::new());
4879        c.membros = vec![crate::aplicacao::Membro {
4880            caixa: "x".into(),
4881            versao: "^0.1".into(),
4882        }];
4883        let err = layout.verify(&c, &root).unwrap_err();
4884        assert!(
4885            matches!(err, LayoutError::LicencaViolation { .. }),
4886            "got {err:?}",
4887        );
4888    }
4889
4890    #[test]
4891    fn licenca_violation_fires_after_descricao_violation() {
4892        // Cross-axis precedence pin (inside the universal metadata
4893        // cascade): a caixa with both an empty `:descricao` *and*
4894        // an empty `:licenca` surfaces `DescricaoViolation`
4895        // first — `:descricao` is the seventh universal axis in the
4896        // cascade and runs before `:licenca`, peer with the
4897        // canonical identity-axis-first cascade the peer gates
4898        // establish. Mirrors the peer
4899        // `descricao_violation_fires_after_repositorio_violation`
4900        // precedence pin (4e6db38) on the repositorio-axis-before-
4901        // descricao-axis pair.
4902        let root = PathBuf::from("/tmp/x");
4903        let manifest = root.join("caixa.lisp");
4904        let default_lib = root.join("lib").join("demo.lisp");
4905        let layout =
4906            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4907        let mut c = caixa(CaixaKind::Biblioteca);
4908        c.descricao = Some(String::new());
4909        c.licenca = Some(String::new());
4910        let err = layout.verify(&c, &root).unwrap_err();
4911        assert!(
4912            matches!(err, LayoutError::DescricaoViolation { .. }),
4913            "got {err:?}",
4914        );
4915    }
4916
4917    #[test]
4918    fn licenca_violation_accepts_none() {
4919        // Positive control sanity pin: a caixa that omits `:licenca`
4920        // entirely (the layout-test fixture defaults to `None`)
4921        // passes the gate trivially — the gate is a no-op when the
4922        // author didn't author a value. Mirrors the peer
4923        // `descricao_violation_accepts_none` pin (4e6db38).
4924        let root = PathBuf::from("/tmp/x");
4925        let manifest = root.join("caixa.lisp");
4926        let default_lib = root.join("lib").join("demo.lisp");
4927        let layout =
4928            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4929        let c = caixa(CaixaKind::Biblioteca);
4930        layout.verify(&c, &root).expect("None must pass");
4931    }
4932
4933    #[test]
4934    fn licenca_violation_accepts_canonical_expression() {
4935        // Positive control pin on the canonical pleme-io `:licenca`
4936        // shape: a non-empty SPDX expression the `caixa-helm` /
4937        // `caixa-flux` / `caixa-mesh` fixtures all carry (`"MIT"`)
4938        // passes the gate end-to-end.
4939        let root = PathBuf::from("/tmp/x");
4940        let manifest = root.join("caixa.lisp");
4941        let default_lib = root.join("lib").join("demo.lisp");
4942        let layout =
4943            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4944        let mut c = caixa(CaixaKind::Biblioteca);
4945        c.licenca = Some("Apache-2.0 OR MIT".into());
4946        layout
4947            .verify(&c, &root)
4948            .expect("canonical SPDX expression must pass");
4949    }
4950
4951    #[test]
4952    fn licenca_violation_on_non_spdx_shape() {
4953        // Shape-predicate wire-up pin: a malformed `:licenca` value
4954        // that's a non-empty `Some(s)` but falls outside the SPDX
4955        // expression alphabet floor surfaces the `LicencaViolation`
4956        // envelope via the manifest-layer `ManifestError::LicencaInvalid`
4957        // arm. Mirrors the peer `licenca_violation_on_empty_some`
4958        // shape on the empty arm of the same axis and the peer
4959        // `edicao_violation_on_non_year_shape` shape on the sibling
4960        // `:edicao` axis. Until this gate landed a value like
4961        // `"Apache_2.0"` (an underscore-instead-of-hyphen typo) or
4962        // `"MIT, Apache-2.0"` (a comma-instead-of-`OR`-keyword
4963        // colloquial idiom) silently passed `StandardLayout::verify`
4964        // and landed in the rendered chart `README.md` `## License`
4965        // section + a future SPDX-aware Chart.yaml `license:`
4966        // emitter would refuse the value at `helm lint` time far
4967        // from the source caixa.lisp.
4968        let root = PathBuf::from("/tmp/x");
4969        let manifest = root.join("caixa.lisp");
4970        let default_lib = root.join("lib").join("demo.lisp");
4971        let layout =
4972            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4973        let mut c = caixa(CaixaKind::Biblioteca);
4974        c.licenca = Some("Apache_2.0".into());
4975        let err = layout.verify(&c, &root).unwrap_err();
4976        let LayoutError::LicencaViolation { caixa, issue } = err else {
4977            panic!("expected LayoutError::LicencaViolation, got {err:?}");
4978        };
4979        assert_eq!(caixa, "demo");
4980        assert!(
4981            issue.contains(":licenca"),
4982            "issue must name the offending slot: {issue}",
4983        );
4984        assert!(
4985            issue.contains("Apache_2.0"),
4986            "issue must quote the offending value: {issue}",
4987        );
4988    }
4989
4990    // ── :edicao empty-Some shape wired into verify (universal axis) ──
4991    //
4992    // Until this wire-up landed `Caixa::validate_edicao` did not
4993    // exist — the universal language-edition axis had no shape gate
4994    // at any layer, so an empty `Some("")` silently passed
4995    // `Caixa::from_lisp` and `StandardLayout::verify` and landed as a
4996    // bare `(:edicao "")` line in the rendered caixa.lisp, ready for
4997    // a future renderer-side consumer's `Option::unwrap_or_else`
4998    // (which only fires on `None`) to skip its fallback. Closes the
4999    // same `Some("")`-skips-`unwrap_or_else` footgun the peer
5000    // `:repositorio` (577b0a9), `:descricao` (4e6db38), and
5001    // `:licenca` (3d1e535) gates closed, on the universal language-
5002    // edition axis — the last un-gated universal-axis
5003    // `Option<String>` Caixa-level value-shape surface.
5004
5005    #[test]
5006    fn edicao_violation_on_empty_some() {
5007        // Canonical paste-from-blank-doc footgun on every kind. The
5008        // wrap envelope wraps [`ManifestError::EdicaoEmpty`]'s
5009        // Display through verbatim, so the issue string names the
5010        // offending `:edicao` axis at the source — the author can
5011        // grep their caixa.lisp for `:edicao ""` and fix the empty
5012        // value in one edit. Mirrors the peer
5013        // `licenca_violation_on_empty_some` shape (3d1e535) on the
5014        // sibling `Option<String>` `:edicao` axis.
5015        let root = PathBuf::from("/tmp/x");
5016        let manifest = root.join("caixa.lisp");
5017        let default_lib = root.join("lib").join("demo.lisp");
5018        let layout =
5019            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5020        let mut c = caixa(CaixaKind::Biblioteca);
5021        c.edicao = Some(String::new());
5022        let err = layout.verify(&c, &root).unwrap_err();
5023        let LayoutError::EdicaoViolation { caixa, issue } = err else {
5024            panic!("expected LayoutError::EdicaoViolation, got {err:?}");
5025        };
5026        assert_eq!(caixa, "demo");
5027        assert!(
5028            issue.contains(":edicao"),
5029            "issue must name the offending slot: {issue}",
5030        );
5031    }
5032
5033    #[test]
5034    fn edicao_violation_fires_before_kind_coherence_mesh_slot() {
5035        // Cross-axis precedence pin: a Biblioteca with empty
5036        // `:edicao` *and* declared mesh slots (`:membros`) surfaces
5037        // the universal `:edicao` diagnostic first, not the
5038        // kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
5039        // `:edicao` is universal (every kind owns the slot), so
5040        // its shape diagnostic is more fundamental than the
5041        // partition-on-kind diagnostic. Mirrors the peer
5042        // `licenca_violation_fires_before_kind_coherence_mesh_slot`
5043        // pin (3d1e535) on the `:licenca` axis vs the same
5044        // kind-coherence gates.
5045        let root = PathBuf::from("/tmp/x");
5046        let manifest = root.join("caixa.lisp");
5047        let default_lib = root.join("lib").join("demo.lisp");
5048        let layout =
5049            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5050        let mut c = caixa(CaixaKind::Biblioteca);
5051        c.edicao = Some(String::new());
5052        c.membros = vec![crate::aplicacao::Membro {
5053            caixa: "x".into(),
5054            versao: "^0.1".into(),
5055        }];
5056        let err = layout.verify(&c, &root).unwrap_err();
5057        assert!(
5058            matches!(err, LayoutError::EdicaoViolation { .. }),
5059            "got {err:?}",
5060        );
5061    }
5062
5063    #[test]
5064    fn edicao_violation_fires_after_licenca_violation() {
5065        // Cross-axis precedence pin (inside the universal metadata
5066        // cascade): a caixa with both an empty `:licenca` *and* an
5067        // empty `:edicao` surfaces `LicencaViolation` first —
5068        // `:licenca` is the eighth universal axis in the cascade
5069        // and runs before `:edicao`, peer with the canonical
5070        // identity-axis-first cascade the peer gates establish.
5071        // Mirrors the peer
5072        // `licenca_violation_fires_after_descricao_violation`
5073        // precedence pin (3d1e535) on the descricao-axis-before-
5074        // licenca-axis pair.
5075        let root = PathBuf::from("/tmp/x");
5076        let manifest = root.join("caixa.lisp");
5077        let default_lib = root.join("lib").join("demo.lisp");
5078        let layout =
5079            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5080        let mut c = caixa(CaixaKind::Biblioteca);
5081        c.licenca = Some(String::new());
5082        c.edicao = Some(String::new());
5083        let err = layout.verify(&c, &root).unwrap_err();
5084        assert!(
5085            matches!(err, LayoutError::LicencaViolation { .. }),
5086            "got {err:?}",
5087        );
5088    }
5089
5090    #[test]
5091    fn edicao_violation_accepts_none() {
5092        // Positive control sanity pin: a caixa that omits `:edicao`
5093        // entirely (the layout-test fixture defaults to `None`)
5094        // passes the gate trivially — the gate is a no-op when the
5095        // author didn't author a value. Mirrors the peer
5096        // `licenca_violation_accepts_none` pin (3d1e535).
5097        let root = PathBuf::from("/tmp/x");
5098        let manifest = root.join("caixa.lisp");
5099        let default_lib = root.join("lib").join("demo.lisp");
5100        let layout =
5101            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5102        let c = caixa(CaixaKind::Biblioteca);
5103        layout.verify(&c, &root).expect("None must pass");
5104    }
5105
5106    #[test]
5107    fn edicao_violation_accepts_canonical_value() {
5108        // Positive control pin on the canonical pleme-io `:edicao`
5109        // shape: the `"2026"` edition every `caixa-helm` /
5110        // `caixa-flux` / `caixa-mesh` / `caixa-core/src/render.rs`
5111        // fixture carries by construction passes the gate end-to-end.
5112        let root = PathBuf::from("/tmp/x");
5113        let manifest = root.join("caixa.lisp");
5114        let default_lib = root.join("lib").join("demo.lisp");
5115        let layout =
5116            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5117        let mut c = caixa(CaixaKind::Biblioteca);
5118        c.edicao = Some("2026".into());
5119        layout
5120            .verify(&c, &root)
5121            .expect("canonical edition must pass");
5122    }
5123
5124    #[test]
5125    fn edicao_violation_on_non_year_shape() {
5126        // Shape-predicate wire-up pin: a malformed `:edicao` value
5127        // that's a non-empty `Some(s)` but not a 4-digit ASCII
5128        // decimal year surfaces the `EdicaoViolation` envelope via
5129        // the manifest-layer `ManifestError::EdicaoInvalid` arm.
5130        // Mirrors the peer `edicao_violation_on_empty_some` shape
5131        // on the empty arm of the same axis. Until this gate landed
5132        // a value like `"v2026"` (a familiar git-tag idiom that
5133        // doesn't apply to the year-shaped edition axis) silently
5134        // passed `StandardLayout::verify` and broke at the
5135        // substrate's build-time edition selector far from the
5136        // source caixa.lisp.
5137        let root = PathBuf::from("/tmp/x");
5138        let manifest = root.join("caixa.lisp");
5139        let default_lib = root.join("lib").join("demo.lisp");
5140        let layout =
5141            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5142        let mut c = caixa(CaixaKind::Biblioteca);
5143        c.edicao = Some("v2026".into());
5144        let err = layout.verify(&c, &root).unwrap_err();
5145        let LayoutError::EdicaoViolation { caixa, issue } = err else {
5146            panic!("expected LayoutError::EdicaoViolation, got {err:?}");
5147        };
5148        assert_eq!(caixa, "demo");
5149        assert!(
5150            issue.contains(":edicao"),
5151            "issue must name the offending slot: {issue}",
5152        );
5153        assert!(
5154            issue.contains("v2026"),
5155            "issue must quote the offending value: {issue}",
5156        );
5157    }
5158
5159    // ── Caixa-identity gates (`:nome`, `:versao`) wired into verify ────
5160    //
5161    // Until this wire-up landed `Caixa::validate_nome` and
5162    // `Caixa::validate_versao` lived as `pub fn` on `Caixa` with full
5163    // per-arm unit coverage in `manifest::tests`, but no production
5164    // path called them — `feira build` silently accepted malformed
5165    // `:nome` / `:versao` and the failure surfaced at `helm install` /
5166    // `kubectl apply` / `feira publish` / lacre-resolve / `:upgrade-from
5167    // :from` matching time, far from the source `caixa.lisp`. The
5168    // following pins fence the layout-pipeline wire-up: every layout
5169    // verify on a structurally-invalid Caixa identity axis surfaces
5170    // the per-axis `*Violation { caixa, issue }` envelope before any
5171    // kind-coherence, code-path, or downstream gate sees it.
5172
5173    #[test]
5174    fn nome_violation_on_uppercase() {
5175        let root = PathBuf::from("/tmp/x");
5176        let manifest = root.join("caixa.lisp");
5177        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5178        let mut c = caixa(CaixaKind::Biblioteca);
5179        c.nome = "MyApp".into();
5180        let err = layout.verify(&c, &root).unwrap_err();
5181        let LayoutError::NomeViolation { caixa, issue } = err else {
5182            panic!("expected LayoutError::NomeViolation, got {err:?}");
5183        };
5184        assert_eq!(caixa, "MyApp");
5185        assert!(
5186            issue.contains("MyApp"),
5187            "issue must quote the offending nome: {issue}",
5188        );
5189    }
5190
5191    #[test]
5192    fn nome_violation_on_underscore() {
5193        let root = PathBuf::from("/tmp/x");
5194        let manifest = root.join("caixa.lisp");
5195        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5196        let mut c = caixa(CaixaKind::Biblioteca);
5197        c.nome = "my_app".into();
5198        let err = layout.verify(&c, &root).unwrap_err();
5199        assert!(
5200            matches!(err, LayoutError::NomeViolation { ref caixa, .. } if caixa == "my_app"),
5201            "got {err:?}",
5202        );
5203    }
5204
5205    #[test]
5206    fn nome_violation_on_empty() {
5207        // Empty `:nome` surfaces NomeViolation wrapping the narrower
5208        // `ManifestError::NomeEmpty` arm — the empty-first cascade the
5209        // peer per-axis name gates already use.
5210        let root = PathBuf::from("/tmp/x");
5211        let manifest = root.join("caixa.lisp");
5212        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5213        let mut c = caixa(CaixaKind::Biblioteca);
5214        c.nome = String::new();
5215        let err = layout.verify(&c, &root).unwrap_err();
5216        let LayoutError::NomeViolation { caixa, issue } = err else {
5217            panic!("expected LayoutError::NomeViolation, got {err:?}");
5218        };
5219        assert!(caixa.is_empty());
5220        assert!(
5221            issue.contains(":nome is empty"),
5222            "issue must surface the empty-arm diagnostic: {issue}",
5223        );
5224    }
5225
5226    #[test]
5227    fn versao_violation_on_missing_patch() {
5228        // `"0.1"` — the canonical "I shortened it" footgun. Helm /
5229        // OCI / lacre-resolve / `:upgrade-from :from` all strict-parse
5230        // through `semver::Version::parse`, which refuses a two-part
5231        // shape.
5232        let root = PathBuf::from("/tmp/x");
5233        let manifest = root.join("caixa.lisp");
5234        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5235        let mut c = caixa(CaixaKind::Biblioteca);
5236        c.versao = "0.1".into();
5237        let err = layout.verify(&c, &root).unwrap_err();
5238        let LayoutError::VersaoViolation { caixa, issue } = err else {
5239            panic!("expected LayoutError::VersaoViolation, got {err:?}");
5240        };
5241        assert_eq!(caixa, "demo");
5242        assert!(
5243            issue.contains("0.1"),
5244            "issue must quote the offending versao: {issue}",
5245        );
5246    }
5247
5248    #[test]
5249    fn versao_violation_on_git_tag_shape() {
5250        // `"v0.1.0"` — the git-tag-shape-leaking-into-versao typo.
5251        let root = PathBuf::from("/tmp/x");
5252        let manifest = root.join("caixa.lisp");
5253        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5254        let mut c = caixa(CaixaKind::Biblioteca);
5255        c.versao = "v0.1.0".into();
5256        let err = layout.verify(&c, &root).unwrap_err();
5257        assert!(
5258            matches!(err, LayoutError::VersaoViolation { ref issue, .. }
5259                if issue.contains("v0.1.0")),
5260            "got {err:?}",
5261        );
5262    }
5263
5264    #[test]
5265    fn versao_violation_on_empty() {
5266        let root = PathBuf::from("/tmp/x");
5267        let manifest = root.join("caixa.lisp");
5268        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5269        let mut c = caixa(CaixaKind::Biblioteca);
5270        c.versao = String::new();
5271        let err = layout.verify(&c, &root).unwrap_err();
5272        let LayoutError::VersaoViolation { caixa, issue } = err else {
5273            panic!("expected LayoutError::VersaoViolation, got {err:?}");
5274        };
5275        assert_eq!(caixa, "demo");
5276        assert!(
5277            issue.contains(":versao is empty"),
5278            "issue must surface the empty-arm diagnostic: {issue}",
5279        );
5280    }
5281
5282    #[test]
5283    fn nome_violation_fires_before_versao_violation() {
5284        // Precedence pin: when both `:nome` and `:versao` are malformed,
5285        // `:nome` surfaces first — the canonical declaration-order
5286        // precedence the `ManifestError` family establishes, the same
5287        // grep-order the author follows when fixing in `caixa.lisp`.
5288        let root = PathBuf::from("/tmp/x");
5289        let manifest = root.join("caixa.lisp");
5290        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5291        let mut c = caixa(CaixaKind::Biblioteca);
5292        c.nome = "MyApp".into();
5293        c.versao = "0.1".into();
5294        let err = layout.verify(&c, &root).unwrap_err();
5295        assert!(
5296            matches!(err, LayoutError::NomeViolation { .. }),
5297            "got {err:?} — nome must fire before versao",
5298        );
5299    }
5300
5301    #[test]
5302    fn nome_violation_fires_before_kind_coherence() {
5303        // Precedence pin: a Biblioteca caixa with a malformed `:nome`
5304        // AND a declared mesh slot surfaces NomeViolation, not
5305        // MeshSlotsOnNonAplicacao — the identity-axis gate is more
5306        // fundamental than the kind-coherence gate (which carries
5307        // `caixa.nome` verbatim in its diagnostic, and so depends on the
5308        // name being structurally valid to render a useful message).
5309        let root = PathBuf::from("/tmp/x");
5310        let manifest = root.join("caixa.lisp");
5311        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5312        let mut c = caixa(CaixaKind::Biblioteca);
5313        c.nome = "MyApp".into();
5314        c.membros = vec![crate::aplicacao::Membro {
5315            caixa: "x".into(),
5316            versao: "^0.1".into(),
5317        }];
5318        let err = layout.verify(&c, &root).unwrap_err();
5319        assert!(
5320            matches!(err, LayoutError::NomeViolation { .. }),
5321            "got {err:?} — nome must fire before MeshSlotsOnNonAplicacao",
5322        );
5323    }
5324
5325    #[test]
5326    fn nome_violation_fires_before_owncode() {
5327        // Precedence pin: a Supervisor with a malformed `:nome` AND
5328        // declared `:bibliotecas` surfaces NomeViolation, not
5329        // SupervisorOwnsCode — same rationale as above.
5330        let root = PathBuf::from("/tmp/x");
5331        let manifest = root.join("caixa.lisp");
5332        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5333        let mut c = caixa(CaixaKind::Supervisor);
5334        c.nome = "MyApp".into();
5335        c.bibliotecas = vec!["lib/x.lisp".into()];
5336        let err = layout.verify(&c, &root).unwrap_err();
5337        assert!(
5338            matches!(err, LayoutError::NomeViolation { .. }),
5339            "got {err:?} — nome must fire before SupervisorOwnsCode",
5340        );
5341    }
5342
5343    #[test]
5344    fn versao_violation_fires_before_kind_coherence() {
5345        // Precedence pin: a Biblioteca with a valid `:nome` but a
5346        // malformed `:versao` AND a declared servico slot surfaces
5347        // VersaoViolation before ServicoSlotsOnNonServico.
5348        let root = PathBuf::from("/tmp/x");
5349        let manifest = root.join("caixa.lisp");
5350        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5351        let mut c = caixa(CaixaKind::Biblioteca);
5352        c.versao = "v0.1.0".into();
5353        c.limits = Some(crate::LimitsSpec {
5354            memory: Some(64 * 1024 * 1024),
5355            ..Default::default()
5356        });
5357        let err = layout.verify(&c, &root).unwrap_err();
5358        assert!(
5359            matches!(err, LayoutError::VersaoViolation { .. }),
5360            "got {err:?} — versao must fire before ServicoSlotsOnNonServico",
5361        );
5362    }
5363
5364    #[test]
5365    fn nome_violation_fires_before_missing_lib() {
5366        // Precedence pin: a Biblioteca with a malformed `:nome` and no
5367        // lib entry surfaces NomeViolation, not MissingLib — the
5368        // identity-axis gate is more fundamental than the layout's
5369        // `lib/<nome>.lisp` default-path check (which derives the
5370        // expected path from `:nome` itself, so would surface a
5371        // misleading "expected lib/MyApp.lisp" diagnostic against an
5372        // unrecoverable name).
5373        let root = PathBuf::from("/tmp/x");
5374        let manifest = root.join("caixa.lisp");
5375        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5376        let mut c = caixa(CaixaKind::Biblioteca);
5377        c.nome = "MyApp".into();
5378        let err = layout.verify(&c, &root).unwrap_err();
5379        assert!(
5380            matches!(err, LayoutError::NomeViolation { .. }),
5381            "got {err:?} — nome must fire before MissingLib",
5382        );
5383    }
5384
5385    #[test]
5386    fn nome_versao_violations_fire_after_missing_manifest() {
5387        // Precedence pin: `MissingManifest` still dominates — there's
5388        // no caixa to identity-check when the manifest is missing.
5389        let root = PathBuf::from("/tmp/x");
5390        let layout = StandardLayout::new().with_path_exists(|_| false);
5391        let mut c = caixa(CaixaKind::Biblioteca);
5392        c.nome = "MyApp".into();
5393        c.versao = "0.1".into();
5394        let err = layout.verify(&c, &root).unwrap_err();
5395        assert!(
5396            matches!(err, LayoutError::MissingManifest(_)),
5397            "got {err:?} — MissingManifest must dominate identity gates",
5398        );
5399    }
5400
5401    #[test]
5402    fn valid_nome_versao_passes_to_downstream_gates() {
5403        // Sanity pin: the canonical "demo" / "0.1.0" identity passes
5404        // both axes; downstream gates (MissingLib here) take over.
5405        let root = PathBuf::from("/tmp/x");
5406        let manifest = root.join("caixa.lisp");
5407        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5408        let err = layout
5409            .verify(&caixa(CaixaKind::Biblioteca), &root)
5410            .unwrap_err();
5411        assert!(
5412            matches!(err, LayoutError::MissingLib { .. }),
5413            "got {err:?} — valid identity must pass to MissingLib",
5414        );
5415    }
5416
5417    // ── :deps / :deps-dev shape gate (lifted to layout-level verify) ─────
5418    //
5419    // Until this wire-up landed `Caixa::validate_deps` lived as `pub fn`
5420    // on `Caixa` with full per-arm unit coverage in `manifest::tests` +
5421    // `dep::tests` but no production path called it — `feira build`
5422    // silently accepted a malformed `:deps` / `:deps-dev` entry and the
5423    // failure surfaced at lacre-resolve / `git clone` / `cargo metadata`
5424    // / `helm install` time on the *first* downstream consumer to
5425    // strict-parse the value, far from the source `caixa.lisp` and
5426    // without any field naming the offending `:deps` axis. The following
5427    // pins fence the layout-pipeline wire-up: every layout verify on a
5428    // structurally-invalid `:deps` value-shape surfaces the per-axis
5429    // `DepsViolation { caixa, issue }` envelope (peer of
5430    // `NomeViolation` / `VersaoViolation` / `CodePathViolation` /
5431    // `LimitsViolation` / `BehaviorViolation` / `UpgradeViolation` /
5432    // `SupervisorViolation` / `AplicacaoViolation`) before any kind-
5433    // coherence, code-path, or downstream gate sees it.
5434
5435    #[test]
5436    fn deps_violation_on_empty_dep_nome() {
5437        // Empty `:nome` on a `:deps` entry surfaces the narrower
5438        // `DepError::NomeEmpty` arm through the wrap envelope.
5439        use crate::Dep;
5440        let root = PathBuf::from("/tmp/x");
5441        let manifest = root.join("caixa.lisp");
5442        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5443        let mut c = caixa(CaixaKind::Biblioteca);
5444        c.deps = vec![Dep::simple("", "^0.1")];
5445        let err = layout.verify(&c, &root).unwrap_err();
5446        let LayoutError::DepsViolation { caixa, issue } = err else {
5447            panic!("expected LayoutError::DepsViolation, got {err:?}");
5448        };
5449        assert_eq!(caixa, "demo");
5450        assert!(
5451            issue.contains(":deps") && issue.contains(":nome"),
5452            "issue must name the offending slot + axis: {issue}",
5453        );
5454    }
5455
5456    #[test]
5457    fn deps_violation_on_uppercase_dep_nome() {
5458        // Uppercase `:nome` on a `:deps` entry surfaces
5459        // `DepError::NomeInvalid` (DNS-1123 violation) through the wrap.
5460        use crate::Dep;
5461        let root = PathBuf::from("/tmp/x");
5462        let manifest = root.join("caixa.lisp");
5463        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5464        let mut c = caixa(CaixaKind::Biblioteca);
5465        c.deps = vec![Dep::simple("Caixa-Teia", "^0.1")];
5466        let err = layout.verify(&c, &root).unwrap_err();
5467        let LayoutError::DepsViolation { caixa, issue } = err else {
5468            panic!("expected LayoutError::DepsViolation, got {err:?}");
5469        };
5470        assert_eq!(caixa, "demo");
5471        assert!(
5472            issue.contains("Caixa-Teia"),
5473            "issue must quote the offending dep nome verbatim: {issue}",
5474        );
5475    }
5476
5477    #[test]
5478    fn deps_violation_on_unparseable_dep_versao() {
5479        // Unparseable `:versao` requirement on a `:deps` entry surfaces
5480        // `DepError::VersaoInvalid` through the wrap — the canonical
5481        // "the semver::Error reached the resolver, far from the source"
5482        // footgun closed at author time.
5483        use crate::Dep;
5484        let root = PathBuf::from("/tmp/x");
5485        let manifest = root.join("caixa.lisp");
5486        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5487        let mut c = caixa(CaixaKind::Biblioteca);
5488        c.deps = vec![Dep::simple("caixa-teia", "not-a-req")];
5489        let err = layout.verify(&c, &root).unwrap_err();
5490        let LayoutError::DepsViolation { caixa, issue } = err else {
5491            panic!("expected LayoutError::DepsViolation, got {err:?}");
5492        };
5493        assert_eq!(caixa, "demo");
5494        assert!(
5495            issue.contains("caixa-teia") && issue.contains("not-a-req"),
5496            "issue must quote the dep nome + offending versao: {issue}",
5497        );
5498    }
5499
5500    #[test]
5501    fn deps_violation_on_duplicate_nome_in_deps() {
5502        // Within-list `:deps :nome` duplicate surfaces
5503        // `DepError::DuplicateNome { list: ":deps" }` through the wrap.
5504        use crate::Dep;
5505        let root = PathBuf::from("/tmp/x");
5506        let manifest = root.join("caixa.lisp");
5507        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5508        let mut c = caixa(CaixaKind::Biblioteca);
5509        c.deps = vec![
5510            Dep::simple("caixa-teia", "^0.1"),
5511            Dep::simple("caixa-teia", "^0.2"),
5512        ];
5513        let err = layout.verify(&c, &root).unwrap_err();
5514        let LayoutError::DepsViolation { caixa, issue } = err else {
5515            panic!("expected LayoutError::DepsViolation, got {err:?}");
5516        };
5517        assert_eq!(caixa, "demo");
5518        assert!(
5519            issue.contains("caixa-teia") && issue.contains(":deps"),
5520            "issue must quote the duplicated nome + list: {issue}",
5521        );
5522    }
5523
5524    #[test]
5525    fn deps_violation_on_duplicate_nome_in_deps_dev() {
5526        // Within-list `:deps-dev :nome` duplicate surfaces the same
5527        // diagnostic on the dev-only axis — neither list is a
5528        // second-class citizen of the typed surface.
5529        use crate::Dep;
5530        let root = PathBuf::from("/tmp/x");
5531        let manifest = root.join("caixa.lisp");
5532        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5533        let mut c = caixa(CaixaKind::Biblioteca);
5534        c.deps_dev = vec![
5535            Dep::simple("caixa-teia", "^0.1"),
5536            Dep::simple("caixa-teia", "^0.2"),
5537        ];
5538        let err = layout.verify(&c, &root).unwrap_err();
5539        let LayoutError::DepsViolation { caixa, issue } = err else {
5540            panic!("expected LayoutError::DepsViolation, got {err:?}");
5541        };
5542        assert_eq!(caixa, "demo");
5543        assert!(
5544            issue.contains(":deps-dev"),
5545            "issue must name the offending list: {issue}",
5546        );
5547    }
5548
5549    #[test]
5550    fn deps_violation_in_deps_fires_before_deps_dev() {
5551        // Precedence pin: when *both* `:deps` and `:deps-dev` carry a
5552        // malformed entry, the `:deps` walk fires first — the canonical
5553        // declaration-order precedence `Caixa::validate_deps` establishes
5554        // (the same author-grep ordering the typed-graph peers use on
5555        // every other Vec-shaped surface).
5556        use crate::Dep;
5557        let root = PathBuf::from("/tmp/x");
5558        let manifest = root.join("caixa.lisp");
5559        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5560        let mut c = caixa(CaixaKind::Biblioteca);
5561        c.deps = vec![Dep::simple("Bad-In-Deps", "^0.1")];
5562        c.deps_dev = vec![Dep::simple("Bad-In-Deps-Dev", "^0.1")];
5563        let err = layout.verify(&c, &root).unwrap_err();
5564        let LayoutError::DepsViolation { caixa: _, issue } = err else {
5565            panic!("expected LayoutError::DepsViolation, got {err:?}");
5566        };
5567        assert!(
5568            issue.contains("Bad-In-Deps") && !issue.contains("Bad-In-Deps-Dev"),
5569            "issue must name the :deps offender, not :deps-dev: {issue}",
5570        );
5571    }
5572
5573    #[test]
5574    fn deps_violation_fires_after_versao_violation() {
5575        // Precedence pin: when both the top-level `:versao` and a `:deps`
5576        // entry are malformed, the Caixa-identity gate fires first — the
5577        // canonical declaration order on `Caixa` (`:nome` → `:versao` →
5578        // ... → `:deps`) and the same identity-axis-dominates-content-
5579        // axis discipline the peer `validate_nome` / `validate_versao`
5580        // wire-up established (1f74a5f). A malformed `:versao` would
5581        // otherwise quote `caixa.nome` against a downstream-shaped
5582        // diagnostic.
5583        use crate::Dep;
5584        let root = PathBuf::from("/tmp/x");
5585        let manifest = root.join("caixa.lisp");
5586        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5587        let mut c = caixa(CaixaKind::Biblioteca);
5588        c.versao = "v0.1.0".into();
5589        c.deps = vec![Dep::simple("Bad-Dep", "^0.1")];
5590        let err = layout.verify(&c, &root).unwrap_err();
5591        assert!(
5592            matches!(err, LayoutError::VersaoViolation { .. }),
5593            "got {err:?} — versao must fire before DepsViolation",
5594        );
5595    }
5596
5597    #[test]
5598    fn deps_violation_fires_before_kind_coherence() {
5599        // Precedence pin: a Supervisor with a malformed `:deps` entry
5600        // AND declared `:bibliotecas` (the canonical SupervisorOwnsCode
5601        // shape) surfaces DepsViolation, not SupervisorOwnsCode — the
5602        // dep surface is universal across all kinds and its shape gate
5603        // is more fundamental than the kind-coherence partitions on
5604        // `:bibliotecas` / `:exe` / `:servicos`. The author can fix the
5605        // dep typo without first being told to move their `:bibliotecas`
5606        // off a Supervisor.
5607        use crate::Dep;
5608        let root = PathBuf::from("/tmp/x");
5609        let manifest = root.join("caixa.lisp");
5610        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5611        let mut c = caixa(CaixaKind::Supervisor);
5612        c.deps = vec![Dep::simple("Bad-Dep", "^0.1")];
5613        c.bibliotecas = vec!["lib/x.lisp".into()];
5614        let err = layout.verify(&c, &root).unwrap_err();
5615        assert!(
5616            matches!(err, LayoutError::DepsViolation { .. }),
5617            "got {err:?} — DepsViolation must fire before SupervisorOwnsCode",
5618        );
5619    }
5620
5621    #[test]
5622    fn deps_violation_fires_after_missing_manifest() {
5623        // Precedence pin: `MissingManifest` still dominates — there's no
5624        // caixa to deps-check when the manifest is missing.
5625        use crate::Dep;
5626        let root = PathBuf::from("/tmp/x");
5627        let layout = StandardLayout::new().with_path_exists(|_| false);
5628        let mut c = caixa(CaixaKind::Biblioteca);
5629        c.deps = vec![Dep::simple("Bad-Dep", "^0.1")];
5630        let err = layout.verify(&c, &root).unwrap_err();
5631        assert!(
5632            matches!(err, LayoutError::MissingManifest(_)),
5633            "got {err:?} — MissingManifest must dominate the deps gate",
5634        );
5635    }
5636
5637    #[test]
5638    fn deps_violation_on_self_dep_in_deps() {
5639        // Cross-slot self-edge: a caixa whose `:deps` lists its own
5640        // `:nome` is rejected at the layout wire-up, the diagnostic
5641        // surfaces through the `DepsViolation` envelope with both the
5642        // offending list tag (`":deps"`) and the parent's `:nome`
5643        // verbatim. Until this wire-up landed the self-dep silently
5644        // passed `feira build` and the resolver's lacre-pipeline
5645        // closure walk either rejected mid-traversal (infinite
5646        // recursion detected far from the source caixa.lisp) or, on
5647        // the unbounded path, recursed until it exhausted its stack.
5648        // Mirrors the supervision-tree
5649        // [`supervisor_violation_on_self_supervision`] and the
5650        // Aplicacao-membership self-edge wire-up tests on the peer
5651        // typed-name-graph axes.
5652        use crate::Dep;
5653        let root = PathBuf::from("/tmp/x");
5654        let manifest = root.join("caixa.lisp");
5655        let default_lib = root.join("lib").join("demo.lisp");
5656        let layout =
5657            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5658        let mut c = caixa(CaixaKind::Biblioteca);
5659        c.deps = vec![Dep::simple("demo", "^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(":deps") && issue.contains("demo"),
5667            "issue must name the offending list + parent :nome: {issue}",
5668        );
5669    }
5670
5671    #[test]
5672    fn deps_violation_on_self_dep_in_deps_dev() {
5673        // Same cross-slot self-edge gate on the `:deps-dev` axis —
5674        // neither dep list is a second-class citizen of the typed
5675        // surface. The diagnostic names `:deps-dev` so the author can
5676        // grep their caixa.lisp for the offending block directly.
5677        use crate::Dep;
5678        let root = PathBuf::from("/tmp/x");
5679        let manifest = root.join("caixa.lisp");
5680        let default_lib = root.join("lib").join("demo.lisp");
5681        let layout =
5682            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5683        let mut c = caixa(CaixaKind::Biblioteca);
5684        c.deps_dev = vec![Dep::simple("demo", "^0.1")];
5685        let err = layout.verify(&c, &root).unwrap_err();
5686        let LayoutError::DepsViolation { caixa, issue } = err else {
5687            panic!("expected LayoutError::DepsViolation, got {err:?}");
5688        };
5689        assert_eq!(caixa, "demo");
5690        assert!(
5691            issue.contains(":deps-dev"),
5692            "issue must name the offending list: {issue}",
5693        );
5694    }
5695
5696    #[test]
5697    fn self_dep_fires_after_per_entry_dep_shape() {
5698        // Precedence pin: the per-entry shape gates of
5699        // [`Caixa::validate_deps`] (DNS-1123 / SemVer / fonte / etc.)
5700        // fire first on a self-dep entry whose `:nome` is malformed.
5701        // Same ordering posture every peer cross-slot gate uses
5702        // (`validate_no_self_supervision` after `SupervisorSpec::validate`,
5703        // `validate_no_self_membership` after `AplicacaoSpec::validate`).
5704        // A malformed self-dep `:nome` surfaces the narrower
5705        // per-entry diagnostic (which already names the parser-side
5706        // reason) before the self-edge gate sees the entry.
5707        use crate::Dep;
5708        let root = PathBuf::from("/tmp/x");
5709        let manifest = root.join("caixa.lisp");
5710        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5711        let mut c = caixa(CaixaKind::Biblioteca);
5712        // The parent is "demo" (DNS-1123 valid); the dep is "DEMO"
5713        // (DNS-1123 invalid). The per-entry shape gate fires on the
5714        // upper-case nome, masking the self-edge gate (and that's the
5715        // canonical precedence — fix the dep shape first, then the
5716        // structural self-edge becomes the next live diagnostic).
5717        c.deps = vec![Dep::simple("DEMO", "^0.1")];
5718        let err = layout.verify(&c, &root).unwrap_err();
5719        let LayoutError::DepsViolation { caixa: _, issue } = err else {
5720            panic!("expected LayoutError::DepsViolation, got {err:?}");
5721        };
5722        assert!(
5723            issue.contains("DNS-1123"),
5724            "issue must be the per-entry shape diagnostic, not the self-edge gate: {issue}",
5725        );
5726    }
5727
5728    #[test]
5729    fn valid_deps_pass_to_downstream_gates() {
5730        // Positive control pin: the canonical authoring shape (one
5731        // `:deps` entry naming a DNS-1123 nome + Cargo-shaped requirement,
5732        // one `:deps-dev` entry on a distinct nome) passes the dep gate;
5733        // downstream gates (MissingLib here) take over. Drift here =
5734        // a future tighten that rejects any canonical shape surfaces as
5735        // a regression at this layout-level pin.
5736        use crate::Dep;
5737        let root = PathBuf::from("/tmp/x");
5738        let manifest = root.join("caixa.lisp");
5739        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5740        let mut c = caixa(CaixaKind::Biblioteca);
5741        c.deps = vec![Dep::simple("caixa-teia", "^0.1")];
5742        c.deps_dev = vec![Dep::simple("caixa-lint", "^0.2")];
5743        let err = layout.verify(&c, &root).unwrap_err();
5744        assert!(
5745            matches!(err, LayoutError::MissingLib { .. }),
5746            "got {err:?} — valid deps must pass to MissingLib",
5747        );
5748    }
5749
5750    #[test]
5751    fn code_path_gate_runs_after_foreign_code_slot_gate() {
5752        // Precedence pin: a Servico that declares `:exe` (foreign code
5753        // surface) surfaces ForeignCodeSlot, *not* a per-entry path
5754        // shape diagnostic, even when the `:exe` entry is itself
5755        // malformed. The kind-coherence gate is the load-bearing
5756        // diagnostic at this site — once the slot is moved off the
5757        // wrong kind, the per-entry shape gate becomes the next live
5758        // diagnostic.
5759        let root = PathBuf::from("/tmp/x");
5760        let manifest = root.join("caixa.lisp");
5761        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5762        let mut c = caixa(CaixaKind::Servico);
5763        c.servicos = vec!["servicos/ok.yaml".into()];
5764        c.exe = vec!["/etc/foreign".into()];
5765        let err = layout.verify(&c, &root).unwrap_err();
5766        assert!(
5767            matches!(err, LayoutError::ForeignCodeSlot { .. }),
5768            "expected ForeignCodeSlot (kind-coherence wins over per-entry shape), got {err:?}",
5769        );
5770    }
5771
5772    // ── M2 typed-substrate invariants ────────────────────────────────────
5773
5774    #[test]
5775    fn behavior_callback_path_must_exist() {
5776        use crate::BehaviorSpec;
5777        use std::path::PathBuf;
5778        let root = PathBuf::from("/tmp/x");
5779        let manifest = root.join("caixa.lisp");
5780        let mut c = caixa(CaixaKind::Servico);
5781        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5782        let svc = root.join("servicos/demo.computeunit.yaml");
5783        c.behavior = Some(BehaviorSpec {
5784            on_init: Some(PathBuf::from("lib/init.lisp")),
5785            ..Default::default()
5786        });
5787        let manifest_clone = manifest.clone();
5788        let svc_clone = svc.clone();
5789        let layout =
5790            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
5791        let err = layout.verify(&c, &root).unwrap_err();
5792        assert!(matches!(
5793            err,
5794            LayoutError::MissingEntry { kind, .. }
5795                if kind == crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK
5796        ));
5797
5798        // Now declare the path exists — passes.
5799        let init = root.join("lib/init.lisp");
5800        let layout =
5801            StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc || p == init);
5802        layout.verify(&c, &root).unwrap();
5803    }
5804
5805    #[test]
5806    fn behavior_absolute_callback_is_violation_not_missing() {
5807        // An absolute path silently subverts `root.join(p)` (Path::join
5808        // replaces the base when the right side is absolute). Before
5809        // BehaviorSpec::validate ran, an `:on-init "/etc/passwd"` would
5810        // surface as a confusing "missing behavior-callback /etc/passwd"
5811        // — or, worse, pass when /etc/passwd happens to exist. Now it's
5812        // a value-shape error naming the slot.
5813        use crate::BehaviorSpec;
5814        let root = PathBuf::from("/tmp/x");
5815        let manifest = root.join("caixa.lisp");
5816        let svc = root.join("servicos/demo.computeunit.yaml");
5817        let mut c = caixa(CaixaKind::Servico);
5818        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5819        c.behavior = Some(BehaviorSpec {
5820            on_init: Some(PathBuf::from("/etc/passwd")),
5821            ..Default::default()
5822        });
5823        // Path exists check would *succeed* on /etc/passwd (proving the
5824        // sandbox bypass) — value-shape pass must fire first.
5825        let layout = StandardLayout::new()
5826            .with_path_exists(move |p| p == manifest || p == svc || p == Path::new("/etc/passwd"));
5827        let err = layout.verify(&c, &root).unwrap_err();
5828        assert!(
5829            matches!(err, LayoutError::BehaviorViolation { ref caixa, .. } if caixa == "demo"),
5830            "got {err:?}",
5831        );
5832    }
5833
5834    #[test]
5835    fn behavior_empty_callback_is_violation() {
5836        use crate::BehaviorSpec;
5837        let root = PathBuf::from("/tmp/x");
5838        let manifest = root.join("caixa.lisp");
5839        let svc = root.join("servicos/demo.computeunit.yaml");
5840        let mut c = caixa(CaixaKind::Servico);
5841        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5842        c.behavior = Some(BehaviorSpec {
5843            on_call: Some(PathBuf::new()),
5844            ..Default::default()
5845        });
5846        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
5847        let err = layout.verify(&c, &root).unwrap_err();
5848        assert!(matches!(err, LayoutError::BehaviorViolation { .. }));
5849    }
5850
5851    #[test]
5852    fn upgrade_from_duplicate_surfaces_as_upgrade_violation() {
5853        // Wiring pin: the cross-entry duplicate-`:from` gate in
5854        // `validate_upgrade_from` lands on the same
5855        // `LayoutError::UpgradeViolation` axis the per-entry
5856        // `UpgradeFromEntry::validate` already does (26da2c7), so a
5857        // caixa.lisp with two `(:from "0.1.0" …)` blocks surfaces at
5858        // `feira build` time naming the offending caixa rather than
5859        // silently passing into the wasm-operator's non-deterministic
5860        // dispatch. Mirrors `behavior_empty_callback_is_violation` on
5861        // the peer M2 typed slot.
5862        use crate::{UpgradeFromEntry, UpgradeInstruction};
5863        let root = PathBuf::from("/tmp/x");
5864        let manifest = root.join("caixa.lisp");
5865        let svc = root.join("servicos/demo.computeunit.yaml");
5866        let mut c = caixa(CaixaKind::Servico);
5867        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5868        c.upgrade_from = vec![
5869            UpgradeFromEntry {
5870                from: "0.1.0".into(),
5871                instructions: vec![UpgradeInstruction::Restart],
5872            },
5873            UpgradeFromEntry {
5874                from: "0.1.0".into(),
5875                instructions: vec![UpgradeInstruction::Restart],
5876            },
5877        ];
5878        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
5879        let err = layout.verify(&c, &root).unwrap_err();
5880        let LayoutError::UpgradeViolation { caixa, issue } = err else {
5881            panic!("expected LayoutError::UpgradeViolation for duplicate `:from`, got {err:?}");
5882        };
5883        assert_eq!(caixa, "demo");
5884        assert!(
5885            issue.contains("0.1.0"),
5886            "UpgradeViolation issue must name the offending `:from` verbatim, got {issue:?}"
5887        );
5888    }
5889
5890    #[test]
5891    fn upgrade_from_downgrade_surfaces_as_upgrade_violation() {
5892        // Wiring pin: the cross-slot precedence gate in
5893        // `validate_upgrade_from_against_versao` lands on the same
5894        // `LayoutError::UpgradeViolation` axis the per-entry and
5895        // cross-entry gates already do (26da2c7, 7c6aef2), so a
5896        // caixa.lisp whose `:upgrade-from :from` is greater than the
5897        // caixa's own `:versao` surfaces at `feira build` time
5898        // naming the offending caixa rather than silently passing
5899        // into the wasm-operator's `:from`-match dispatch where the
5900        // entry would sit dormant forever. Mirrors
5901        // `upgrade_from_duplicate_surfaces_as_upgrade_violation` on
5902        // the peer cross-entry gate.
5903        use crate::{UpgradeFromEntry, UpgradeInstruction};
5904        let root = PathBuf::from("/tmp/x");
5905        let manifest = root.join("caixa.lisp");
5906        let svc = root.join("servicos/demo.computeunit.yaml");
5907        let mut c = caixa(CaixaKind::Servico);
5908        c.versao = "0.1.5".into();
5909        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5910        c.upgrade_from = vec![UpgradeFromEntry {
5911            from: "0.2.0".into(),
5912            instructions: vec![UpgradeInstruction::Restart],
5913        }];
5914        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
5915        let err = layout.verify(&c, &root).unwrap_err();
5916        let LayoutError::UpgradeViolation { caixa, issue } = err else {
5917            panic!(
5918                "expected LayoutError::UpgradeViolation for downgrade-shaped `:from`, got {err:?}"
5919            );
5920        };
5921        assert_eq!(caixa, "demo");
5922        assert!(
5923            issue.contains("0.2.0") && issue.contains("0.1.5"),
5924            "UpgradeViolation issue must name both `:from` and `:versao` verbatim, got {issue:?}"
5925        );
5926    }
5927
5928    #[test]
5929    fn upgrade_from_equal_to_versao_surfaces_as_upgrade_violation() {
5930        // Self-upgrade no-op arm: `:from "0.1.0"` while
5931        // `:versao "0.1.0"` declares "upgrade from myself to
5932        // myself", which the operator's dispatch either skips
5933        // silently or trivially "succeeds" with no observable
5934        // transition. Surfaces at validate time naming both values
5935        // so the author can fix in one edit.
5936        use crate::{UpgradeFromEntry, UpgradeInstruction};
5937        let root = PathBuf::from("/tmp/x");
5938        let manifest = root.join("caixa.lisp");
5939        let svc = root.join("servicos/demo.computeunit.yaml");
5940        let mut c = caixa(CaixaKind::Servico);
5941        c.versao = "0.1.0".into();
5942        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5943        c.upgrade_from = vec![UpgradeFromEntry {
5944            from: "0.1.0".into(),
5945            instructions: vec![UpgradeInstruction::Restart],
5946        }];
5947        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
5948        let err = layout.verify(&c, &root).unwrap_err();
5949        let LayoutError::UpgradeViolation { caixa, issue } = err else {
5950            panic!(
5951                "expected LayoutError::UpgradeViolation for self-upgrade `:from == :versao`, got \
5952                 {err:?}"
5953            );
5954        };
5955        assert_eq!(caixa, "demo");
5956        assert!(
5957            issue.contains("0.1.0"),
5958            "UpgradeViolation issue must name the equal `:from`/`:versao` verbatim, got {issue:?}"
5959        );
5960    }
5961
5962    #[test]
5963    fn upgrade_from_strict_upgrade_passes_layout() {
5964        // Positive control for the precedence gate at the
5965        // LayoutInvariants level: a valid `:from < :versao` chain
5966        // (`0.1.0 → 0.2.0`) must not regress into a false-positive
5967        // `UpgradeViolation`. Mirrors `behavior_callback_path_must_exist`'s
5968        // positive-control arm.
5969        use crate::{UpgradeFromEntry, UpgradeInstruction};
5970        let root = PathBuf::from("/tmp/x");
5971        let manifest = root.join("caixa.lisp");
5972        let svc = root.join("servicos/demo.computeunit.yaml");
5973        let mut c = caixa(CaixaKind::Servico);
5974        c.versao = "0.2.0".into();
5975        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5976        c.upgrade_from = vec![UpgradeFromEntry {
5977            from: "0.1.0".into(),
5978            instructions: vec![UpgradeInstruction::Restart],
5979        }];
5980        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
5981        layout.verify(&c, &root).unwrap();
5982    }
5983
5984    #[test]
5985    fn upgrade_script_path_must_exist() {
5986        use crate::{BehaviorSpec, UpgradeFromEntry, UpgradeInstruction};
5987        use std::path::PathBuf;
5988        let root = PathBuf::from("/tmp/x");
5989        let manifest = root.join("caixa.lisp");
5990        let svc = root.join("servicos/demo.computeunit.yaml");
5991        let on_state_change = root.join("lib/migrations.lisp");
5992        let mut c = caixa(CaixaKind::Servico);
5993        // `:versao` past the entry's `:from` so the cross-slot
5994        // precedence gate (`FromNotBeforeVersao`) lets this case
5995        // through to the path-existence pass under test.
5996        c.versao = "0.2.0".into();
5997        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5998        // `:on-state-change` declared so the cross-slot composition
5999        // gate (`validate_upgrade_from_against_behavior`) lets the
6000        // `:state-change` entry through to the path-existence pass
6001        // under test. Without the callback the missing-callback gate
6002        // would surface first and the path-existence pass wouldn't be
6003        // exercised.
6004        c.behavior = Some(BehaviorSpec {
6005            on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
6006            ..Default::default()
6007        });
6008        // A `:load-module` precedes the `:state-change` so the entry
6009        // satisfies the within-entry state-change-ordering gate
6010        // (`StateChangeWithoutPriorLoad`) and the path-existence pass
6011        // under test is the gate actually exercised. `:load-module`
6012        // carries no on-disk path, so it adds no existence requirement.
6013        c.upgrade_from = vec![UpgradeFromEntry {
6014            from: "0.1.0".into(),
6015            instructions: vec![
6016                UpgradeInstruction::LoadModule {
6017                    module: "demo".into(),
6018                },
6019                UpgradeInstruction::StateChange {
6020                    script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
6021                },
6022            ],
6023        }];
6024        let manifest_clone = manifest.clone();
6025        let svc_clone = svc.clone();
6026        let on_state_change_clone = on_state_change.clone();
6027        let layout = StandardLayout::new().with_path_exists(move |p| {
6028            p == manifest_clone || p == svc_clone || p == on_state_change_clone
6029        });
6030        let err = layout.verify(&c, &root).unwrap_err();
6031        assert!(matches!(
6032            err,
6033            LayoutError::MissingEntry { kind, .. }
6034                if kind == crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT
6035        ));
6036    }
6037
6038    #[test]
6039    fn layout_missing_entry_kind_m2_consts_pin_canonical_kebab_case_labels() {
6040        // Byte-identity pin: the two per-M2-slot leaf-kind labels the
6041        // [`LayoutError::MissingEntry`] `kind: &'static str`
6042        // discriminator surfaces under (the M2 `:behavior` per-callback
6043        // on-disk-leaf axis, the M2 `:upgrade-from :instructions`
6044        // per-`:state-change` script-path on-disk-leaf axis) route
6045        // through the lifted [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`]
6046        // / [`crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT`]
6047        // consts, so a future rebrand that reaches the const but not
6048        // the production emit / test probe (or vice versa) surfaces
6049        // here at build time rather than at runtime as a downstream
6050        // [`LayoutError::MissingEntry`] `kind: <stale-label>`
6051        // diagnostic mismatch far from the rename's commit. Mirror of
6052        // the peer
6053        // [`crate::aplicacao::tests::contrato_author_key_consts_pin_canonical_kebab_case_labels`]
6054        // (f50c875) and
6055        // [`crate::upgrade::tests::upgrade_instruction_kind_consts_pin_canonical_kebab_case_tags`]
6056        // (56120ef) byte-identity pins on the sibling M3 `:contratos`
6057        // per-entry endpoint-label + M2 `:upgrade-from :instructions`
6058        // per-variant kind-tag axes.
6059        assert_eq!(
6060            crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
6061            "behavior-callback"
6062        );
6063        assert_eq!(
6064            crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
6065            "upgrade-script"
6066        );
6067    }
6068
6069    #[test]
6070    fn layout_missing_entry_kind_m0_consts_pin_canonical_code_slot_labels() {
6071        // Byte-identity pin: the three per-M0-code-slot leaf-kind
6072        // labels the [`LayoutError::MissingEntry`] `kind: &'static
6073        // str` discriminator surfaces under (the `:bibliotecas`
6074        // per-entry axis, the `:exe` per-entry axis, the `:servicos`
6075        // per-entry axis) route through the lifted
6076        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`] /
6077        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] /
6078        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] consts,
6079        // so a future rebrand that reaches the const but not the
6080        // production emit (or vice versa) surfaces here at build time
6081        // rather than at runtime as a downstream
6082        // [`LayoutError::MissingEntry`] `kind: <stale-label>`
6083        // diagnostic mismatch far from the rename's commit. Mirror of
6084        // the peer M2-tier pin
6085        // [`layout_missing_entry_kind_m2_consts_pin_canonical_kebab_case_labels`]
6086        // (95c9c4c) on the sibling `:behavior` / `:upgrade-from`
6087        // per-slot leaf-kind axes.
6088        assert_eq!(
6089            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
6090            "biblioteca"
6091        );
6092        assert_eq!(crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE, "exe");
6093        assert_eq!(crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO, "servico");
6094    }
6095
6096    #[test]
6097    fn layout_missing_entry_kind_m0_consts_align_with_caixa_kind_as_str() {
6098        // Cross-axis byte-identity pin: the two `:kind`-namesake M0
6099        // leaf-kind labels ([`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`]
6100        // = `"biblioteca"`,
6101        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] =
6102        // `"servico"`) must equal [`crate::CaixaKind::Biblioteca`] /
6103        // [`crate::CaixaKind::Servico`]'s
6104        // [`crate::CaixaKind::as_str`] outputs verbatim — the
6105        // substrate's canonical human-readable-kind axis and the
6106        // layout diagnostic's per-slot leaf-kind axis share one
6107        // vocabulary for these two arms by design (both label the
6108        // caixa's code-producing shape by its Portuguese-native
6109        // idiom), so drift between the two lands as a build-time
6110        // pattern-arm miss here rather than as a runtime diagnostic
6111        // that reads inconsistently across `feira build`'s
6112        // per-invocation output.
6113        //
6114        // The third M0 arm ([`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`]
6115        // = `"exe"`) is deliberately *distinct* from
6116        // [`crate::CaixaKind::Binario`]'s [`crate::CaixaKind::as_str`]
6117        // output (`"binario"`) — the `:exe` code slot names the
6118        // per-directory leaf-kind at the `exe/` subtree, whereas
6119        // [`crate::CaixaKind::Binario`] names the caixa's own runtime
6120        // kind. Two axes, two labels — the inequality assertion here
6121        // pins the split so a future accidental collapse of the two
6122        // onto one scalar (a rebrand that reroutes either axis to
6123        // match the other) trips at build time.
6124        assert_eq!(
6125            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
6126            CaixaKind::Biblioteca.as_str()
6127        );
6128        assert_eq!(
6129            crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
6130            CaixaKind::Servico.as_str()
6131        );
6132        assert_ne!(
6133            crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
6134            CaixaKind::Binario.as_str(),
6135            "`exe` leaf-kind label names the per-directory code-slot \
6136             axis; `binario` names the caixa-kind axis — the two must \
6137             not silently collapse onto one scalar"
6138        );
6139    }
6140
6141    #[test]
6142    fn layout_missing_entry_kind_consts_are_pairwise_distinct() {
6143        // Distinctness pin: the five [`LayoutError::MissingEntry`]
6144        // `kind: &'static str` accept-set members
6145        // ([`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`] /
6146        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] /
6147        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] on the
6148        // M0 code-slot arms plus
6149        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`]
6150        // / [`crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT`]
6151        // on the M2 slot arms) must be pairwise distinct — an
6152        // accidental copy-paste flip that reroutes one label's byte-
6153        // string to also match another silently collapses two
6154        // per-slot diagnostics onto one, so an operator running
6155        // `feira build` reads `kind: "biblioteca"` for what should
6156        // have surfaced as a `:behavior :on-init` script-not-found
6157        // diagnostic (or vice versa). This pin catches any such
6158        // flip at build time. Mirror of the peer
6159        // [`crate::render::tests::m2_limits_key_consts_are_pairwise_distinct`]
6160        // / peer distinctness pins on other closed-set typed axes.
6161        let entries: &[(&str, &str)] = &[
6162            (
6163                "LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA",
6164                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
6165            ),
6166            (
6167                "LAYOUT_MISSING_ENTRY_KIND_EXE",
6168                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
6169            ),
6170            (
6171                "LAYOUT_MISSING_ENTRY_KIND_SERVICO",
6172                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
6173            ),
6174            (
6175                "LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK",
6176                crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
6177            ),
6178            (
6179                "LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT",
6180                crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
6181            ),
6182        ];
6183        for (i, (name_a, value_a)) in entries.iter().enumerate() {
6184            for (name_b, value_b) in entries.iter().skip(i + 1) {
6185                assert_ne!(
6186                    value_a, value_b,
6187                    "LAYOUT_MISSING_ENTRY_KIND_* consts must be \
6188                     pairwise-distinct byte-strings — {name_a} and \
6189                     {name_b} both resolve to {value_a:?}"
6190                );
6191            }
6192        }
6193    }
6194
6195    #[test]
6196    fn layout_dir_consts_pin_canonical_directory_names() {
6197        // Scalar-value pin for the three [`crate::render::LAYOUT_DIR_*`]
6198        // consts naming the CSE-invariant per-[`CaixaKind`]
6199        // on-disk-directory-name axes the substrate's layout invariants
6200        // pin (`lib/` for [`CaixaKind::Biblioteca`], `exe/` for
6201        // [`CaixaKind::Binario`], `servicos/` for [`CaixaKind::Servico`]).
6202        // A future rebrand of any of the three on-disk directory landing
6203        // conventions must reach this pin — the const-edit lands on one
6204        // arm, the assertion here re-pins the new byte-string, and every
6205        // downstream consumer (the caixa-feira `init` / `fmt` / `lint` /
6206        // `tofu` scaffolders, the [`crate::LayoutInvariants::verify`]
6207        // sandbox reconstruction, the future
6208        // `feira app deploy`-cluster scaffolder) picks up the new
6209        // directory name at build time. Mirror of the peer
6210        // [`layout_missing_entry_kind_m0_consts_pin_canonical_code_slot_labels`]
6211        // (fe2a898) on the sibling
6212        // [`crate::LayoutError::MissingEntry`] `kind:` discriminator
6213        // axis this on-disk-directory axis composes with.
6214        assert_eq!(crate::render::LAYOUT_DIR_LIB, "lib");
6215        assert_eq!(crate::render::LAYOUT_DIR_EXE, "exe");
6216        assert_eq!(crate::render::LAYOUT_DIR_SERVICOS, "servicos");
6217    }
6218
6219    #[test]
6220    fn layout_dir_consts_are_pairwise_distinct() {
6221        // Distinctness pin: the three per-[`CaixaKind`]
6222        // on-disk-directory-name arms must resolve to pairwise-distinct
6223        // byte-strings — a future accidental copy-paste flip that
6224        // reroutes any one of the three onto another's value silently
6225        // collapses two per-kind on-disk sandboxes onto one, so
6226        // [`crate::LayoutInvariants::verify`] would gate a
6227        // [`CaixaKind::Binario`] caixa's `:exe` entries against the
6228        // wrong sub-tree (or a `:kind Servico` caixa's `:servicos`
6229        // entries against `lib/` and pass every entry `feira build`
6230        // should have rejected as [`crate::LayoutError::ServicoOutsideDir`]).
6231        // Mirror of the peer
6232        // [`layout_missing_entry_kind_consts_are_pairwise_distinct`]
6233        // (fe2a898) on the sibling leaf-kind label accept-set.
6234        let entries: &[(&str, &str)] = &[
6235            ("LAYOUT_DIR_LIB", crate::render::LAYOUT_DIR_LIB),
6236            ("LAYOUT_DIR_EXE", crate::render::LAYOUT_DIR_EXE),
6237            ("LAYOUT_DIR_SERVICOS", crate::render::LAYOUT_DIR_SERVICOS),
6238        ];
6239        for (i, (name_a, value_a)) in entries.iter().enumerate() {
6240            for (name_b, value_b) in entries.iter().skip(i + 1) {
6241                assert_ne!(
6242                    value_a, value_b,
6243                    "LAYOUT_DIR_* consts must be pairwise-distinct \
6244                     byte-strings — {name_a} and {name_b} both resolve \
6245                     to {value_a:?}"
6246                );
6247            }
6248        }
6249    }
6250
6251    #[test]
6252    fn layout_dir_exe_matches_layout_missing_entry_kind_exe() {
6253        // Cross-axis byte-identity pin: [`crate::render::LAYOUT_DIR_EXE`]
6254        // (the on-disk-directory-name arm) equals
6255        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] (the
6256        // [`LayoutError::MissingEntry`] `kind:` leaf-kind categorization
6257        // arm) verbatim — the M0 `:kind Binario` on-disk-directory axis
6258        // and the [`crate::LayoutError::MissingEntry`] `kind:` leaf-kind
6259        // discriminator name the same three-byte sub-tree (`exe/`), a
6260        // coincidence [`crate::LayoutInvariants::verify`] itself relies
6261        // on: it joins `root` with [`crate::render::LAYOUT_DIR_EXE`] to
6262        // reconstruct `exe_dir` and emits [`crate::LayoutError::MissingEntry
6263        // { kind: LAYOUT_MISSING_ENTRY_KIND_EXE, path: <under exe_dir> }`]
6264        // for every non-resolving entry. Making the coincidence
6265        // load-bearing means a future rebrand touching either axis
6266        // without the other (a per-consumer disambiguation collapsing
6267        // the leaf-kind label onto `"binary"` while the directory stays
6268        // `"exe"`, or vice versa) trips at caixa-core build time rather
6269        // than surfacing at runtime as a mismatched
6270        // [`crate::LayoutInvariants::verify`] diagnostic whose `kind:`
6271        // reads one label while the `path:` sits under a differently-named
6272        // sub-tree.
6273        assert_eq!(
6274            crate::render::LAYOUT_DIR_EXE,
6275            crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
6276            "LAYOUT_DIR_EXE must equal LAYOUT_MISSING_ENTRY_KIND_EXE — \
6277             both name the M0 `:kind Binario` sub-tree by the same \
6278             three-byte scalar"
6279        );
6280    }
6281
6282    #[test]
6283    fn layout_dir_bib_is_distinct_from_layout_missing_entry_kind_bib() {
6284        // Cross-axis distinctness pin: [`crate::render::LAYOUT_DIR_LIB`]
6285        // (`"lib"`, the Cargo-style abbreviated on-disk directory name)
6286        // is *deliberately* distinct from
6287        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`]
6288        // (`"biblioteca"`, the full-form Portuguese-native leaf-kind
6289        // label) — the substrate splits the on-disk convention terse
6290        // (`lib/`) from the diagnostic vocabulary full (`biblioteca`),
6291        // matching Cargo's `src/lib.rs` abbreviation of the `library`
6292        // crate-type discriminator. A future accidental collapse of the
6293        // two axes onto one scalar (a rebrand aligning either arm with
6294        // the other for schema-clarity, an English-uniformity pass that
6295        // renames `LAYOUT_DIR_LIB` to `LAYOUT_DIR_BIBLIOTECA` or the
6296        // diagnostic label to `"lib"`) would silently reroute either
6297        // consumer onto the other's byte-string. This pin catches the
6298        // collapse at build time. Peer of the sibling
6299        // [`layout_missing_entry_kind_m0_consts_align_with_caixa_kind_as_str`]
6300        // (fe2a898) that pins the analogous *equality* between the M0
6301        // `:kind Biblioteca` diagnostic-label arm and
6302        // [`crate::CaixaKind::Biblioteca`]'s [`crate::CaixaKind::as_str`]
6303        // output — the two pins jointly encode the "which of the three
6304        // Biblioteca-related scalars are load-bearing-equal, which are
6305        // load-bearing-distinct" invariant across the substrate's
6306        // per-kind vocabulary.
6307        assert_ne!(
6308            crate::render::LAYOUT_DIR_LIB,
6309            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
6310            "LAYOUT_DIR_LIB (`\"lib\"`) and LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA \
6311             (`\"biblioteca\"`) name two distinct axes — the on-disk \
6312             directory convention (Cargo-style abbreviated) and the \
6313             layout-diagnostic leaf-kind label (full-form Portuguese) — \
6314             and must not silently collapse onto one scalar"
6315        );
6316    }
6317
6318    #[test]
6319    fn layout_dir_servicos_is_distinct_from_layout_missing_entry_kind_servico() {
6320        // Cross-axis distinctness pin: [`crate::render::LAYOUT_DIR_SERVICOS`]
6321        // (`"servicos"`, the Portuguese-*plural* on-disk directory
6322        // name) is *deliberately* distinct from
6323        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`]
6324        // (`"servico"`, the singular leaf-kind label) — the on-disk
6325        // sub-tree houses one-or-more ComputeUnit YAML descriptors per
6326        // caixa (hence the plural), the diagnostic label names the
6327        // caixa's own kind (singular). A future accidental collapse
6328        // onto one scalar (a per-consumer disambiguation aligning the
6329        // two, a hypothetical English-uniformity pass renaming
6330        // `"servicos"` → `"services"` while retaining `"servico"` on
6331        // the diagnostic arm — or vice versa) would silently reroute
6332        // either consumer onto the other's byte-string. Peer of the
6333        // sibling [`layout_dir_bib_is_distinct_from_layout_missing_entry_kind_bib`]
6334        // pin on the M0 `:kind Biblioteca` split axis; two of the three
6335        // per-kind on-disk / leaf-kind splits carry a distinctness
6336        // pin here, the third ([`crate::render::LAYOUT_DIR_EXE`] vs
6337        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`]) carries an
6338        // equality pin under
6339        // [`layout_dir_exe_matches_layout_missing_entry_kind_exe`].
6340        assert_ne!(
6341            crate::render::LAYOUT_DIR_SERVICOS,
6342            crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
6343            "LAYOUT_DIR_SERVICOS (`\"servicos\"`, plural on-disk sub-tree) \
6344             and LAYOUT_MISSING_ENTRY_KIND_SERVICO (`\"servico\"`, singular \
6345             leaf-kind label) name two distinct axes and must not silently \
6346             collapse onto one scalar"
6347        );
6348    }
6349
6350    #[test]
6351    fn layout_invariants_reconstruct_sandbox_roots_through_lifted_layout_dir_consts() {
6352        // Production-through-const pin: [`LayoutInvariants::verify`]
6353        // routes its three per-kind sandbox-root joins
6354        // (`root.join(LAYOUT_DIR_LIB)` for the `:kind Biblioteca`
6355        // default `lib/<nome>.lisp` reconstruction, `root.join(LAYOUT_DIR_EXE)`
6356        // for the [`LayoutError::ExeOutsideDir`] gate,
6357        // `root.join(LAYOUT_DIR_SERVICOS)` for the
6358        // [`LayoutError::ServicoOutsideDir`] gate) through the three
6359        // lifted consts, not through inline `"lib"` / `"exe"` /
6360        // `"servicos"` `&str` literals. This test drives the
6361        // [`LayoutError::ExeOutsideDir`] arm through a `:kind Binario`
6362        // caixa whose declared `:exe` entry deliberately escapes
6363        // `root.join(LAYOUT_DIR_EXE)` (a sibling `bin/tool` path) —
6364        // if the production emit reads the wrong const (or reverts to
6365        // an inline literal that drifts from the const) the diagnostic
6366        // arm surfaces the wrong variant, catching the drift at build
6367        // time rather than as a per-invocation runtime mismatch.
6368        //
6369        // Mirror of the peer production-through-const pin
6370        // [`crate::dep::tests::validate_no_self_dep_deps_field_routes_through_dep_author_key`]
6371        // (4da6fba) on the sibling M0 `:deps` `list:` diagnostic axis.
6372        use std::path::PathBuf;
6373        let root = PathBuf::from("/tmp/x");
6374        let manifest = root.join("caixa.lisp");
6375        let bin_entry_outside = root.join("bin/tool");
6376        let mut c = caixa(CaixaKind::Binario);
6377        c.exe = vec!["bin/tool".into()];
6378        let manifest_clone = manifest.clone();
6379        let outside_clone = bin_entry_outside.clone();
6380        let layout = StandardLayout::new()
6381            .with_path_exists(move |p| p == manifest_clone || p == outside_clone);
6382        let err = layout.verify(&c, &root).unwrap_err();
6383        match err {
6384            LayoutError::ExeOutsideDir(path) => {
6385                assert_eq!(
6386                    path, bin_entry_outside,
6387                    "ExeOutsideDir must carry the resolved `:exe` entry that \
6388                     escapes `root.join(LAYOUT_DIR_EXE)`"
6389                );
6390                // Byte-identity check: the escape must be against the
6391                // lifted `LAYOUT_DIR_EXE` sub-tree, not a stale inline
6392                // literal — a future const-edit that drifts from `"exe"`
6393                // reroutes `exe_dir` off the sandbox `bin/tool` escapes
6394                // from, and this pattern-arm miss re-surfaces here.
6395                assert!(
6396                    !path.starts_with(root.join(crate::render::LAYOUT_DIR_EXE)),
6397                    "resolved `:exe` entry {path:?} must escape the \
6398                     `root.join(LAYOUT_DIR_EXE)` sub-tree the production \
6399                     emit uses to gate the [`LayoutError::ExeOutsideDir`] arm"
6400                );
6401            }
6402            other => panic!("expected ExeOutsideDir, got {other:?}"),
6403        }
6404    }
6405
6406    #[test]
6407    fn upgrade_state_change_without_behavior_callback_surfaces_as_upgrade_violation() {
6408        // Wiring pin for the cross-slot composition gate
6409        // (`validate_upgrade_from_against_behavior`): a caixa whose
6410        // `:upgrade-from` declares a `(:state-change "lib/m.lisp")`
6411        // instruction but does not declare `:behavior :on-state-change`
6412        // surfaces at `feira build` time as a `LayoutError::UpgradeViolation`
6413        // naming the offending caixa + the entry's `:from` + the
6414        // offending script — not at hot-upgrade dispatch when the
6415        // operator reaches for the missing callback. Mirrors
6416        // `upgrade_from_downgrade_surfaces_as_upgrade_violation` on the
6417        // peer `:from` ↔ `:versao` cross-slot precedence gate.
6418        use crate::{UpgradeFromEntry, UpgradeInstruction};
6419        use std::path::PathBuf;
6420        let root = PathBuf::from("/tmp/x");
6421        let manifest = root.join("caixa.lisp");
6422        let svc = root.join("servicos/demo.computeunit.yaml");
6423        let mut c = caixa(CaixaKind::Servico);
6424        c.versao = "0.2.0".into();
6425        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6426        // `:behavior` is None (the canonical "I added the upgrade path
6427        // but never declared :behavior" footgun the gate closes); a
6428        // peer arm covers the BehaviorSpec-Some-but-on-state-change-
6429        // None shape in `upgrade::tests::behavior_gate_rejects_state_
6430        // change_when_on_state_change_is_none`.
6431        c.upgrade_from = vec![UpgradeFromEntry {
6432            from: "0.1.0".into(),
6433            instructions: vec![
6434                UpgradeInstruction::LoadModule {
6435                    module: "demo".into(),
6436                },
6437                UpgradeInstruction::StateChange {
6438                    script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
6439                },
6440            ],
6441        }];
6442        let manifest_clone = manifest.clone();
6443        let svc_clone = svc.clone();
6444        let layout =
6445            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
6446        let err = layout.verify(&c, &root).unwrap_err();
6447        match err {
6448            LayoutError::UpgradeViolation { caixa, issue } => {
6449                assert_eq!(caixa, "demo", "diagnostic must name the offending caixa");
6450                assert!(
6451                    issue.contains(crate::render::M2_BEHAVIOR_AUTHOR_KEY_ON_STATE_CHANGE),
6452                    "diagnostic must name the missing callback slot for self-locating fix, \
6453                     got {issue:?}"
6454                );
6455                assert!(
6456                    issue.contains("0.1.0"),
6457                    "diagnostic must name the offending entry's :from, got {issue:?}"
6458                );
6459                assert!(
6460                    issue.contains("v01-to-v02.lisp"),
6461                    "diagnostic must name the offending :script for self-locating fix, \
6462                     got {issue:?}"
6463                );
6464            }
6465            other => panic!("expected UpgradeViolation, got {other:?}"),
6466        }
6467    }
6468
6469    #[test]
6470    fn supervisor_must_have_children() {
6471        use crate::RestartStrategy;
6472        let root = PathBuf::from("/tmp/x");
6473        let manifest = root.join("caixa.lisp");
6474        let manifest_clone = manifest.clone();
6475        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6476        let mut c = caixa(CaixaKind::Supervisor);
6477        c.estrategia = Some(RestartStrategy::OneForOne);
6478        c.max_restarts = Some(5);
6479        // No children → should fail
6480        let err = layout.verify(&c, &root).unwrap_err();
6481        assert!(matches!(err, LayoutError::SupervisorViolation { .. }));
6482    }
6483
6484    #[test]
6485    fn supervisor_self_referential_child_is_violation() {
6486        // A Supervisor whose `:children` names its own `:nome` is a
6487        // one-node supervision cycle. The cross-slot gate fires at
6488        // verify time, surfacing as a SupervisorViolation that names the
6489        // offending supervisor — not at the cluster apply far from
6490        // source. The `caixa()` helper's `:nome` is "demo".
6491        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6492        let root = PathBuf::from("/tmp/x");
6493        let manifest = root.join("caixa.lisp");
6494        let manifest_clone = manifest.clone();
6495        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6496        let mut c = caixa(CaixaKind::Supervisor);
6497        c.estrategia = Some(RestartStrategy::OneForOne);
6498        c.max_restarts = Some(5);
6499        c.children = vec![
6500            ChildSpec {
6501                caixa: "worker".into(),
6502                versao: "^0.1".into(),
6503                restart: RestartPolicy::Permanent,
6504            },
6505            ChildSpec {
6506                caixa: "demo".into(),
6507                versao: "^0.1".into(),
6508                restart: RestartPolicy::Permanent,
6509            },
6510        ];
6511        let err = layout.verify(&c, &root).unwrap_err();
6512        let LayoutError::SupervisorViolation { caixa, issue } = err else {
6513            panic!("expected SupervisorViolation for self-referential child, got {err:?}");
6514        };
6515        assert_eq!(caixa, "demo");
6516        assert!(
6517            issue.contains("demo") && issue.contains("itself"),
6518            "issue must name the self-supervising caixa, got {issue:?}"
6519        );
6520    }
6521
6522    #[test]
6523    fn supervisor_distinct_children_pass_self_supervision_gate() {
6524        // Positive control: a Supervisor whose children are all distinct
6525        // from its own `:nome` verifies cleanly.
6526        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6527        let root = PathBuf::from("/tmp/x");
6528        let manifest = root.join("caixa.lisp");
6529        let manifest_clone = manifest.clone();
6530        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6531        let mut c = caixa(CaixaKind::Supervisor);
6532        c.estrategia = Some(RestartStrategy::OneForOne);
6533        c.max_restarts = Some(5);
6534        c.children = vec![ChildSpec {
6535            caixa: "worker".into(),
6536            versao: "^0.1".into(),
6537            restart: RestartPolicy::Permanent,
6538        }];
6539        layout.verify(&c, &root).unwrap();
6540    }
6541
6542    #[test]
6543    fn cross_slot_self_edge_gates_route_parent_nome_through_lifted_accessor() {
6544        // Composition pin: every cross-slot self-edge gate fired from
6545        // `LayoutInvariants::verify` — the supervision-tree arm's
6546        // `crate::supervisor::validate_no_self_supervision` call, the
6547        // Aplicacao arm's `crate::aplicacao::validate_no_self_membership`
6548        // call, and the dep-graph arm's
6549        // `crate::dep::validate_no_self_dep` call — must key its
6550        // `parent_nome` arg off the typed [`Caixa::nome`] accessor, not
6551        // the raw `&caixa.nome` `&String`-borrow of the underlying
6552        // field.
6553        //
6554        // Structurally: a rename of the storage field or a hypothetical
6555        // accessor rebrand (a per-cluster alias table pinned through a
6556        // future `:placement`-scoped slot, the M4 CR materializer's
6557        // per-CR namespace-qualified rewrite, a `:nome-suffix` overlay
6558        // the MESH-COMPOSITION §III.2 roadmap acknowledges) would land
6559        // through the accessor by construction; a raw-borrow bypass
6560        // would silently disagree with every peer consumer that already
6561        // routes through `caixa.nome()` (the caixa-mesh 980c059,
6562        // caixa-helm 22461ef, caixa-flux 162e2e2, caixa-crd 61d3429,
6563        // caixa-feira ef83332 raw-borrow converges), reintroducing the
6564        // drift surface the sibling converges closed. Each arm fires
6565        // its per-kind `LayoutError` variant (`SupervisorViolation` /
6566        // `AplicacaoViolation` / `DepsViolation`) whose `caixa` field
6567        // carries the offending parent name verbatim through
6568        // `caixa.nome().clone()`; asserting the field equals
6569        // `caixa.nome()` on the mutated fixture pins the accessor-
6570        // routed parent-nome projection at every call site — a future
6571        // silent detour that had the gate observe a stale / aliased
6572        // name at the arg boundary would surface here as a
6573        // `caixa != "demo"` inequality.
6574        //
6575        // Peer of the sibling per-caixa-crate `nome`-arg raw-borrow
6576        // convergence pin discipline (54bf2f3 / 22461ef / 162e2e2 on the
6577        // renderer crates; ef83332 on the CLI) — extends the "one typed
6578        // dispatch per `:nome` consumer" discipline onto the substrate's
6579        // own [`LayoutInvariants::verify`] cross-slot self-edge gate
6580        // wire-up on all three typed-name-graph kinds.
6581        use crate::{
6582            ChildSpec, Dep, Membro, Placement, PlacementStrategy, RestartPolicy, RestartStrategy,
6583        };
6584        let root = PathBuf::from("/tmp/x");
6585        let manifest = root.join("caixa.lisp");
6586        let manifest_clone = manifest.clone();
6587        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6588
6589        // Supervisor arm — the `caixa()` helper's `:nome` is "demo",
6590        // and the accessor's return `caixa.nome()` must equal the
6591        // parent-nome that the self-supervision gate observes.
6592        let mut sup = caixa(CaixaKind::Supervisor);
6593        sup.estrategia = Some(RestartStrategy::OneForOne);
6594        sup.max_restarts = Some(5);
6595        sup.children = vec![ChildSpec {
6596            caixa: "demo".into(),
6597            versao: "^0.1".into(),
6598            restart: RestartPolicy::Permanent,
6599        }];
6600        let parent_nome_via_accessor = sup.nome();
6601        assert_eq!(
6602            parent_nome_via_accessor, "demo",
6603            "the caixa() fixture helper's `:nome` must be \"demo\" — \
6604             the accessor's return is the pin's ground truth for the \
6605             cross-slot gate's parent-nome arg",
6606        );
6607        let err = layout.verify(&sup, &root).unwrap_err();
6608        let LayoutError::SupervisorViolation { caixa: c_nome, .. } = err else {
6609            panic!("expected SupervisorViolation for self-referential child, got {err:?}");
6610        };
6611        assert_eq!(
6612            c_nome, parent_nome_via_accessor,
6613            "the SupervisorViolation's `caixa` field must equal \
6614             `sup.nome()` — the cross-slot self-supervision gate's \
6615             `parent_nome` arg must route through the lifted \
6616             [`Caixa::nome`] accessor, not the raw `&caixa.nome` \
6617             `&String`-borrow of the underlying field",
6618        );
6619
6620        // Aplicacao arm — same discipline on the peer typed-name-graph
6621        // kind. Constructed alongside the supervisor arm so any future
6622        // accessor drift lands on both arms in the same pin.
6623        let mut app = caixa(CaixaKind::Aplicacao);
6624        app.placement = Some(Placement {
6625            estrategia: PlacementStrategy::Replicated,
6626            clusters: vec!["rio".into()],
6627            affinity: None,
6628            shard_key: None,
6629        });
6630        app.membros = vec![Membro {
6631            caixa: "demo".into(),
6632            versao: "^0.1".into(),
6633        }];
6634        let parent_nome_via_accessor = app.nome();
6635        assert_eq!(
6636            parent_nome_via_accessor, "demo",
6637            "the caixa() fixture helper's `:nome` must be \"demo\" on \
6638             the Aplicacao arm too — same accessor-ground-truth as the \
6639             sibling supervisor arm above",
6640        );
6641        let err = layout.verify(&app, &root).unwrap_err();
6642        let LayoutError::AplicacaoViolation { caixa: c_nome, .. } = err else {
6643            panic!("expected AplicacaoViolation for self-referential membro, got {err:?}");
6644        };
6645        assert_eq!(
6646            c_nome, parent_nome_via_accessor,
6647            "the AplicacaoViolation's `caixa` field must equal \
6648             `app.nome()` — the cross-slot self-membership gate's \
6649             `parent_nome` arg must route through the lifted \
6650             [`Caixa::nome`] accessor, not the raw `&caixa.nome` \
6651             `&String`-borrow of the underlying field",
6652        );
6653
6654        // Dep-graph arm — third typed-name-graph kind on the
6655        // `parent_nome` arg boundary. Same discipline as the peer
6656        // supervision-tree and Aplicacao-membership arms above.
6657        // Constructed alongside so any future accessor drift lands on
6658        // all three arms in the same pin. Needs a distinct layout
6659        // fixture from the supervisor / aplicacao arms above because
6660        // the `Biblioteca` kind's code-path existence gate demands the
6661        // canonical `lib/<nome>.lisp` path also `exists`, so the shim
6662        // covers both `caixa.lisp` and `lib/demo.lisp`.
6663        let default_lib = root.join("lib").join("demo.lisp");
6664        let manifest_dep = manifest.clone();
6665        let default_lib_clone = default_lib.clone();
6666        let layout_dep = StandardLayout::new()
6667            .with_path_exists(move |p| p == manifest_dep || p == default_lib_clone);
6668        let mut lib = caixa(CaixaKind::Biblioteca);
6669        lib.deps = vec![Dep::simple("demo", "^0.1")];
6670        let parent_nome_via_accessor = lib.nome();
6671        assert_eq!(
6672            parent_nome_via_accessor, "demo",
6673            "the caixa() fixture helper's `:nome` must be \"demo\" on \
6674             the Biblioteca arm too — same accessor-ground-truth as the \
6675             sibling supervisor + Aplicacao arms above",
6676        );
6677        let err = layout_dep.verify(&lib, &root).unwrap_err();
6678        let LayoutError::DepsViolation { caixa: c_nome, .. } = err else {
6679            panic!("expected DepsViolation for self-referential :deps entry, got {err:?}");
6680        };
6681        assert_eq!(
6682            c_nome, parent_nome_via_accessor,
6683            "the DepsViolation's `caixa` field must equal \
6684             `lib.nome()` — the cross-slot self-dep gate's `parent_nome` \
6685             arg must route through the lifted [`Caixa::nome`] accessor, \
6686             not the raw `&caixa.nome` `&String`-borrow of the underlying \
6687             field",
6688        );
6689    }
6690
6691    #[test]
6692    fn upgrade_against_versao_gate_routes_current_versao_through_lifted_accessor() {
6693        // Composition pin: the cross-slot `:upgrade-from :from` ↔
6694        // `:versao` precedence gate fired from
6695        // `LayoutInvariants::verify` — the
6696        // `crate::upgrade::validate_upgrade_from_against_versao` call —
6697        // must key its `versao` arg off the typed [`Caixa::versao`]
6698        // accessor, not the raw `&caixa.versao` `&String`-borrow of
6699        // the underlying field.
6700        //
6701        // Same "arg-boundary reads through the lifted accessor"
6702        // discipline as the sibling
6703        // [`cross_slot_self_edge_gates_route_parent_nome_through_lifted_accessor`]
6704        // pin above on the `:nome`-arg axis of the three typed-name-
6705        // graph self-edge gates — extended here onto the `:versao`-arg
6706        // axis of the substrate's remaining `LayoutInvariants::verify`
6707        // cross-slot arg-carrying call site. Structurally byte-equal
6708        // today (the accessor is `pub fn versao(&self) -> &str { &self.versao }`,
6709        // so both paths coerce to the same `&str`); the pin catches a
6710        // future silent detour (an accessor rebrand that no longer
6711        // shipped the raw slot verbatim — a per-`:edicao` overlay,
6712        // a promotion of `:versao` to a `CaixaVersion` newtype with a
6713        // canonicalizing accessor, an M4 CR-materializer-side pinning
6714        // through a resolver-annotated `:versao-resolved` slot) that
6715        // would silently split the substrate's own precedence gate
6716        // from every peer consumer already routing `:versao` reads
6717        // through the lifted accessor.
6718        //
6719        // The gate fires an `UpgradeViolation { caixa, issue }` when a
6720        // `:upgrade-from` entry's `:from` is not strictly less than
6721        // the top-level `:versao` — the `issue` string names both the
6722        // offending prior version and the current version verbatim,
6723        // so asserting the substring `caixa.versao()` appears in the
6724        // fired diagnostic pins the accessor-routed current-versao
6725        // projection at the arg boundary. A raw-borrow bypass would
6726        // still surface the same bytes today, but the presence of
6727        // this pin makes any future divergence between the accessor's
6728        // return and the raw slot's contents a build-time failure at
6729        // this call site.
6730        use crate::{UpgradeFromEntry, UpgradeInstruction};
6731        let root = PathBuf::from("/tmp/x");
6732        let manifest = root.join("caixa.lisp");
6733        let servico_path = root.join("servicos").join("demo.computeunit.yaml");
6734        let manifest_clone = manifest.clone();
6735        let servico_clone = servico_path.clone();
6736        let layout = StandardLayout::new()
6737            .with_path_exists(move |p| p == manifest_clone || p == servico_clone);
6738        let mut svc = caixa(CaixaKind::Servico);
6739        svc.versao = "0.1.0".into();
6740        svc.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6741        // `:from` >= current `:versao` — trips the precedence gate the
6742        // `validate_upgrade_from_against_versao` cross-slot call
6743        // enforces. `:load-module` carries no on-disk path so the
6744        // path-existence gate downstream stays inert; the precedence
6745        // gate is what fires. No `:on-state-change` needed because the
6746        // instruction list carries no `:state-change` entry, so the
6747        // sibling `validate_upgrade_from_against_behavior` gate is
6748        // inert too.
6749        svc.upgrade_from = vec![UpgradeFromEntry {
6750            from: "0.2.0".into(),
6751            instructions: vec![UpgradeInstruction::LoadModule {
6752                module: "demo".into(),
6753            }],
6754        }];
6755        let current_versao_via_accessor = svc.versao().to_string();
6756        assert_eq!(
6757            current_versao_via_accessor, "0.1.0",
6758            "the mutated fixture's `:versao` must be observable through \
6759             the accessor before layout verification fires — a drift on \
6760             `Caixa::versao` would surface here as a `!= \"0.1.0\"` \
6761             inequality",
6762        );
6763        let err = layout.verify(&svc, &root).unwrap_err();
6764        let LayoutError::UpgradeViolation {
6765            caixa: c_nome,
6766            issue,
6767        } = err
6768        else {
6769            panic!("expected UpgradeViolation for :from >= :versao, got {err:?}");
6770        };
6771        assert_eq!(c_nome, svc.nome(), "wrap envelope names the caixa");
6772        assert!(
6773            issue.contains(&current_versao_via_accessor),
6774            "the UpgradeViolation's `issue` must quote the current \
6775             `:versao` byte-string verbatim — the cross-slot precedence \
6776             gate's `versao` arg must route through the lifted \
6777             [`Caixa::versao`] accessor, not the raw `&caixa.versao` \
6778             `&String`-borrow of the underlying field. issue: {issue}",
6779        );
6780    }
6781
6782    #[test]
6783    fn layout_violation_envelopes_carry_caixa_nome_through_lifted_accessor() {
6784        // Wrap-envelope drift-detection pin: every per-axis
6785        // `LayoutError::*Violation { caixa, issue }` envelope fired
6786        // from `LayoutInvariants::verify` must key its offending-caixa
6787        // field off the typed [`Caixa::nome`] accessor's
6788        // `.to_string()` extension, not the raw
6789        // `caixa.nome.clone()` `String::clone()` of the underlying
6790        // field. Structurally byte-equal today (each accessor is
6791        // `pub fn nome(&self) -> &str { &self.nome }`, so
6792        // `caixa.nome().to_string()` and `caixa.nome.clone()` produce
6793        // the same bytes); the pin catches a future silent detour
6794        // (an accessor rebrand that no longer shipped the raw slot
6795        // verbatim — a per-cluster alias table pinned through a
6796        // future `:placement`-scoped slot, the M4 CR materializer's
6797        // per-CR namespace-qualified rewrite, a `:nome-suffix`
6798        // overlay the MESH-COMPOSITION §III.2 roadmap acknowledges)
6799        // that would silently split the substrate's own layout
6800        // invariant verifier's diagnostic surface from every peer
6801        // caixa-crate consumer that already routes `:nome` reads
6802        // through the lifted accessor (the caixa-mesh 980c059,
6803        // caixa-helm 22461ef, caixa-flux 162e2e2, caixa-crd 61d3429,
6804        // caixa-feira ef83332 raw-borrow converges).
6805        //
6806        // Exercises a representative variant on each of the three
6807        // wrap-envelope arm shapes the substrate's per-axis fan-out
6808        // carries: (1) `LayoutError::NomeViolation` (the leading arm
6809        // in the `verify` order — the `:nome` axis's DNS-1123 shape
6810        // gate fires immediately after the manifest-existence gate),
6811        // (2) `LayoutError::BinarioWithoutExe` (a tuple-variant on
6812        // the kind-coherence family — different envelope shape than
6813        // the struct-variant `*Violation { caixa, issue }` family
6814        // but the same converge target on the `caixa.nome().to_string()`
6815        // arg), and (3) `LayoutError::ServicoWithoutServicos` (the
6816        // sibling tuple-variant on the same kind-coherence family).
6817        // Together they cover the two `LayoutError` envelope shapes
6818        // (struct-variant + tuple-variant) the layout invariants file
6819        // emits on `:nome`-carrying arms.
6820        //
6821        // Peer of the sibling
6822        // [`cross_slot_self_edge_gates_route_parent_nome_through_lifted_accessor`]
6823        // pin above — extends the "wrap-envelope `caixa:` field
6824        // reads through the lifted accessor" discipline from the
6825        // cross-slot self-edge gates' `parent_nome` arg boundary
6826        // onto the per-axis `LayoutError::*Violation` envelope's
6827        // `caixa:` field boundary.
6828
6829        let root = PathBuf::from("/tmp/x");
6830        let manifest = root.join("caixa.lisp");
6831        let manifest_clone = manifest.clone();
6832        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6833
6834        // (1) `NomeViolation` on the struct-variant envelope: force a
6835        // DNS-1123-invalid `:nome` (uppercase byte — `is_dns_1123_label`
6836        // rejects) and assert the fired envelope's `caixa:` field
6837        // byte-equals `c.nome().to_string()`.
6838        let mut c = caixa(CaixaKind::Biblioteca);
6839        c.nome = "BAD_NAME".into();
6840        c.bibliotecas = vec!["lib/demo.lisp".into()];
6841        let expected_nome_via_accessor = c.nome().to_string();
6842        assert_eq!(
6843            expected_nome_via_accessor, "BAD_NAME",
6844            "the mutated fixture's `:nome` must be observable through \
6845             the accessor before layout verification fires — a drift \
6846             on `Caixa::nome` would surface here as a `!= \"BAD_NAME\"` \
6847             inequality",
6848        );
6849        let err = layout.verify(&c, &root).unwrap_err();
6850        let LayoutError::NomeViolation { caixa: c_nome, .. } = err else {
6851            panic!("expected NomeViolation for DNS-1123-invalid :nome, got {err:?}");
6852        };
6853        assert_eq!(
6854            c_nome, expected_nome_via_accessor,
6855            "the NomeViolation's `caixa` field must equal \
6856             `c.nome().to_string()` — the wrap envelope's per-axis \
6857             projection must route through the lifted [`Caixa::nome`] \
6858             accessor's `.to_string()` extension, not the raw \
6859             `caixa.nome.clone()` `String::clone()` of the underlying \
6860             field",
6861        );
6862
6863        // (2) `BinarioWithoutExe` on the tuple-variant envelope: a
6864        // Binario-kind caixa with an empty `:exe` list fires the
6865        // kind-coherence gate whose payload is a bare `String`, so the
6866        // pattern is `LayoutError::BinarioWithoutExe(String)` rather
6867        // than the struct-variant `{ caixa, issue }` family. The
6868        // converge target is the same — `caixa.nome().to_string()` — but
6869        // the envelope shape is different, so the pin exercises both.
6870        let mut c = caixa(CaixaKind::Binario);
6871        // `:exe` empty is the trigger — the fixture helper defaults
6872        // it to `vec![]`, so no mutation is needed.
6873        c.nome = "binario-demo".into();
6874        let expected_nome_via_accessor = c.nome().to_string();
6875        assert_eq!(
6876            expected_nome_via_accessor, "binario-demo",
6877            "the mutated fixture's `:nome` must be observable through \
6878             the accessor before layout verification fires",
6879        );
6880        let err = layout.verify(&c, &root).unwrap_err();
6881        let LayoutError::BinarioWithoutExe(c_nome) = err else {
6882            panic!("expected BinarioWithoutExe for empty :exe list on Binario kind, got {err:?}");
6883        };
6884        assert_eq!(
6885            c_nome, expected_nome_via_accessor,
6886            "the BinarioWithoutExe's payload must equal \
6887             `c.nome().to_string()` — the tuple-variant envelope's \
6888             per-axis projection must route through the lifted \
6889             [`Caixa::nome`] accessor's `.to_string()` extension, not \
6890             the raw `caixa.nome.clone()` `String::clone()` of the \
6891             underlying field",
6892        );
6893
6894        // (3) `ServicoWithoutServicos` on the sibling tuple-variant
6895        // envelope: same discipline on the peer kind-coherence
6896        // partition arm. Constructed alongside the Binario arm so any
6897        // future accessor drift lands on both arms in the same pin.
6898        let mut c = caixa(CaixaKind::Servico);
6899        // `:servicos` empty is the trigger — the fixture helper
6900        // defaults it to `vec![]`, so no mutation is needed.
6901        c.nome = "servico-demo".into();
6902        let expected_nome_via_accessor = c.nome().to_string();
6903        assert_eq!(
6904            expected_nome_via_accessor, "servico-demo",
6905            "the mutated fixture's `:nome` must be observable through \
6906             the accessor before layout verification fires",
6907        );
6908        let err = layout.verify(&c, &root).unwrap_err();
6909        let LayoutError::ServicoWithoutServicos(c_nome) = err else {
6910            panic!(
6911                "expected ServicoWithoutServicos for empty :servicos list on Servico kind, \
6912                 got {err:?}"
6913            );
6914        };
6915        assert_eq!(
6916            c_nome, expected_nome_via_accessor,
6917            "the ServicoWithoutServicos's payload must equal \
6918             `c.nome().to_string()` — same converge discipline as the \
6919             sibling `BinarioWithoutExe` tuple-variant arm above",
6920        );
6921    }
6922
6923    #[test]
6924    fn supervisor_must_not_have_bibliotecas() {
6925        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6926        let root = PathBuf::from("/tmp/x");
6927        let manifest = root.join("caixa.lisp");
6928        let manifest_clone = manifest.clone();
6929        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6930        let mut c = caixa(CaixaKind::Supervisor);
6931        c.estrategia = Some(RestartStrategy::OneForOne);
6932        c.max_restarts = Some(5);
6933        c.bibliotecas = vec!["lib/code.lisp".into()];
6934        c.children = vec![ChildSpec {
6935            caixa: "worker".into(),
6936            versao: "^0.1".into(),
6937            restart: RestartPolicy::Permanent,
6938        }];
6939        let err = layout.verify(&c, &root).unwrap_err();
6940        assert!(matches!(err, LayoutError::SupervisorOwnsCode(_)));
6941    }
6942
6943    // ── Caixa::validate_restart_window wired into Supervisor verify ─────
6944    //
6945    // Until this wire-up landed `Caixa::validate_restart_window` lived as
6946    // `pub fn` on `Caixa` with full per-arm unit coverage in
6947    // `manifest::tests` (`validate_restart_window_rejects_*` — fractional,
6948    // decimal-shaped integer, half-unit minute, leading sign, unknown
6949    // unit, garbage, empty-after-trim) but no production path called it;
6950    // `feira build` silently accepted malformed `:restart-window` and
6951    // `Caixa::supervisor_view` soft-swallowed the parse failure as
6952    // `restart_window: None` (the canonical "no reset" sentinel), turning
6953    // every authoring footgun into a never-reset supervisor far from the
6954    // source caixa.lisp. The following pins fence the layout-pipeline
6955    // wire-up: every layout verify on a structurally-invalid `:restart-
6956    // window` axis surfaces the per-axis `RestartWindowViolation { caixa,
6957    // issue }` envelope before the typed `SupervisorSpec::validate` gate
6958    // sees the laundered `None`.
6959
6960    fn supervisor_with_window(window: Option<&str>) -> Caixa {
6961        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6962        let mut c = caixa(CaixaKind::Supervisor);
6963        c.estrategia = Some(RestartStrategy::OneForOne);
6964        c.max_restarts = Some(5);
6965        c.restart_window = window.map(str::to_string);
6966        c.children = vec![ChildSpec {
6967            caixa: "worker".into(),
6968            versao: "^0.1".into(),
6969            restart: RestartPolicy::Permanent,
6970        }];
6971        c
6972    }
6973
6974    #[test]
6975    fn restart_window_violation_on_fractional_seconds() {
6976        // `"1.5s"` is the canonical fractional-seconds drift footgun the
6977        // shared integer-magnitude codec (1c55a2a) rejects: round-trips
6978        // through `render` as `"1500ms"` on first serialize, breaking
6979        // THEORY.md §V.2.7 render-determinism. Before this wire-up
6980        // `supervisor_view` soft-swallowed the parse error as
6981        // `restart_window: None`, masking the drift as a never-reset
6982        // supervisor.
6983        let root = PathBuf::from("/tmp/x");
6984        let manifest = root.join("caixa.lisp");
6985        let manifest_clone = manifest.clone();
6986        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6987        let c = supervisor_with_window(Some("1.5s"));
6988        let err = layout.verify(&c, &root).unwrap_err();
6989        let LayoutError::RestartWindowViolation { caixa, issue } = err else {
6990            panic!("expected LayoutError::RestartWindowViolation, got {err:?}");
6991        };
6992        assert_eq!(caixa, "demo");
6993        assert!(
6994            issue.contains("1.5s"),
6995            "issue must quote the offending raw value: {issue}",
6996        );
6997    }
6998
6999    #[test]
7000    fn restart_window_violation_on_decimal_shaped_integer() {
7001        // `"1.0s"` — decimal-shaped integer the codec also rejects (a
7002        // canonical authoring form is `"1s"`). Sibling of the fractional
7003        // case; same codec arm.
7004        let root = PathBuf::from("/tmp/x");
7005        let manifest = root.join("caixa.lisp");
7006        let manifest_clone = manifest.clone();
7007        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7008        let c = supervisor_with_window(Some("1.0s"));
7009        let err = layout.verify(&c, &root).unwrap_err();
7010        assert!(
7011            matches!(
7012                err,
7013                LayoutError::RestartWindowViolation { ref caixa, ref issue }
7014                    if caixa == "demo" && issue.contains("1.0s")
7015            ),
7016            "got {err:?}",
7017        );
7018    }
7019
7020    #[test]
7021    fn restart_window_violation_on_leading_sign() {
7022        // `"+30s"` / `"-30s"` — leading-sign drift the codec rejects.
7023        // Canonical form is `"30s"`. Pin both signs separately because
7024        // a future relaxation might accept one but not the other.
7025        for raw in ["+30s", "-30s"] {
7026            let root = PathBuf::from("/tmp/x");
7027            let manifest = root.join("caixa.lisp");
7028            let manifest_clone = manifest.clone();
7029            let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7030            let c = supervisor_with_window(Some(raw));
7031            let err = layout.verify(&c, &root).unwrap_err();
7032            assert!(
7033                matches!(
7034                    err,
7035                    LayoutError::RestartWindowViolation { ref caixa, ref issue }
7036                        if caixa == "demo" && issue.contains(raw)
7037                ),
7038                "leading-sign {raw:?} got {err:?}",
7039            );
7040        }
7041    }
7042
7043    #[test]
7044    fn restart_window_violation_on_unknown_unit() {
7045        // `"30x"` — unknown duration unit. The codec admits only
7046        // `ms`/`s`/`m`/`h`.
7047        let root = PathBuf::from("/tmp/x");
7048        let manifest = root.join("caixa.lisp");
7049        let manifest_clone = manifest.clone();
7050        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7051        let c = supervisor_with_window(Some("30x"));
7052        let err = layout.verify(&c, &root).unwrap_err();
7053        assert!(
7054            matches!(
7055                err,
7056                LayoutError::RestartWindowViolation { ref caixa, .. } if caixa == "demo"
7057            ),
7058            "got {err:?}",
7059        );
7060    }
7061
7062    #[test]
7063    fn restart_window_violation_on_garbage() {
7064        // `"abc"` — pure garbage. The codec's parse fails before the
7065        // unit dispatch; the wrap envelope still surfaces the
7066        // self-locating diagnostic at the source.
7067        let root = PathBuf::from("/tmp/x");
7068        let manifest = root.join("caixa.lisp");
7069        let manifest_clone = manifest.clone();
7070        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7071        let c = supervisor_with_window(Some("abc"));
7072        let err = layout.verify(&c, &root).unwrap_err();
7073        assert!(
7074            matches!(err, LayoutError::RestartWindowViolation { ref caixa, .. } if caixa == "demo"),
7075            "got {err:?}",
7076        );
7077    }
7078
7079    #[test]
7080    fn restart_window_violation_on_empty_string() {
7081        // `""` — empty after trim. The shared codec's digit-only gate
7082        // refuses an empty magnitude. Distinguished here from the
7083        // `None` ("omit the slot") canonical authoring shape: an empty
7084        // string is an authored-but-empty slot, never the author's
7085        // intent.
7086        let root = PathBuf::from("/tmp/x");
7087        let manifest = root.join("caixa.lisp");
7088        let manifest_clone = manifest.clone();
7089        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7090        let c = supervisor_with_window(Some(""));
7091        let err = layout.verify(&c, &root).unwrap_err();
7092        assert!(
7093            matches!(err, LayoutError::RestartWindowViolation { ref caixa, .. } if caixa == "demo"),
7094            "got {err:?}",
7095        );
7096    }
7097
7098    #[test]
7099    fn verify_accepts_supervisor_without_restart_window() {
7100        // `None` is the canonical "omit the slot to express no reset"
7101        // shape — never reaches the codec, validates cleanly.
7102        let root = PathBuf::from("/tmp/x");
7103        let manifest = root.join("caixa.lisp");
7104        let manifest_clone = manifest.clone();
7105        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7106        let c = supervisor_with_window(None);
7107        layout.verify(&c, &root).unwrap();
7108    }
7109
7110    #[test]
7111    fn verify_accepts_supervisor_with_canonical_restart_window() {
7112        // Every canonical form the shared codec round-trips losslessly
7113        // must pass — `"500ms"`, `"30s"`, `"60s"`, `"1m"`, `"2m"`,
7114        // `"1h"`. Pin every form so a future tightening of the codec's
7115        // accepted set surfaces here as a test failure.
7116        for form in ["500ms", "30s", "60s", "1m", "2m", "1h"] {
7117            let root = PathBuf::from("/tmp/x");
7118            let manifest = root.join("caixa.lisp");
7119            let manifest_clone = manifest.clone();
7120            let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7121            let c = supervisor_with_window(Some(form));
7122            layout
7123                .verify(&c, &root)
7124                .unwrap_or_else(|e| panic!("canonical {form:?} must validate, got {e:?}"));
7125        }
7126    }
7127
7128    #[test]
7129    fn restart_window_violation_fires_before_supervisor_view_validate() {
7130        // Diagnostic-precedence pin: a Supervisor with a malformed
7131        // `:restart-window` AND a typed-shape defect on the typed view
7132        // (zero `:max-restarts`, which `SupervisorSpec::validate`'s
7133        // `ZeroMaxRestarts` arm rejects) surfaces the raw-string
7134        // diagnostic first — the narrower self-locating gate wins. Until
7135        // this wire-up landed `supervisor_view` would silently launder
7136        // the malformed `:restart-window` to `None` and then the typed
7137        // view's `ZeroMaxRestarts` gate would surface, masking the
7138        // raw-string footgun.
7139        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
7140        let root = PathBuf::from("/tmp/x");
7141        let manifest = root.join("caixa.lisp");
7142        let manifest_clone = manifest.clone();
7143        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7144        let mut c = caixa(CaixaKind::Supervisor);
7145        c.estrategia = Some(RestartStrategy::OneForOne);
7146        c.max_restarts = Some(0);
7147        c.restart_window = Some("1.5s".into());
7148        c.children = vec![ChildSpec {
7149            caixa: "worker".into(),
7150            versao: "^0.1".into(),
7151            restart: RestartPolicy::Permanent,
7152        }];
7153        let err = layout.verify(&c, &root).unwrap_err();
7154        assert!(
7155            matches!(err, LayoutError::RestartWindowViolation { .. }),
7156            "got {err:?} — RestartWindowViolation must fire before SupervisorViolation",
7157        );
7158    }
7159
7160    #[test]
7161    fn supervisor_slots_on_non_supervisor_fires_before_restart_window_violation() {
7162        // Order pin: a non-Supervisor caixa with a malformed
7163        // `:restart-window` surfaces `SupervisorSlotsOnNonSupervisor`
7164        // (the kind-coherence gate at the top of verify) before the
7165        // raw-string parse gate inside the Supervisor branch — because
7166        // `:restart-window` is foreign to non-Supervisor kinds, the
7167        // kind-coherence diagnostic is the load-bearing one. Mirrors
7168        // the existing `nome_violation_on_*` ordering tests that fence
7169        // the precedence between universal and kind-specific gates.
7170        let root = PathBuf::from("/tmp/x");
7171        let manifest = root.join("caixa.lisp");
7172        let default_lib = root.join("lib").join("demo.lisp");
7173        let layout =
7174            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
7175        let mut c = caixa(CaixaKind::Biblioteca);
7176        c.restart_window = Some("1.5s".into());
7177        let err = layout.verify(&c, &root).unwrap_err();
7178        assert!(
7179            matches!(err, LayoutError::SupervisorSlotsOnNonSupervisor { .. }),
7180            "got {err:?} — kind-coherence must fire before RestartWindowViolation",
7181        );
7182    }
7183
7184    #[test]
7185    fn restart_window_violation_diagnostic_carries_offending_value() {
7186        // Diagnostic-shape pin: the wrap envelope's `issue` carries the
7187        // codec's parser-shaped reason verbatim (which names the
7188        // offending raw value), so the author can grep their caixa.lisp
7189        // for `:restart-window "<value>"` and fix in one edit. Mirrors
7190        // `nome_violation_*_carries_offending_*` shape pins.
7191        let root = PathBuf::from("/tmp/x");
7192        let manifest = root.join("caixa.lisp");
7193        let manifest_clone = manifest.clone();
7194        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7195        let c = supervisor_with_window(Some("0.5m"));
7196        let err = layout.verify(&c, &root).unwrap_err();
7197        let LayoutError::RestartWindowViolation { caixa, issue } = err else {
7198            panic!("expected LayoutError::RestartWindowViolation, got {err:?}");
7199        };
7200        assert_eq!(caixa, "demo");
7201        assert!(
7202            issue.contains("0.5m"),
7203            "issue must quote the offending raw value verbatim: {issue}",
7204        );
7205        assert!(
7206            !issue.is_empty(),
7207            "issue must carry the codec's parser-shaped reason",
7208        );
7209    }
7210
7211    // ── Aplicacao layout tests ──────────────────────────────────────────
7212
7213    #[test]
7214    fn aplicacao_must_have_membros() {
7215        use crate::{Membro, Placement, PlacementStrategy};
7216        let root = PathBuf::from("/tmp/x");
7217        let manifest = root.join("caixa.lisp");
7218        let manifest_clone = manifest.clone();
7219        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7220        let mut c = caixa(CaixaKind::Aplicacao);
7221        c.placement = Some(Placement {
7222            estrategia: PlacementStrategy::Replicated,
7223            clusters: vec!["rio".into()],
7224            affinity: None,
7225            shard_key: None,
7226        });
7227        // No membros → fails
7228        let err = layout.verify(&c, &root).unwrap_err();
7229        assert!(matches!(err, LayoutError::AplicacaoViolation { .. }));
7230
7231        // With membros → passes
7232        c.membros = vec![Membro {
7233            caixa: "service-a".into(),
7234            versao: "^0.1".into(),
7235        }];
7236        layout.verify(&c, &root).unwrap();
7237    }
7238
7239    #[test]
7240    fn aplicacao_self_referential_membro_is_violation() {
7241        // An Aplicacao whose `:membros` names its own `:nome` is a
7242        // one-node lacre-closure recursion. The cross-slot gate fires
7243        // at verify time, surfacing as an AplicacaoViolation that
7244        // names the offending aplicacao — not at lacre-resolve time
7245        // far from source. The `caixa()` helper's `:nome` is "demo".
7246        // Peer of `supervisor_self_referential_child_is_violation`
7247        // on the supervision-tree axis.
7248        use crate::{Membro, Placement, PlacementStrategy};
7249        let root = PathBuf::from("/tmp/x");
7250        let manifest = root.join("caixa.lisp");
7251        let manifest_clone = manifest.clone();
7252        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7253        let mut c = caixa(CaixaKind::Aplicacao);
7254        c.placement = Some(Placement {
7255            estrategia: PlacementStrategy::Replicated,
7256            clusters: vec!["rio".into()],
7257            affinity: None,
7258            shard_key: None,
7259        });
7260        c.membros = vec![
7261            Membro {
7262                caixa: "service-a".into(),
7263                versao: "^0.1".into(),
7264            },
7265            Membro {
7266                caixa: "demo".into(),
7267                versao: "^0.1".into(),
7268            },
7269        ];
7270        let err = layout.verify(&c, &root).unwrap_err();
7271        let LayoutError::AplicacaoViolation { caixa, issue } = err else {
7272            panic!("expected AplicacaoViolation for self-referential membro, got {err:?}");
7273        };
7274        assert_eq!(caixa, "demo");
7275        assert!(
7276            issue.contains("demo") && issue.contains("lists itself"),
7277            "issue must name the self-membering aplicacao, got {issue:?}"
7278        );
7279    }
7280
7281    #[test]
7282    fn aplicacao_distinct_membros_pass_self_membership_gate() {
7283        // Positive control: an Aplicacao whose membros are all distinct
7284        // from its own `:nome` verifies cleanly.
7285        use crate::{Membro, Placement, PlacementStrategy};
7286        let root = PathBuf::from("/tmp/x");
7287        let manifest = root.join("caixa.lisp");
7288        let manifest_clone = manifest.clone();
7289        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7290        let mut c = caixa(CaixaKind::Aplicacao);
7291        c.placement = Some(Placement {
7292            estrategia: PlacementStrategy::Replicated,
7293            clusters: vec!["rio".into()],
7294            affinity: None,
7295            shard_key: None,
7296        });
7297        c.membros = vec![
7298            Membro {
7299                caixa: "service-a".into(),
7300                versao: "^0.1".into(),
7301            },
7302            Membro {
7303                caixa: "service-b".into(),
7304                versao: "^0.1".into(),
7305            },
7306        ];
7307        layout.verify(&c, &root).unwrap();
7308    }
7309
7310    #[test]
7311    fn aplicacao_self_membership_fires_after_view_validate() {
7312        // Diagnostic-precedence pin: a self-referential membro alongside
7313        // a duplicate-:caixa shape surfaces the more-fundamental
7314        // `MembroDuplicate` (from `view.validate()`) first; only when the
7315        // per-membros shape diagnostics pass does the cross-slot
7316        // self-membership gate fire. Mirrors the ordering pin
7317        // `supervisor_self_referential_child_is_violation` carries on
7318        // the peer supervision-tree axis (`view.validate()` runs first,
7319        // then the cross-slot gate). Without this ordering a future
7320        // refactor that swaps the two calls would silently mask the
7321        // narrower per-membro defect.
7322        use crate::{Membro, Placement, PlacementStrategy};
7323        let root = PathBuf::from("/tmp/x");
7324        let manifest = root.join("caixa.lisp");
7325        let manifest_clone = manifest.clone();
7326        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7327        let mut c = caixa(CaixaKind::Aplicacao);
7328        c.placement = Some(Placement {
7329            estrategia: PlacementStrategy::Replicated,
7330            clusters: vec!["rio".into()],
7331            affinity: None,
7332            shard_key: None,
7333        });
7334        c.membros = vec![
7335            Membro {
7336                caixa: "service-a".into(),
7337                versao: "^0.1".into(),
7338            },
7339            Membro {
7340                caixa: "service-a".into(),
7341                versao: "^0.2".into(),
7342            },
7343            Membro {
7344                caixa: "demo".into(),
7345                versao: "^0.1".into(),
7346            },
7347        ];
7348        let err = layout.verify(&c, &root).unwrap_err();
7349        let LayoutError::AplicacaoViolation { issue, .. } = err else {
7350            panic!("expected AplicacaoViolation, got {err:?}");
7351        };
7352        // The per-membros duplicate diagnostic (from view.validate())
7353        // surfaces ahead of the cross-slot self-membership gate, so the
7354        // `service-a` duplicate is named — not the `demo` self-reference.
7355        assert!(
7356            issue.contains("service-a") && issue.contains("more than once"),
7357            "duplicate-:caixa diagnostic must surface before self-membership gate, \
7358             got {issue:?}"
7359        );
7360    }
7361
7362    #[test]
7363    fn mesh_slots_on_servico_rejected() {
7364        // The canonical real-world footgun: an author adds :entrada to a
7365        // :kind Servico expecting it to expose ingress. aplicacao_view
7366        // returns None for Servico, so the slot is the manifest's
7367        // "ignored otherwise" — never validated, never rendered. The
7368        // kind-coherence gate rejects it at build time (before the
7369        // :servicos existence loop), naming the offending slot + kind.
7370        use crate::Entrada;
7371        let root = PathBuf::from("/tmp/x");
7372        let manifest = root.join("caixa.lisp");
7373        let manifest_clone = manifest.clone();
7374        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7375        let mut c = caixa(CaixaKind::Servico);
7376        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7377        c.entrada = Some(Entrada {
7378            host: "demo.example.com".into(),
7379            para: "demo".into(),
7380            paths: vec![],
7381            port: 8080,
7382        });
7383        let err = layout.verify(&c, &root).unwrap_err();
7384        match err {
7385            LayoutError::MeshSlotsOnNonAplicacao { caixa, kind, slots } => {
7386                assert_eq!(caixa, "demo");
7387                assert_eq!(kind, CaixaKind::Servico);
7388                assert_eq!(slots, crate::render::M3_AUTHOR_KEY_ENTRADA);
7389            }
7390            other => panic!("expected MeshSlotsOnNonAplicacao, got {other:?}"),
7391        }
7392    }
7393
7394    #[test]
7395    fn mesh_slots_on_non_aplicacao_lists_slots_in_canonical_order() {
7396        // All five mesh slots declared on a Biblioteca → the diagnostic
7397        // enumerates them in canonical declaration order, deterministic
7398        // across runs. The gate fires on declared-ness only (the values
7399        // need not be a *valid* AplicacaoSpec — aplicacao_view is never
7400        // called for a non-Aplicacao kind).
7401        use crate::{Entrada, Membro, MeshPolicy, Placement, PlacementStrategy, WitContract};
7402        let root = PathBuf::from("/tmp/x");
7403        let manifest = root.join("caixa.lisp");
7404        let manifest_clone = manifest.clone();
7405        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7406        let mut c = caixa(CaixaKind::Biblioteca);
7407        c.membros = vec![Membro {
7408            caixa: "a".into(),
7409            versao: "^0.1".into(),
7410        }];
7411        c.contratos = vec![WitContract {
7412            de: "a".into(),
7413            para: "a".into(),
7414            wit: "wasi:http/proxy".into(),
7415            endpoint: Some("/x".into()),
7416            subject: None,
7417            slot: None,
7418        }];
7419        c.politicas = Some(MeshPolicy::default());
7420        c.placement = Some(Placement {
7421            estrategia: PlacementStrategy::Replicated,
7422            clusters: vec!["rio".into()],
7423            affinity: None,
7424            shard_key: None,
7425        });
7426        c.entrada = Some(Entrada {
7427            host: "x.example.com".into(),
7428            para: "a".into(),
7429            paths: vec![],
7430            port: 8080,
7431        });
7432        let err = layout.verify(&c, &root).unwrap_err();
7433        match err {
7434            LayoutError::MeshSlotsOnNonAplicacao { slots, .. } => {
7435                assert_eq!(
7436                    slots,
7437                    format!(
7438                        "{} {} {} {} {}",
7439                        crate::render::M3_AUTHOR_KEY_MEMBROS,
7440                        crate::render::M3_AUTHOR_KEY_CONTRATOS,
7441                        crate::render::M3_AUTHOR_KEY_POLITICAS,
7442                        crate::render::M3_AUTHOR_KEY_PLACEMENT,
7443                        crate::render::M3_AUTHOR_KEY_ENTRADA,
7444                    )
7445                );
7446            }
7447            other => panic!("expected MeshSlotsOnNonAplicacao, got {other:?}"),
7448        }
7449    }
7450
7451    #[test]
7452    fn servico_without_mesh_slots_still_verifies() {
7453        // Pass-after control: a well-formed Servico carrying no mesh
7454        // slots must remain accepted — the gate keys off declared-ness,
7455        // so it must not over-fire on the common case.
7456        let root = PathBuf::from("/tmp/x");
7457        let servico = root.join("servicos/demo.computeunit.yaml");
7458        let manifest = root.join("caixa.lisp");
7459        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == servico);
7460        let mut c = caixa(CaixaKind::Servico);
7461        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7462        layout.verify(&c, &root).unwrap();
7463    }
7464
7465    #[test]
7466    fn supervisor_slots_on_servico_rejected() {
7467        // Mirror of `mesh_slots_on_servico_rejected` on the
7468        // supervisor-tree slot set: an author adds `:children` to a
7469        // `:kind Servico` expecting it to spawn workers. supervisor_view
7470        // returns None for Servico, so the slot is the manifest's
7471        // "ignored otherwise" — never validated, never reconciled. The
7472        // kind-coherence gate rejects it at build time (before the
7473        // :servicos existence loop), naming the offending slot + kind.
7474        use crate::{ChildSpec, RestartPolicy};
7475        let root = PathBuf::from("/tmp/x");
7476        let manifest = root.join("caixa.lisp");
7477        let manifest_clone = manifest.clone();
7478        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7479        let mut c = caixa(CaixaKind::Servico);
7480        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7481        c.children = vec![ChildSpec {
7482            caixa: "worker".into(),
7483            versao: "^0.1".into(),
7484            restart: RestartPolicy::Permanent,
7485        }];
7486        let err = layout.verify(&c, &root).unwrap_err();
7487        match err {
7488            LayoutError::SupervisorSlotsOnNonSupervisor { caixa, kind, slots } => {
7489                assert_eq!(caixa, "demo");
7490                assert_eq!(kind, CaixaKind::Servico);
7491                assert_eq!(slots, crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN);
7492            }
7493            other => panic!("expected SupervisorSlotsOnNonSupervisor, got {other:?}"),
7494        }
7495    }
7496
7497    #[test]
7498    fn supervisor_slots_on_non_supervisor_lists_slots_in_canonical_order() {
7499        // All four supervisor slots declared on a Biblioteca → the
7500        // diagnostic enumerates them in canonical declaration order
7501        // (`:estrategia` → `:max-restarts` → `:restart-window` →
7502        // `:children`), deterministic across runs. The gate fires on
7503        // declared-ness only (the values need not be a *valid*
7504        // SupervisorSpec — supervisor_view is never called for a
7505        // non-Supervisor kind). Mirror of
7506        // `mesh_slots_on_non_aplicacao_lists_slots_in_canonical_order`.
7507        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
7508        let root = PathBuf::from("/tmp/x");
7509        let manifest = root.join("caixa.lisp");
7510        let manifest_clone = manifest.clone();
7511        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7512        let mut c = caixa(CaixaKind::Biblioteca);
7513        c.estrategia = Some(RestartStrategy::OneForOne);
7514        c.max_restarts = Some(5);
7515        c.restart_window = Some("60s".into());
7516        c.children = vec![ChildSpec {
7517            caixa: "worker".into(),
7518            versao: "^0.1".into(),
7519            restart: RestartPolicy::Permanent,
7520        }];
7521        let err = layout.verify(&c, &root).unwrap_err();
7522        match err {
7523            LayoutError::SupervisorSlotsOnNonSupervisor { slots, .. } => {
7524                assert_eq!(slots, ":estrategia :max-restarts :restart-window :children");
7525            }
7526            other => panic!("expected SupervisorSlotsOnNonSupervisor, got {other:?}"),
7527        }
7528    }
7529
7530    #[test]
7531    fn aplicacao_with_supervisor_slots_rejected() {
7532        // Cross-kind pin: an Aplicacao (the other no-code orchestrator
7533        // kind) that declares a supervisor slot is rejected by the
7534        // supervisor-slot gate, just as a Supervisor declaring a mesh
7535        // slot is rejected by the mesh-slot gate — the two kind ↔ slot
7536        // coherence gates are symmetric and mutually exclusive. The
7537        // gate fires before the Aplicacao typed-graph validation, so
7538        // the diagnostic names the foreign supervisor slot rather than
7539        // a downstream AplicacaoViolation.
7540        use crate::{Membro, Placement, PlacementStrategy, RestartStrategy};
7541        let root = PathBuf::from("/tmp/x");
7542        let manifest = root.join("caixa.lisp");
7543        let manifest_clone = manifest.clone();
7544        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7545        let mut c = caixa(CaixaKind::Aplicacao);
7546        c.membros = vec![Membro {
7547            caixa: "service-a".into(),
7548            versao: "^0.1".into(),
7549        }];
7550        c.placement = Some(Placement {
7551            estrategia: PlacementStrategy::Replicated,
7552            clusters: vec!["rio".into()],
7553            affinity: None,
7554            shard_key: None,
7555        });
7556        c.estrategia = Some(RestartStrategy::OneForAll);
7557        let err = layout.verify(&c, &root).unwrap_err();
7558        match err {
7559            LayoutError::SupervisorSlotsOnNonSupervisor { caixa, kind, slots } => {
7560                assert_eq!(caixa, "demo");
7561                assert_eq!(kind, CaixaKind::Aplicacao);
7562                assert_eq!(slots, crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA);
7563            }
7564            other => panic!("expected SupervisorSlotsOnNonSupervisor, got {other:?}"),
7565        }
7566    }
7567
7568    #[test]
7569    fn servico_without_supervisor_slots_still_verifies() {
7570        // Pass-after control: a well-formed Servico carrying no
7571        // supervisor slots must remain accepted — the gate keys off
7572        // declared-ness, so it must not over-fire on the common case.
7573        let root = PathBuf::from("/tmp/x");
7574        let servico = root.join("servicos/demo.computeunit.yaml");
7575        let manifest = root.join("caixa.lisp");
7576        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == servico);
7577        let mut c = caixa(CaixaKind::Servico);
7578        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7579        layout.verify(&c, &root).unwrap();
7580    }
7581
7582    #[test]
7583    fn servico_slots_on_biblioteca_rejected() {
7584        // Mirror of `mesh_slots_on_servico_rejected` /
7585        // `supervisor_slots_on_servico_rejected` on the M2
7586        // Servico-runtime slot set: an author adds `:limits` to a
7587        // `:kind Biblioteca` expecting per-process sandboxing. The
7588        // caixa-helm / caixa-flux renderers gate on `require_kind(_,
7589        // Servico)`, so the slot is the manifest's "ignored otherwise" —
7590        // never rendered into any artifact. The kind-coherence gate
7591        // rejects it at build time (before the M2 validate blocks),
7592        // naming the offending slot + kind.
7593        use crate::LimitsSpec;
7594        let root = PathBuf::from("/tmp/x");
7595        let manifest = root.join("caixa.lisp");
7596        let lib = root.join("lib").join("demo.lisp");
7597        let manifest_clone = manifest.clone();
7598        let layout =
7599            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == lib);
7600        let mut c = caixa(CaixaKind::Biblioteca);
7601        c.limits = Some(LimitsSpec {
7602            fuel: Some(1_000_000),
7603            ..Default::default()
7604        });
7605        let err = layout.verify(&c, &root).unwrap_err();
7606        match err {
7607            LayoutError::ServicoSlotsOnNonServico { caixa, kind, slots } => {
7608                assert_eq!(caixa, "demo");
7609                assert_eq!(kind, CaixaKind::Biblioteca);
7610                assert_eq!(slots, crate::render::M2_AUTHOR_KEY_LIMITS);
7611            }
7612            other => panic!("expected ServicoSlotsOnNonServico, got {other:?}"),
7613        }
7614    }
7615
7616    #[test]
7617    fn servico_slots_on_non_servico_lists_slots_in_canonical_order() {
7618        // All three M2 slots declared on a Biblioteca → the diagnostic
7619        // enumerates them in canonical declaration order (`:limits` →
7620        // `:behavior` → `:upgrade-from`), deterministic across runs. The
7621        // gate fires on declared-ness only (the values need not pass the
7622        // M2 validate blocks — those run only after the kind-coherence
7623        // gate, and never for a non-Servico declared-slot caixa). Mirror
7624        // of the mesh/supervisor `*_lists_slots_in_canonical_order` pins.
7625        use crate::{BehaviorSpec, LimitsSpec, UpgradeFromEntry, UpgradeInstruction};
7626        let root = PathBuf::from("/tmp/x");
7627        let manifest = root.join("caixa.lisp");
7628        let manifest_clone = manifest.clone();
7629        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7630        let mut c = caixa(CaixaKind::Biblioteca);
7631        c.limits = Some(LimitsSpec {
7632            fuel: Some(1_000_000),
7633            ..Default::default()
7634        });
7635        c.behavior = Some(BehaviorSpec {
7636            on_init: Some(PathBuf::from("lib/init.lisp")),
7637            ..Default::default()
7638        });
7639        c.upgrade_from = vec![UpgradeFromEntry {
7640            from: "0.1.0".into(),
7641            instructions: vec![UpgradeInstruction::Restart],
7642        }];
7643        let err = layout.verify(&c, &root).unwrap_err();
7644        match err {
7645            LayoutError::ServicoSlotsOnNonServico { slots, .. } => {
7646                assert_eq!(
7647                    slots,
7648                    format!(
7649                        "{} {} {}",
7650                        crate::render::M2_AUTHOR_KEY_LIMITS,
7651                        crate::render::M2_AUTHOR_KEY_BEHAVIOR,
7652                        crate::render::M2_AUTHOR_KEY_UPGRADE_FROM,
7653                    )
7654                );
7655            }
7656            other => panic!("expected ServicoSlotsOnNonServico, got {other:?}"),
7657        }
7658    }
7659
7660    #[test]
7661    fn aplicacao_with_servico_slots_rejected() {
7662        // Cross-kind pin (mirror of `aplicacao_with_supervisor_slots_rejected`):
7663        // an Aplicacao that declares an M2 Servico-runtime slot is
7664        // rejected by the Servico-slot gate, just as a Supervisor
7665        // declaring a mesh slot is rejected by the mesh-slot gate — the
7666        // three kind ↔ slot coherence gates are symmetric and mutually
7667        // exclusive. The gate fires before the Aplicacao typed-graph
7668        // validation, so the diagnostic names the foreign M2 slot rather
7669        // than a downstream AplicacaoViolation about missing :membros.
7670        use crate::{Membro, Placement, PlacementStrategy, UpgradeFromEntry, UpgradeInstruction};
7671        let root = PathBuf::from("/tmp/x");
7672        let manifest = root.join("caixa.lisp");
7673        let manifest_clone = manifest.clone();
7674        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7675        let mut c = caixa(CaixaKind::Aplicacao);
7676        c.membros = vec![Membro {
7677            caixa: "service-a".into(),
7678            versao: "^0.1".into(),
7679        }];
7680        c.placement = Some(Placement {
7681            estrategia: PlacementStrategy::Replicated,
7682            clusters: vec!["rio".into()],
7683            affinity: None,
7684            shard_key: None,
7685        });
7686        c.upgrade_from = vec![UpgradeFromEntry {
7687            from: "0.1.0".into(),
7688            instructions: vec![UpgradeInstruction::Restart],
7689        }];
7690        let err = layout.verify(&c, &root).unwrap_err();
7691        match err {
7692            LayoutError::ServicoSlotsOnNonServico { caixa, kind, slots } => {
7693                assert_eq!(caixa, "demo");
7694                assert_eq!(kind, CaixaKind::Aplicacao);
7695                assert_eq!(slots, crate::render::M2_AUTHOR_KEY_UPGRADE_FROM);
7696            }
7697            other => panic!("expected ServicoSlotsOnNonServico, got {other:?}"),
7698        }
7699    }
7700
7701    #[test]
7702    fn servico_with_servico_slots_still_verifies() {
7703        // Pass-after control: a well-formed Servico carrying all three M2
7704        // slots must remain accepted — the gate is guarded by `kind !=
7705        // Servico`, so it must not over-fire on the kind these slots
7706        // exist for. Mirror of `servico_without_{mesh,supervisor}_slots_
7707        // still_verifies` on the legitimate-declaration axis.
7708        use crate::{BehaviorSpec, LimitsSpec, UpgradeFromEntry, UpgradeInstruction};
7709        let root = PathBuf::from("/tmp/x");
7710        let manifest = root.join("caixa.lisp");
7711        let svc = root.join("servicos/demo.computeunit.yaml");
7712        let init = root.join("lib/init.lisp");
7713        let layout =
7714            StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc || p == init);
7715        let mut c = caixa(CaixaKind::Servico);
7716        c.versao = "0.2.0".into();
7717        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7718        c.limits = Some(LimitsSpec {
7719            fuel: Some(1_000_000),
7720            ..Default::default()
7721        });
7722        c.behavior = Some(BehaviorSpec {
7723            on_init: Some(PathBuf::from("lib/init.lisp")),
7724            ..Default::default()
7725        });
7726        c.upgrade_from = vec![UpgradeFromEntry {
7727            from: "0.1.0".into(),
7728            instructions: vec![UpgradeInstruction::Restart],
7729        }];
7730        layout.verify(&c, &root).unwrap();
7731    }
7732
7733    #[test]
7734    fn aplicacao_must_not_have_bibliotecas() {
7735        use crate::{Membro, Placement, PlacementStrategy};
7736        let root = PathBuf::from("/tmp/x");
7737        let manifest = root.join("caixa.lisp");
7738        let manifest_clone = manifest.clone();
7739        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7740        let mut c = caixa(CaixaKind::Aplicacao);
7741        c.bibliotecas = vec!["lib/code.lisp".into()];
7742        c.membros = vec![Membro {
7743            caixa: "x".into(),
7744            versao: "^0.1".into(),
7745        }];
7746        c.placement = Some(Placement {
7747            estrategia: PlacementStrategy::Replicated,
7748            clusters: vec!["rio".into()],
7749            affinity: None,
7750            shard_key: None,
7751        });
7752        let err = layout.verify(&c, &root).unwrap_err();
7753        assert!(matches!(err, LayoutError::AplicacaoOwnsCode(_)));
7754    }
7755
7756    #[test]
7757    fn acao_must_not_have_bibliotecas() {
7758        // Mirror of `supervisor_must_not_have_bibliotecas` /
7759        // `aplicacao_must_not_have_bibliotecas` on the third no-code
7760        // kind. `has_code` fires before the `:ci`-presence gates below
7761        // it, so this must surface `AcaoOwnsCode` even though the
7762        // caixa also lacks a `:ci` slot (which would otherwise surface
7763        // as `MissingCi`) — the more-fundamental "this kind runs no
7764        // code at all" diagnostic wins.
7765        let root = PathBuf::from("/tmp/x");
7766        let manifest = root.join("caixa.lisp");
7767        let manifest_clone = manifest.clone();
7768        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7769        let mut c = caixa(CaixaKind::Acao);
7770        c.bibliotecas = vec!["lib/code.lisp".into()];
7771        let err = layout.verify(&c, &root).unwrap_err();
7772        assert!(matches!(err, LayoutError::AcaoOwnsCode(_)));
7773    }
7774
7775    #[test]
7776    fn acao_without_ci_errors() {
7777        // Mirror of `binario_without_exe_errors` on the fifth required-
7778        // slot axis.
7779        let root = PathBuf::from("/tmp/x");
7780        let manifest = root.join("caixa.lisp");
7781        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
7782        let err = layout.verify(&caixa(CaixaKind::Acao), &root).unwrap_err();
7783        assert!(matches!(err, LayoutError::MissingCi(_)));
7784    }
7785
7786    #[test]
7787    fn acao_with_ci_passes() {
7788        let root = PathBuf::from("/tmp/x");
7789        let manifest = root.join("caixa.lisp");
7790        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
7791        let mut c = caixa(CaixaKind::Acao);
7792        c.ci = Some(canteiro_types::CiRun {
7793            workspace: "pleme-io".into(),
7794            repo: "caixa".into(),
7795            nodes: vec![],
7796        });
7797        layout
7798            .verify(&c, &root)
7799            .expect("an Acao caixa with a declared :ci slot passes layout verify");
7800    }
7801
7802    #[test]
7803    fn acao_with_cyclic_ci_rejected_at_layout() {
7804        // Layout-side wire-up pin on the compound
7805        // [`crate::Caixa::validate_acao_shape`] gate: a `:kind Acao`
7806        // caixa declaring a structurally illegal `:ci` (here — a
7807        // minimal two-node cycle `a → b → a`, one of the three
7808        // `canteiro_types::DecomposeError` arms
7809        // [`crate::decompose_ci`] refuses) surfaces
7810        // [`LayoutError::AcaoViolation`] at `feira build` time rather
7811        // than passing the layout gate silently and deferring the
7812        // diagnostic to [`caixa_actions::validate`] at renderer time.
7813        //
7814        // Pre-lift the layout pipeline only checked `:ci` *presence*
7815        // via [`LayoutError::MissingCi`]; the decompose gate lived
7816        // only wired open-coded at
7817        // [`caixa_actions::validate`] via the substrate-canonical
7818        // [`crate::require_acao_view`] compound helper. This wire-up
7819        // pin locks the new layout-side compound-shape gate in place
7820        // — a future regression that dropped the `if
7821        // caixa.kind().is_acao() { validate_acao_shape() }` block or
7822        // relaxed the diagnostic surface trips here at caixa-core
7823        // build time. Sibling in shape to the peer
7824        // [`aplicacao_must_not_have_bibliotecas`] /
7825        // [`supervisor_must_not_have_bibliotecas`] /
7826        // [`acao_must_not_have_bibliotecas`] layout wire-up pins on
7827        // the sibling per-kind shape gates.
7828        let root = PathBuf::from("/tmp/x");
7829        let manifest = root.join("caixa.lisp");
7830        let manifest_clone = manifest.clone();
7831        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7832        let mut c = caixa(CaixaKind::Acao);
7833        c.ci = Some(canteiro_types::CiRun {
7834            workspace: "pleme-io".into(),
7835            repo: "caixa".into(),
7836            nodes: vec![
7837                canteiro_types::CiNode::new(
7838                    "a",
7839                    canteiro_types::EnvClass::None,
7840                    canteiro_types::ActionRef {
7841                        name: "a".into(),
7842                        command: "true".into(),
7843                        args: vec![],
7844                    },
7845                    vec!["b".into()],
7846                ),
7847                canteiro_types::CiNode::new(
7848                    "b",
7849                    canteiro_types::EnvClass::None,
7850                    canteiro_types::ActionRef {
7851                        name: "b".into(),
7852                        command: "true".into(),
7853                        args: vec![],
7854                    },
7855                    vec!["a".into()],
7856                ),
7857            ],
7858        });
7859        let err = layout.verify(&c, &root).unwrap_err();
7860        match err {
7861            LayoutError::AcaoViolation { caixa, issue } => {
7862                assert_eq!(caixa, "demo");
7863                assert!(
7864                    issue.contains("decompose"),
7865                    "AcaoViolation issue must name the decompose axis (got: {issue:?})",
7866                );
7867                assert!(
7868                    issue.contains("demo"),
7869                    "AcaoViolation issue must name the offending caixa nome via the folded \
7870                     CiDecomposeFailure Display (got: {issue:?})",
7871                );
7872            }
7873            other => panic!("expected AcaoViolation on a cyclic :ci, got {other:?}"),
7874        }
7875    }
7876
7877    #[test]
7878    fn ci_on_non_acao_errors() {
7879        // Mirror of `mesh_slots_on_non_aplicacao_lists_slots_in_canonical_order`
7880        // on the Acao-only `:ci` axis — declaring `:ci` on any other
7881        // kind is the same "silently ignored" footgun the sibling
7882        // mesh-/supervisor-/servico-slot gates already close.
7883        let root = PathBuf::from("/tmp/x");
7884        let manifest = root.join("caixa.lisp");
7885        let manifest_clone = manifest.clone();
7886        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7887        let mut c = caixa(CaixaKind::Biblioteca);
7888        c.ci = Some(canteiro_types::CiRun {
7889            workspace: "pleme-io".into(),
7890            repo: "caixa".into(),
7891            nodes: vec![],
7892        });
7893        let err = layout.verify(&c, &root).unwrap_err();
7894        match err {
7895            LayoutError::CiOnNonAcao { caixa, kind } => {
7896                assert_eq!(caixa, "demo");
7897                assert_eq!(kind, CaixaKind::Biblioteca);
7898            }
7899            other => panic!("expected CiOnNonAcao, got {other:?}"),
7900        }
7901    }
7902
7903    // ── ForeignCodeSlot — kind ↔ code-surface coherence ────────────────
7904
7905    #[test]
7906    fn biblioteca_with_exe_rejected() {
7907        // Fail-before-pass-after pin: a `:kind Biblioteca` declaring
7908        // `:exe` is the "I added a CLI to my library" footgun — the nix
7909        // flake renderer for Binario gates on `require_kind(_, Binario)`,
7910        // so on a Biblioteca the `:exe` path is silently dropped past
7911        // the layout's path-existence check (no executable target is
7912        // ever generated). The diagnostic names the offending kind +
7913        // slot verbatim so the author can grep their caixa.lisp for
7914        // `:exe` and fix in one edit (drop the slot or change
7915        // `:kind Biblioteca` → `:kind Binario`).
7916        let root = PathBuf::from("/tmp/x");
7917        let manifest = root.join("caixa.lisp");
7918        let lib = root.join("lib").join("demo.lisp");
7919        let exe_path = root.join("exe").join("tool");
7920        let layout = StandardLayout::new()
7921            .with_path_exists(move |p| p == manifest || p == lib || p == exe_path);
7922        let mut c = caixa(CaixaKind::Biblioteca);
7923        c.exe = vec!["exe/tool".into()];
7924        let err = layout.verify(&c, &root).unwrap_err();
7925        match err {
7926            LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
7927                assert_eq!(caixa, "demo");
7928                assert_eq!(kind, CaixaKind::Biblioteca);
7929                assert_eq!(slots, ":exe");
7930            }
7931            other => panic!("expected ForeignCodeSlot, got {other:?}"),
7932        }
7933    }
7934
7935    #[test]
7936    fn biblioteca_with_servicos_rejected() {
7937        // Symmetric to `biblioteca_with_exe_rejected` on the
7938        // `:servicos` axis: a `:kind Biblioteca` declaring a Servico
7939        // computeunit silently passed validate and the daemon's
7940        // ComputeUnit / lareira chart never materialized (caixa-helm /
7941        // caixa-flux gate emission on `require_kind(_, Servico)`).
7942        let root = PathBuf::from("/tmp/x");
7943        let manifest = root.join("caixa.lisp");
7944        let lib = root.join("lib").join("demo.lisp");
7945        let svc = root.join("servicos").join("demo.computeunit.yaml");
7946        let layout =
7947            StandardLayout::new().with_path_exists(move |p| p == manifest || p == lib || p == svc);
7948        let mut c = caixa(CaixaKind::Biblioteca);
7949        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7950        let err = layout.verify(&c, &root).unwrap_err();
7951        match err {
7952            LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
7953                assert_eq!(caixa, "demo");
7954                assert_eq!(kind, CaixaKind::Biblioteca);
7955                assert_eq!(slots, ":servicos");
7956            }
7957            other => panic!("expected ForeignCodeSlot, got {other:?}"),
7958        }
7959    }
7960
7961    #[test]
7962    fn biblioteca_with_exe_and_servicos_lists_slots_in_canonical_order() {
7963        // Both foreign code slots declared on a Biblioteca → the
7964        // diagnostic enumerates them in canonical declaration order
7965        // (`:exe` → `:servicos`), deterministic across runs. Mirrors the
7966        // mesh/supervisor/servico-slot `*_lists_slots_in_canonical_order`
7967        // pins on the peer kind ↔ slot algebra axes; drift in the
7968        // [`Caixa::declared_foreign_code_slots`] iteration order surfaces
7969        // here.
7970        let root = PathBuf::from("/tmp/x");
7971        let manifest = root.join("caixa.lisp");
7972        let manifest_clone = manifest.clone();
7973        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7974        let mut c = caixa(CaixaKind::Biblioteca);
7975        c.exe = vec!["exe/tool".into()];
7976        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7977        let err = layout.verify(&c, &root).unwrap_err();
7978        match err {
7979            LayoutError::ForeignCodeSlot { slots, .. } => {
7980                assert_eq!(slots, ":exe :servicos");
7981            }
7982            other => panic!("expected ForeignCodeSlot, got {other:?}"),
7983        }
7984    }
7985
7986    #[test]
7987    fn binario_with_servicos_rejected() {
7988        // The peer footgun on the Binario kind: declaring a Servico
7989        // computeunit on a `:kind Binario` caixa. The caixa-helm /
7990        // caixa-flux renderers gate on `require_kind(_, Servico)`, so
7991        // the `:servicos` slot vanishes past the layout's path-
7992        // existence check — no ComputeUnit, no Helm chart. `:exe` stays
7993        // valid (Binario's native code surface), so the kind-coherence
7994        // diagnostic targets only `:servicos`.
7995        let root = PathBuf::from("/tmp/x");
7996        let manifest = root.join("caixa.lisp");
7997        let exe_path = root.join("exe").join("tool");
7998        let svc = root.join("servicos").join("demo.computeunit.yaml");
7999        let layout = StandardLayout::new()
8000            .with_path_exists(move |p| p == manifest || p == exe_path || p == svc);
8001        let mut c = caixa(CaixaKind::Binario);
8002        c.exe = vec!["exe/tool".into()];
8003        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8004        let err = layout.verify(&c, &root).unwrap_err();
8005        match err {
8006            LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
8007                assert_eq!(caixa, "demo");
8008                assert_eq!(kind, CaixaKind::Binario);
8009                assert_eq!(slots, ":servicos");
8010            }
8011            other => panic!("expected ForeignCodeSlot, got {other:?}"),
8012        }
8013    }
8014
8015    #[test]
8016    fn servico_with_exe_rejected() {
8017        // Symmetric to `binario_with_servicos_rejected` on the other
8018        // code-running peer: a `:kind Servico` declaring an `:exe` is
8019        // the "I added a host-side CLI to my wasm component" footgun —
8020        // the nix flake's Binario target gates on `require_kind(_,
8021        // Binario)`, so the `:exe` path vanishes past the layout's
8022        // path-existence check.
8023        let root = PathBuf::from("/tmp/x");
8024        let manifest = root.join("caixa.lisp");
8025        let svc = root.join("servicos").join("demo.computeunit.yaml");
8026        let exe_path = root.join("exe").join("tool");
8027        let layout = StandardLayout::new()
8028            .with_path_exists(move |p| p == manifest || p == svc || p == exe_path);
8029        let mut c = caixa(CaixaKind::Servico);
8030        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8031        c.exe = vec!["exe/tool".into()];
8032        let err = layout.verify(&c, &root).unwrap_err();
8033        match err {
8034            LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
8035                assert_eq!(caixa, "demo");
8036                assert_eq!(kind, CaixaKind::Servico);
8037                assert_eq!(slots, ":exe");
8038            }
8039            other => panic!("expected ForeignCodeSlot, got {other:?}"),
8040        }
8041    }
8042
8043    #[test]
8044    fn binario_without_servicos_still_verifies() {
8045        // Pass-after control: a well-formed Binario carrying only its
8046        // native `:exe` surface must remain accepted — the gate keys off
8047        // declared-ness of the *foreign* slots, so it must not over-fire
8048        // on the legitimate same-kind case. Mirror of
8049        // `servico_with_servico_slots_still_verifies` on the peer axis.
8050        let root = PathBuf::from("/tmp/x");
8051        let manifest = root.join("caixa.lisp");
8052        let exe_path = root.join("exe").join("tool");
8053        let layout =
8054            StandardLayout::new().with_path_exists(move |p| p == manifest || p == exe_path);
8055        let mut c = caixa(CaixaKind::Binario);
8056        c.exe = vec!["exe/tool".into()];
8057        layout.verify(&c, &root).unwrap();
8058    }
8059
8060    #[test]
8061    fn biblioteca_with_only_bibliotecas_still_verifies() {
8062        // Pass-after control: a well-formed Biblioteca carrying only
8063        // its native `:bibliotecas` surface (or the default
8064        // `lib/<nome>.lisp`) must remain accepted. The gate keys off
8065        // declared-ness of `:exe` + `:servicos` only — `:bibliotecas`
8066        // is deliberately excluded from the foreign-set on every
8067        // code-running kind (`declared_foreign_code_slots` doc), so a
8068        // Biblioteca with the canonical lib surface alone passes.
8069        let root = PathBuf::from("/tmp/x");
8070        let manifest = root.join("caixa.lisp");
8071        let lib = root.join("lib").join("demo.lisp");
8072        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == lib);
8073        layout
8074            .verify(&caixa(CaixaKind::Biblioteca), &root)
8075            .expect("Biblioteca with default lib must verify");
8076    }
8077
8078    #[test]
8079    fn binario_with_bibliotecas_helper_still_verifies() {
8080        // Pass-after control on the deliberate `:bibliotecas`-as-helper
8081        // shape: a `:kind Binario` may legitimately bundle a `lib/`
8082        // helper its nix flake build consumes (the same shape a
8083        // `:kind Servico` may bundle for its wasm-component source).
8084        // The foreign-code-slot gate must NOT fire on `:bibliotecas` for
8085        // either code-running kind; pinned here so a future tightening
8086        // that adds `:bibliotecas` to the foreign set on Binario /
8087        // Servico surfaces as a test failure rather than as a silent
8088        // over-reach.
8089        let root = PathBuf::from("/tmp/x");
8090        let manifest = root.join("caixa.lisp");
8091        let exe_path = root.join("exe").join("tool");
8092        let lib = root.join("lib").join("helper.lisp");
8093        let layout = StandardLayout::new()
8094            .with_path_exists(move |p| p == manifest || p == exe_path || p == lib);
8095        let mut c = caixa(CaixaKind::Binario);
8096        c.exe = vec!["exe/tool".into()];
8097        c.bibliotecas = vec!["lib/helper.lisp".into()];
8098        layout.verify(&c, &root).unwrap();
8099    }
8100
8101    #[test]
8102    fn supervisor_with_exe_still_surfaces_owns_code() {
8103        // Diagnostic-precedence pin: a `:kind Supervisor` declaring
8104        // `:exe` is *both* "Supervisor with code" and "foreign code
8105        // slot". The more-fundamental `SupervisorOwnsCode` must win
8106        // (Supervisor doesn't run code at all — the foreign-slot
8107        // diagnostic would mislead the author toward changing `:kind`
8108        // when the underlying defect is that supervisors orchestrate
8109        // children, not code). Guards the call order in `verify`
8110        // against silent reordering.
8111        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
8112        let root = PathBuf::from("/tmp/x");
8113        let manifest = root.join("caixa.lisp");
8114        let manifest_clone = manifest.clone();
8115        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8116        let mut c = caixa(CaixaKind::Supervisor);
8117        c.estrategia = Some(RestartStrategy::OneForOne);
8118        c.max_restarts = Some(5);
8119        c.exe = vec!["exe/tool".into()];
8120        c.children = vec![ChildSpec {
8121            caixa: "worker".into(),
8122            versao: "^0.1".into(),
8123            restart: RestartPolicy::Permanent,
8124        }];
8125        let err = layout.verify(&c, &root).unwrap_err();
8126        assert!(
8127            matches!(err, LayoutError::SupervisorOwnsCode(_)),
8128            "Supervisor-with-:exe must surface as SupervisorOwnsCode (the more-fundamental \
8129             no-code-at-all diagnostic), got {err:?}"
8130        );
8131    }
8132
8133    #[test]
8134    fn declared_foreign_code_slots_returns_canonical_order() {
8135        // Unit-level pin for the lifted method: the canonical iteration
8136        // order is `:exe` → `:servicos`, independent of which subset is
8137        // populated. Empty input + each single-slot subset + the full
8138        // pair are all checked so a future axis added to the method
8139        // (a hypothetical fifth code-surface slot) is one extension
8140        // point + one assertion update here, not a coordinated rewrite
8141        // across the layout-test sites that reach for the canonical
8142        // order.
8143        let mut c = caixa(CaixaKind::Biblioteca);
8144        assert!(c.declared_foreign_code_slots().is_empty());
8145        c.exe = vec!["exe/tool".into()];
8146        assert_eq!(c.declared_foreign_code_slots(), vec![":exe"]);
8147        c.exe.clear();
8148        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8149        assert_eq!(c.declared_foreign_code_slots(), vec![":servicos"]);
8150        c.exe = vec!["exe/tool".into()];
8151        assert_eq!(c.declared_foreign_code_slots(), vec![":exe", ":servicos"]);
8152    }
8153
8154    #[test]
8155    fn aplicacao_with_unknown_contrato_member_fails() {
8156        use crate::{Membro, Placement, PlacementStrategy, WitContract};
8157        let root = PathBuf::from("/tmp/x");
8158        let manifest = root.join("caixa.lisp");
8159        let manifest_clone = manifest.clone();
8160        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8161        let mut c = caixa(CaixaKind::Aplicacao);
8162        c.membros = vec![Membro {
8163            caixa: "service-a".into(),
8164            versao: "^0.1".into(),
8165        }];
8166        c.contratos = vec![WitContract {
8167            de: "service-a".into(),
8168            para: "phantom".into(),
8169            wit: "wasi:http/proxy".into(),
8170            endpoint: Some("/x".into()),
8171            subject: None,
8172            slot: None,
8173        }];
8174        c.placement = Some(Placement {
8175            estrategia: PlacementStrategy::Replicated,
8176            clusters: vec!["rio".into()],
8177            affinity: None,
8178            shard_key: None,
8179        });
8180        let err = layout.verify(&c, &root).unwrap_err();
8181        assert!(matches!(err, LayoutError::AplicacaoViolation { .. }));
8182    }
8183
8184    #[test]
8185    fn limits_zero_axis_surfaces_as_layout_violation() {
8186        use crate::LimitsSpec;
8187        let root = PathBuf::from("/tmp/x");
8188        let manifest = root.join("caixa.lisp");
8189        let svc = root.join("servicos/demo.computeunit.yaml");
8190        let mut c = caixa(CaixaKind::Servico);
8191        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8192        c.limits = Some(LimitsSpec {
8193            fuel: Some(0),
8194            ..Default::default()
8195        });
8196        let manifest_clone = manifest.clone();
8197        let svc_clone = svc.clone();
8198        let layout =
8199            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8200        let err = layout.verify(&c, &root).unwrap_err();
8201        let LayoutError::LimitsViolation { caixa, issue } = err else {
8202            panic!("expected LimitsViolation, got {err:?}");
8203        };
8204        assert_eq!(caixa, "demo");
8205        assert!(issue.contains(":fuel"), "issue must name the axis: {issue}");
8206    }
8207
8208    #[test]
8209    fn limits_well_formed_passes_layout() {
8210        use crate::LimitsSpec;
8211        use std::time::Duration;
8212        let root = PathBuf::from("/tmp/x");
8213        let manifest = root.join("caixa.lisp");
8214        let svc = root.join("servicos/demo.computeunit.yaml");
8215        let mut c = caixa(CaixaKind::Servico);
8216        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8217        c.limits = Some(LimitsSpec {
8218            memory: Some(64 * 1024 * 1024),
8219            fuel: Some(1_000_000),
8220            wall_clock: Some(Duration::from_secs(30)),
8221            cpu: Some(500),
8222        });
8223        let manifest_clone = manifest.clone();
8224        let svc_clone = svc.clone();
8225        let layout =
8226            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8227        layout.verify(&c, &root).unwrap();
8228    }
8229
8230    #[test]
8231    fn supervisor_with_valid_children_passes() {
8232        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
8233        let root = PathBuf::from("/tmp/x");
8234        let manifest = root.join("caixa.lisp");
8235        let manifest_clone = manifest.clone();
8236        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8237        let mut c = caixa(CaixaKind::Supervisor);
8238        c.estrategia = Some(RestartStrategy::OneForOne);
8239        c.max_restarts = Some(5);
8240        c.children = vec![
8241            ChildSpec {
8242                caixa: "worker".into(),
8243                versao: "^0.1".into(),
8244                restart: RestartPolicy::Permanent,
8245            },
8246            ChildSpec {
8247                caixa: "cache".into(),
8248                versao: "^0.1".into(),
8249                restart: RestartPolicy::Transient,
8250            },
8251        ];
8252        layout.verify(&c, &root).unwrap();
8253    }
8254
8255    // ── :upgrade-from entry validation pipes through layout ─────────────
8256
8257    #[test]
8258    fn upgrade_invalid_module_surfaces_as_layout_violation() {
8259        // End-to-end pin that
8260        // [`crate::UpgradeFromEntry::validate`] runs *inside*
8261        // `LayoutInvariants::verify` and surfaces value-shape
8262        // violations through the new `UpgradeViolation` arm
8263        // (parallel to `BehaviorViolation`, `LimitsViolation`,
8264        // `SupervisorViolation`, `AplicacaoViolation`). Until this
8265        // wiring landed the entry validator was unreachable from any
8266        // build-pipeline caller — an `:upgrade-from
8267        // ((:from "0.1.0" :instructions ((:load-module "Hello")))` (uppercase
8268        // module name the K8s apiserver would reject on the per-
8269        // ComputeUnit `metadata.name` axis) silently passed
8270        // `feira lint` / `feira build` and surfaced only at wasm-engine
8271        // hot-upgrade time as a per-backend "module not found" /
8272        // `code:load_module/1` `badarg` runtime error, far from the
8273        // source caixa.lisp. Pinning the wiring here so a future
8274        // refactor that drops the `entry.validate()` call surfaces as
8275        // a build-pipeline regression at this test, not as a runtime
8276        // surprise per consumer.
8277        use crate::{UpgradeFromEntry, UpgradeInstruction};
8278        use std::path::PathBuf;
8279        let root = PathBuf::from("/tmp/x");
8280        let manifest = root.join("caixa.lisp");
8281        let svc = root.join("servicos/demo.computeunit.yaml");
8282        let mut c = caixa(CaixaKind::Servico);
8283        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8284        c.upgrade_from = vec![UpgradeFromEntry {
8285            from: "0.1.0".into(),
8286            instructions: vec![UpgradeInstruction::LoadModule {
8287                module: "Hello".into(), // uppercase — not DNS-1123
8288            }],
8289        }];
8290        let manifest_clone = manifest.clone();
8291        let svc_clone = svc.clone();
8292        let layout =
8293            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8294        let err = layout.verify(&c, &root).unwrap_err();
8295        let LayoutError::UpgradeViolation { caixa, issue } = err else {
8296            panic!("expected UpgradeViolation, got {err:?}");
8297        };
8298        assert_eq!(caixa, "demo");
8299        assert!(
8300            issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_LOAD_MODULE),
8301            "issue must name the lisp-form of the offending instruction: {issue}"
8302        );
8303        assert!(
8304            issue.contains("Hello"),
8305            "issue must name the offending :module verbatim: {issue}"
8306        );
8307    }
8308
8309    #[test]
8310    fn upgrade_empty_module_surfaces_as_layout_violation() {
8311        // Companion to the DNS-1123 footgun above on the narrower
8312        // empty arm. Every Module-bearing variant's empty value
8313        // reaches the layout pipeline through the kind-tagged
8314        // `ModuleEmpty` diagnostic naming its lisp-form.
8315        use crate::{UpgradeFromEntry, UpgradeInstruction};
8316        use std::path::PathBuf;
8317        let root = PathBuf::from("/tmp/x");
8318        let manifest = root.join("caixa.lisp");
8319        let svc = root.join("servicos/demo.computeunit.yaml");
8320        let mut c = caixa(CaixaKind::Servico);
8321        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8322        c.upgrade_from = vec![UpgradeFromEntry {
8323            from: "0.1.0".into(),
8324            instructions: vec![UpgradeInstruction::SoftPurge {
8325                module: String::new(),
8326            }],
8327        }];
8328        let manifest_clone = manifest.clone();
8329        let svc_clone = svc.clone();
8330        let layout =
8331            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8332        let err = layout.verify(&c, &root).unwrap_err();
8333        let LayoutError::UpgradeViolation { caixa, issue } = err else {
8334            panic!("expected UpgradeViolation, got {err:?}");
8335        };
8336        assert_eq!(caixa, "demo");
8337        assert!(
8338            issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_SOFT_PURGE),
8339            "issue must name the lisp-form of the empty instruction: {issue}"
8340        );
8341    }
8342
8343    #[test]
8344    fn upgrade_invalid_state_change_script_surfaces_as_layout_violation() {
8345        // Pins that the b0c8389 script value-shape gates
8346        // (AbsoluteScript / ParentEscapeScript) — previously
8347        // unreachable from any build-pipeline caller — now fire
8348        // through the same `UpgradeViolation` arm before the path-
8349        // existence pass would otherwise emit the less-helpful
8350        // "missing upgrade-script" (or, worse, *succeed* against
8351        // /etc/passwd, proving the sandbox bypass — same defect
8352        // the b0c8389 BehaviorSpec wiring closed on the peer M2
8353        // slot).
8354        use crate::{UpgradeFromEntry, UpgradeInstruction};
8355        use std::path::PathBuf;
8356        let root = PathBuf::from("/tmp/x");
8357        let manifest = root.join("caixa.lisp");
8358        let svc = root.join("servicos/demo.computeunit.yaml");
8359        let etc_passwd = PathBuf::from("/etc/passwd");
8360        let mut c = caixa(CaixaKind::Servico);
8361        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8362        c.upgrade_from = vec![UpgradeFromEntry {
8363            from: "0.1.0".into(),
8364            instructions: vec![UpgradeInstruction::StateChange {
8365                script: PathBuf::from("/etc/passwd"),
8366            }],
8367        }];
8368        let manifest_clone = manifest.clone();
8369        let svc_clone = svc.clone();
8370        let etc_passwd_clone = etc_passwd.clone();
8371        // Critically: /etc/passwd "exists" in our mock — without the
8372        // value-shape pre-check, the existence loop would *succeed*
8373        // and the path-traversal exit from the project sandbox would
8374        // pass `feira build` silently.
8375        let layout = StandardLayout::new().with_path_exists(move |p| {
8376            p == manifest_clone || p == svc_clone || p == etc_passwd_clone
8377        });
8378        let err = layout.verify(&c, &root).unwrap_err();
8379        let LayoutError::UpgradeViolation { caixa, issue } = err else {
8380            panic!("expected UpgradeViolation, got {err:?}");
8381        };
8382        assert_eq!(caixa, "demo");
8383        assert!(
8384            issue.contains("absolute") || issue.contains("Absolute"),
8385            "issue must name the violation kind (absolute): {issue}"
8386        );
8387    }
8388
8389    #[test]
8390    fn upgrade_well_formed_passes_layout() {
8391        // Positive control — every documented authoring shape
8392        // (`:load-module`, `:state-change` with a relative path,
8393        // `:soft-purge`, `:purge`, sole `:restart`) passes the wired
8394        // gate. The typed sequence (`:load-module` → `:state-change`
8395        // → `:soft-purge` → `:purge`) lives in one entry; the sole
8396        // `:restart` fallback lives in a *separate* entry on a
8397        // different `:from` (the within-entry restart-exclusivity
8398        // gate added in this commit rejects mixing the fallback with
8399        // the typed sequence — per the UpgradeInstruction::Restart
8400        // doc, `:restart` is terminal and any other instructions in
8401        // the same entry are dead code). Drift here = a future
8402        // tighten that rejects any canonical shape surfaces as a
8403        // regression at this layout-level pin, not piecemeal across
8404        // per-renderer call sites.
8405        //
8406        // `:soft-purge` and `:purge` target *distinct* old-version
8407        // modules (`hello-rio-old` and `hello-rio-oldest`) so the
8408        // within-entry cleanup-singularity gate
8409        // (`UpgradeError::DuplicateCleanup`) passes — that gate
8410        // rejects more than one cleanup per module per entry (one
8411        // semantic per old version; mixing drain + discard on one
8412        // module is the soft-then-hard fallback footgun the author
8413        // shouldn't write because the operator handles cleanup
8414        // failure escalation itself). The two distinct names cover
8415        // the legitimate "drain a recent old, hard-discard an
8416        // older-still" shape — both authoring forms remain load-
8417        // bearing in this positive-control enumeration.
8418        use crate::{BehaviorSpec, UpgradeFromEntry, UpgradeInstruction};
8419        use std::path::PathBuf;
8420        let root = PathBuf::from("/tmp/x");
8421        let manifest = root.join("caixa.lisp");
8422        let svc = root.join("servicos/demo.computeunit.yaml");
8423        let migration = root.join("lib/migrations/v01-to-v02.lisp");
8424        let on_state_change = root.join("lib/migrations.lisp");
8425        let mut c = caixa(CaixaKind::Servico);
8426        // `:versao` past both entries' `:from` so the cross-slot
8427        // precedence gate (`FromNotBeforeVersao`) lets this canonical
8428        // authoring shape through to the positive-control assertion.
8429        c.versao = "0.2.0".into();
8430        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8431        // `:on-state-change` declared alongside the `(:state-change …)`
8432        // instruction below — the cross-slot composition gate
8433        // (`validate_upgrade_from_against_behavior`) rejects a
8434        // `:state-change` without the callback, so the canonical
8435        // authoring shape this positive control pins now includes the
8436        // runtime delivery hook (the `gen_server:code_change/3` analog
8437        // that the per-version script is invoked through during hot
8438        // upgrade per the upgrade.rs module doc "Composes with"
8439        // promise).
8440        c.behavior = Some(BehaviorSpec {
8441            on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
8442            ..Default::default()
8443        });
8444        c.upgrade_from = vec![
8445            UpgradeFromEntry {
8446                from: "0.1.0".into(),
8447                instructions: vec![
8448                    UpgradeInstruction::LoadModule {
8449                        module: "hello-rio".into(),
8450                    },
8451                    UpgradeInstruction::StateChange {
8452                        script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
8453                    },
8454                    UpgradeInstruction::SoftPurge {
8455                        module: "hello-rio-old".into(),
8456                    },
8457                    UpgradeInstruction::Purge {
8458                        module: "hello-rio-oldest".into(),
8459                    },
8460                ],
8461            },
8462            UpgradeFromEntry {
8463                from: "0.0.9".into(),
8464                instructions: vec![UpgradeInstruction::Restart],
8465            },
8466        ];
8467        let manifest_clone = manifest.clone();
8468        let svc_clone = svc.clone();
8469        let migration_clone = migration.clone();
8470        let on_state_change_clone = on_state_change.clone();
8471        let layout = StandardLayout::new().with_path_exists(move |p| {
8472            p == manifest_clone
8473                || p == svc_clone
8474                || p == migration_clone
8475                || p == on_state_change_clone
8476        });
8477        layout.verify(&c, &root).unwrap();
8478    }
8479
8480    #[test]
8481    fn upgrade_from_restart_mixed_surfaces_as_upgrade_violation() {
8482        // Wiring pin: the within-entry `(:restart)`-exclusivity gate
8483        // (`UpgradeFromEntry::validate_restart_exclusive`) lands on
8484        // the same `LayoutError::UpgradeViolation` axis the per-entry
8485        // shape gate (26da2c7), the cross-entry duplicate-`:from`
8486        // gate (7c6aef2), and the cross-slot `:from < :versao`
8487        // precedence gate (de7ab1a) already do. A caixa.lisp whose
8488        // `:upgrade-from` entry mixes `(:restart)` with a typed
8489        // instruction surfaces at `feira build` time naming the
8490        // offending caixa + the entry's `:from` rather than silently
8491        // passing into the wasm-operator with semantically dead code
8492        // in the operator's dispatch table. Mirrors
8493        // `upgrade_from_duplicate_surfaces_as_upgrade_violation` on
8494        // the peer cross-entry gate.
8495        use crate::{UpgradeFromEntry, UpgradeInstruction};
8496        let root = PathBuf::from("/tmp/x");
8497        let manifest = root.join("caixa.lisp");
8498        let svc = root.join("servicos/demo.computeunit.yaml");
8499        let mut c = caixa(CaixaKind::Servico);
8500        c.versao = "0.2.0".into();
8501        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8502        c.upgrade_from = vec![UpgradeFromEntry {
8503            from: "0.1.0".into(),
8504            instructions: vec![
8505                UpgradeInstruction::LoadModule {
8506                    module: "hello-rio".into(),
8507                },
8508                UpgradeInstruction::Restart,
8509            ],
8510        }];
8511        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
8512        let err = layout.verify(&c, &root).unwrap_err();
8513        let LayoutError::UpgradeViolation { caixa, issue } = err else {
8514            panic!("expected LayoutError::UpgradeViolation for restart-mixed entry, got {err:?}");
8515        };
8516        assert_eq!(caixa, "demo");
8517        assert!(
8518            issue.contains("0.1.0"),
8519            "UpgradeViolation issue must name the offending entry's `:from` verbatim, got \
8520             {issue:?}"
8521        );
8522        assert!(
8523            issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_RESTART),
8524            "UpgradeViolation issue must name the `:restart` axis verbatim, got {issue:?}"
8525        );
8526        assert!(
8527            issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_LOAD_MODULE),
8528            "UpgradeViolation issue must name the non-:restart peer instruction's lisp-form \
8529             verbatim, got {issue:?}"
8530        );
8531    }
8532
8533    #[test]
8534    fn upgrade_from_restart_duplicated_surfaces_as_upgrade_violation() {
8535        // Companion arm: the duplicate-`(:restart)` mode of
8536        // `RestartNotExclusive` (no typed peers, just multiple
8537        // `Restart` variants) surfaces through the same wiring as the
8538        // mixed-with-typed mode above. The diagnostic still names the
8539        // offending entry's `:from` verbatim even when `other_kinds`
8540        // is empty.
8541        use crate::{UpgradeFromEntry, UpgradeInstruction};
8542        let root = PathBuf::from("/tmp/x");
8543        let manifest = root.join("caixa.lisp");
8544        let svc = root.join("servicos/demo.computeunit.yaml");
8545        let mut c = caixa(CaixaKind::Servico);
8546        c.versao = "0.2.0".into();
8547        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8548        c.upgrade_from = vec![UpgradeFromEntry {
8549            from: "0.1.0".into(),
8550            instructions: vec![UpgradeInstruction::Restart, UpgradeInstruction::Restart],
8551        }];
8552        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
8553        let err = layout.verify(&c, &root).unwrap_err();
8554        let LayoutError::UpgradeViolation { caixa, issue } = err else {
8555            panic!(
8556                "expected LayoutError::UpgradeViolation for duplicate-restart entry, got \
8557                 {err:?}"
8558            );
8559        };
8560        assert_eq!(caixa, "demo");
8561        assert!(
8562            issue.contains("0.1.0"),
8563            "UpgradeViolation issue must name the offending entry's `:from` verbatim, got \
8564             {issue:?}"
8565        );
8566        assert!(
8567            issue.contains("(:restart)")
8568                || issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_RESTART),
8569            "UpgradeViolation issue must name the `:restart` axis verbatim, got {issue:?}"
8570        );
8571    }
8572
8573    #[test]
8574    fn upgrade_from_invalid_surfaces_as_layout_violation() {
8575        // The `:from` semver gate (`UpgradeError::FromInvalid`)
8576        // was likewise unreachable before this wiring landed — a
8577        // typo-shaped `:from "v0.1.0"` (git-tag-shape leaking into
8578        // the semver slot) silently passed `feira build` and
8579        // surfaced only when the operator's hot-upgrade decision
8580        // engine tried to match against the version key it couldn't
8581        // parse. Now wired through `UpgradeViolation` with the
8582        // peer-shaped `{ from, reason }` payload — the
8583        // parser-shaped `reason` flows through `Display` so the
8584        // wrapped issue string carries both the offending value
8585        // *and* the SemVer-2 parser's wording (peer with the
8586        // `VersaoInvalid` / `MembroVersaoInvalid` envelopes on the
8587        // sibling SemVer-2 axes).
8588        use crate::{UpgradeFromEntry, UpgradeInstruction};
8589        use std::path::PathBuf;
8590        let root = PathBuf::from("/tmp/x");
8591        let manifest = root.join("caixa.lisp");
8592        let svc = root.join("servicos/demo.computeunit.yaml");
8593        let mut c = caixa(CaixaKind::Servico);
8594        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8595        c.upgrade_from = vec![UpgradeFromEntry {
8596            from: "v0.1.0".into(), // git-tag-shape, not semver
8597            instructions: vec![UpgradeInstruction::Restart],
8598        }];
8599        let manifest_clone = manifest.clone();
8600        let svc_clone = svc.clone();
8601        let layout =
8602            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8603        let err = layout.verify(&c, &root).unwrap_err();
8604        let LayoutError::UpgradeViolation { caixa, issue } = err else {
8605            panic!("expected UpgradeViolation, got {err:?}");
8606        };
8607        assert_eq!(caixa, "demo");
8608        assert!(
8609            issue.contains("v0.1.0"),
8610            "UpgradeViolation issue must name the offending :from value verbatim, got {issue:?}"
8611        );
8612        assert!(
8613            issue.contains(":from"),
8614            "UpgradeViolation issue must name the :from slot verbatim, got {issue:?}"
8615        );
8616        // Pin the parser-shaped reason flow-through: the renamed
8617        // `FromInvalid { from, reason }` carries the SemVer-2 parser's
8618        // wording verbatim, and the [`UpgradeError`] Display routes it
8619        // into the wrapped `issue` string so the layout envelope
8620        // surfaces both the offending value *and* the parser's
8621        // diagnosis. Mirrors the peer flow-through on
8622        // `ManifestError::VersaoInvalid` (top-level `:versao`) and
8623        // `AplicacaoError::MembroVersaoInvalid` (`:membros :versao`).
8624        assert!(
8625            issue.contains("SemVer-2"),
8626            "UpgradeViolation issue must carry the parser-shaped reason (\"SemVer-2\"), got {issue:?}"
8627        );
8628    }
8629
8630    #[test]
8631    fn missing_lib_gate_routes_through_kind_requires_lib_and_caixa_nome() {
8632        // Fail-before-pass-after pin on the two-part converge landed
8633        // at layout.rs:844-847:
8634        //   (a) `caixa.kind().is_biblioteca()` →
8635        //       `caixa.kind().requires_lib()` — routes the biblioteca
8636        //       required-slot gate onto the same `requires_*()`
8637        //       predicate family the three sibling required-slot
8638        //       gates (`requires_exe()` at :856, `requires_servicos()`
8639        //       at :860, `requires_ci()` at :874) already key off.
8640        //       All four gates in the block now share one convention;
8641        //       a future kind that gains its own required-slot gate
8642        //       (an M4/M5 typed arm the CAIXA-SDLC §I six-kind roster
8643        //       may grow) reaches for the same predicate family and
8644        //       inherits the accessor discipline for free.
8645        //   (b) raw `caixa.nome` → `caixa.nome()` — routes the
8646        //       `expected` path composition through the typed
8647        //       [`crate::Caixa::nome`] accessor, closing the last
8648        //       unlifted raw `caixa.nome` production field-access
8649        //       site in `caixa-core/src/layout.rs` (every peer
8650        //       diagnostic in the file already routes through
8651        //       `caixa.nome().to_string()`).
8652        //
8653        // The behavioral pin: for a Biblioteca kind with no fallback
8654        // `lib/<nome>.lisp` file, MissingLib fires and its `expected`
8655        // path composes through `Caixa::nome()`; for every other
8656        // kind, MissingLib does NOT fire (the gate short-circuits on
8657        // kinds where `requires_lib()` returns false), even when the
8658        // fallback file is likewise absent. A future regression that
8659        // reroutes the gate off `requires_lib()` (e.g. onto
8660        // `is_biblioteca()` again, or onto a hand-authored
8661        // `matches!(caixa.kind(), CaixaKind::Biblioteca)`) that
8662        // *happens* to agree byte-for-byte on today's arm-set trips
8663        // this test the moment a future kind's `requires_lib()`
8664        // returns true for a non-`Biblioteca` arm (or the sibling
8665        // required-slot gates diverge from the same convention).
8666        let root = PathBuf::from("/tmp/x");
8667        let manifest = root.join("caixa.lisp");
8668        let manifest_only = manifest.clone();
8669        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_only);
8670
8671        // Biblioteca kind + no lib fallback → MissingLib fires with
8672        // the expected path composed through `Caixa::nome()`.
8673        let bib = caixa(CaixaKind::Biblioteca);
8674        assert!(
8675            bib.kind().requires_lib(),
8676            "requires_lib() must return true for Biblioteca — the four-required-\
8677             slot-gate family's routing depends on this arm's assignment"
8678        );
8679        let err = layout.verify(&bib, &root).unwrap_err();
8680        let LayoutError::MissingLib {
8681            caixa: cname,
8682            expected,
8683        } = err
8684        else {
8685            panic!("expected MissingLib for Biblioteca kind with no lib fallback, got {err:?}");
8686        };
8687        assert_eq!(
8688            cname,
8689            bib.nome(),
8690            "MissingLib `caixa:` carrier must byte-equal Caixa::nome()"
8691        );
8692        assert_eq!(
8693            expected,
8694            root.join(crate::render::LAYOUT_DIR_LIB)
8695                .join(format!("{}.lisp", bib.nome())),
8696            "MissingLib `expected:` path must compose through Caixa::nome() \
8697             verbatim — a raw-field-access regression would silently drift \
8698             the composed path on any future `:nome` axis extension \
8699             (namespace-qualified rewrite, per-cluster alias overlay)"
8700        );
8701
8702        // Non-Biblioteca kinds → the MissingLib gate short-circuits.
8703        // Different kinds fail on their own required-slot gate
8704        // (BinarioWithoutExe, ServicoWithoutServicos, MissingCi) or
8705        // on downstream M2/M3 invariants; none of them may surface as
8706        // MissingLib, because `requires_lib()` returns false for each.
8707        for kind in [
8708            CaixaKind::Binario,
8709            CaixaKind::Servico,
8710            CaixaKind::Supervisor,
8711            CaixaKind::Aplicacao,
8712            CaixaKind::Acao,
8713        ] {
8714            assert!(
8715                !kind.requires_lib(),
8716                "requires_lib() must return false for {kind:?} — the \
8717                 four-required-slot-gate family's arm assignment pins \
8718                 exactly one kind (Biblioteca) as the arm that requires \
8719                 a `lib/` entry"
8720            );
8721            let c = caixa(kind);
8722            let result = layout.verify(&c, &root);
8723            assert!(
8724                !matches!(result, Err(LayoutError::MissingLib { .. })),
8725                "MissingLib gate at layout.rs:844 must short-circuit for \
8726                 kinds where requires_lib() returns false; unexpectedly \
8727                 fired for {kind:?}: {result:?}"
8728            );
8729        }
8730    }
8731
8732    #[test]
8733    fn missing_lib_ctor_matches_struct_literal_wrap() {
8734        // Equivalence pin locking [`LayoutError::missing_lib`] to its
8735        // struct-literal peer under PartialEq. The pre-lift wire-up at
8736        // layout.rs:1010 read `LayoutError::MissingLib { caixa:
8737        // caixa.nome().to_string(), expected }`; the post-lift
8738        // dispatch reads `LayoutError::missing_lib(caixa, expected)`.
8739        // Both must produce byte-equal variants — a silent divergence
8740        // (a `to_lowercase()`, a `trim()`, a lost `.to_string()` copy,
8741        // an accidental `.clone()` of the wrong side of the `expected`
8742        // path) surfaces here rather than at a downstream diagnostic-
8743        // shape drift.
8744        let bib = caixa(CaixaKind::Biblioteca);
8745        let expected = PathBuf::from("/tmp/x")
8746            .join(crate::render::LAYOUT_DIR_LIB)
8747            .join(format!("{}.lisp", bib.nome()));
8748        let struct_lit = LayoutError::MissingLib {
8749            caixa: bib.nome().to_string(),
8750            expected: expected.clone(),
8751        };
8752        let ctor = LayoutError::missing_lib(&bib, expected);
8753        assert_eq!(
8754            struct_lit, ctor,
8755            "missing_lib ctor must byte-equal the pre-lift struct-literal"
8756        );
8757    }
8758
8759    #[test]
8760    fn missing_lib_ctor_projects_nome_through_accessor() {
8761        // Accessor-fidelity pin: any future `:nome` axis extension
8762        // (namespace-qualified rewrite `pleme-io/<nome>`, per-cluster
8763        // alias overlay, case-normalization pass) that lands on
8764        // [`crate::Caixa::nome`] must reach the `caixa:` carrier
8765        // through this projection rather than a raw field access.
8766        // The neighbour required-slot ctor family
8767        // ([`layout_nome_only_ctors!`]) projects the same way; this
8768        // pin locks `missing_lib` onto the same discipline so the
8769        // whole `LayoutError` family stays coherent under any future
8770        // `:nome` rewrite.
8771        //
8772        // Deliberately uses a byte-distinctive nome ("named-lib") so
8773        // a regression that hard-codes a fixture literal at the ctor
8774        // body (rather than projecting through the accessor) drops
8775        // the bytes and trips the assertion.
8776        let mut bib = caixa(CaixaKind::Biblioteca);
8777        bib.nome = "named-lib".into();
8778        let expected = PathBuf::from("/srv")
8779            .join(crate::render::LAYOUT_DIR_LIB)
8780            .join(format!("{}.lisp", bib.nome()));
8781        let err = LayoutError::missing_lib(&bib, expected.clone());
8782        let LayoutError::MissingLib {
8783            caixa: cname,
8784            expected: got,
8785        } = err
8786        else {
8787            panic!("missing_lib ctor must construct the MissingLib variant, got a foreign arm");
8788        };
8789        assert_eq!(
8790            cname,
8791            bib.nome(),
8792            "missing_lib `caixa:` carrier must project through Caixa::nome()"
8793        );
8794        assert_eq!(
8795            got, expected,
8796            "missing_lib `expected:` path must pass through verbatim"
8797        );
8798    }
8799
8800    #[test]
8801    fn missing_lib_verify_wire_up_routes_through_ctor() {
8802        // Behavioural pin: [`StandardLayout::verify`] must reach the
8803        // `MissingLib` variant through the newly lifted ctor rather
8804        // than a residual struct-literal block. The end-to-end
8805        // observable — a Biblioteca with no lib fallback — must
8806        // surface a `MissingLib` whose `caixa:` and `expected:`
8807        // carriers are byte-equal to what the ctor would produce
8808        // when called directly.
8809        let root = PathBuf::from("/opt/pkg");
8810        let manifest = root.join("caixa.lisp");
8811        let manifest_only = manifest.clone();
8812        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_only);
8813        let bib = caixa(CaixaKind::Biblioteca);
8814        let expected = root
8815            .join(crate::render::LAYOUT_DIR_LIB)
8816            .join(format!("{}.lisp", bib.nome()));
8817
8818        let observed = layout.verify(&bib, &root).unwrap_err();
8819        let synthesized = LayoutError::missing_lib(&bib, expected);
8820        assert_eq!(
8821            observed, synthesized,
8822            "StandardLayout::verify must reach MissingLib through the missing_lib ctor \
8823             — a residual struct-literal block would silently diverge on any future \
8824             accessor projection change"
8825        );
8826    }
8827
8828    #[test]
8829    fn ci_on_non_acao_ctor_matches_struct_literal_wrap() {
8830        // Equivalence pin locking [`LayoutError::ci_on_non_acao`] to
8831        // its struct-literal peer under `PartialEq`. The pre-lift
8832        // wire-up at `caixa-core/src/manifest.rs:5586` read
8833        // `LayoutError::CiOnNonAcao { caixa: self.nome().to_string(),
8834        // kind: self.kind() }`; the post-lift dispatch reads
8835        // `LayoutError::ci_on_non_acao(self)`. Both must produce
8836        // byte-equal variants — a silent divergence (a `to_lowercase()`,
8837        // a lost `.to_string()` copy, a swapped kind-copy) surfaces
8838        // here rather than at a downstream diagnostic-shape drift.
8839        let bib = caixa(CaixaKind::Biblioteca);
8840        let struct_lit = LayoutError::CiOnNonAcao {
8841            caixa: bib.nome().to_string(),
8842            kind: bib.kind(),
8843        };
8844        let ctor = LayoutError::ci_on_non_acao(&bib);
8845        assert_eq!(
8846            struct_lit, ctor,
8847            "ci_on_non_acao ctor must byte-equal the pre-lift struct-literal"
8848        );
8849    }
8850
8851    #[test]
8852    fn ci_on_non_acao_ctor_projects_nome_and_kind_through_accessors() {
8853        // Accessor-fidelity pin: any future `:nome` axis extension
8854        // (namespace-qualified rewrite `pleme-io/<nome>`, per-cluster
8855        // alias overlay, case-normalization pass) that lands on
8856        // [`crate::Caixa::nome`] and any future `:kind` re-projection
8857        // (an overlay pass returning a different `CaixaKind` copy)
8858        // that lands on [`crate::Caixa::kind`] must reach the
8859        // `caixa:` / `kind:` carriers through these projections
8860        // rather than raw field accesses. The neighbour
8861        // [`LayoutError::missing_lib`] ctor (b4d5a49) projects nome
8862        // the same way; this pin locks the `:ci` axis onto the same
8863        // discipline so the whole `LayoutError` family stays coherent
8864        // under any future rewrite.
8865        //
8866        // Deliberately uses a byte-distinctive nome ("ci-on-binario")
8867        // and a non-Acao kind (`Binario`) so a regression that
8868        // hard-codes a fixture literal at the ctor body (rather than
8869        // projecting through the accessors) drops the bytes or the
8870        // kind and trips the assertion.
8871        let mut bin = caixa(CaixaKind::Binario);
8872        bin.nome = "ci-on-binario".into();
8873        let err = LayoutError::ci_on_non_acao(&bin);
8874        let LayoutError::CiOnNonAcao {
8875            caixa: cname,
8876            kind: cknd,
8877        } = err
8878        else {
8879            panic!("ci_on_non_acao ctor must construct the CiOnNonAcao variant, got a foreign arm");
8880        };
8881        assert_eq!(
8882            cname,
8883            bin.nome(),
8884            "ci_on_non_acao `caixa:` carrier must project through Caixa::nome()"
8885        );
8886        assert_eq!(
8887            cknd,
8888            bin.kind(),
8889            "ci_on_non_acao `kind:` carrier must project through Caixa::kind()"
8890        );
8891    }
8892
8893    #[test]
8894    fn ci_on_non_acao_verify_wire_up_routes_through_ctor() {
8895        // Behavioural pin: [`StandardLayout::verify`] must reach the
8896        // `CiOnNonAcao` variant through the newly lifted ctor rather
8897        // than a residual struct-literal block. The end-to-end
8898        // observable — a non-Acao caixa with `:ci` declared — must
8899        // surface a `CiOnNonAcao` whose `caixa:` and `kind:` carriers
8900        // are byte-equal to what the ctor would produce when called
8901        // directly.
8902        let root = PathBuf::from("/tmp/x");
8903        let manifest = root.join("caixa.lisp");
8904        let manifest_only = manifest.clone();
8905        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_only);
8906        let mut bib = caixa(CaixaKind::Biblioteca);
8907        bib.ci = Some(canteiro_types::CiRun {
8908            workspace: "pleme-io".into(),
8909            repo: "caixa".into(),
8910            nodes: vec![],
8911        });
8912
8913        let observed = layout.verify(&bib, &root).unwrap_err();
8914        let synthesized = LayoutError::ci_on_non_acao(&bib);
8915        assert_eq!(
8916            observed, synthesized,
8917            "Caixa::validate_ci_kind_coherence must reach CiOnNonAcao through the \
8918             ci_on_non_acao ctor — a residual struct-literal block would silently \
8919             diverge on any future accessor projection change"
8920        );
8921    }
8922}