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
62impl std::fmt::Debug for StandardLayout {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        f.debug_struct("StandardLayout")
65            .field("custom_exists", &self.path_exists.is_some())
66            .finish()
67    }
68}
69
70impl LayoutInvariants for StandardLayout {
71    fn verify(&self, caixa: &Caixa, root: &Path) -> Result<(), LayoutError> {
72        let manifest = root.join("caixa.lisp");
73        if !self.exists(&manifest) {
74            return Err(LayoutError::MissingManifest(manifest));
75        }
76
77        // Caixa-identity value-shape gates on the two universal axes
78        // (`:nome`, `:versao`) every substrate-side artifact's
79        // `metadata.name` / version derivation flows through. The
80        // [`Caixa::validate_nome`] / [`Caixa::validate_versao`] doc-
81        // comments name the canonical authoring footguns verbatim —
82        // `:nome` "MyApp" / "my_app" / "team.app" / "-app" / "café"
83        // (DNS-1123 violations the K8s apiserver refuses at admission
84        // time on every derived `metadata.name`: `lareira-<nome>`,
85        // programs.yaml entry, CiliumNetworkPolicy / HTTPRoute names,
86        // `LABEL_APLICACAO` value); `:versao` "0.1" / "v0.1.0" / "latest"
87        // / "^0.1" / "0.1.0.0" (SemVer-2 violations Helm / OCI tag /
88        // `feira publish` git tag / lacre `concrete_versao` /
89        // `:upgrade-from :from` peer matching each refuse downstream).
90        // Until this wire-up landed both validators existed as `pub fn`
91        // on [`Caixa`] (with full per-arm test coverage in
92        // `manifest::tests`) but no production code path called them —
93        // `feira build` (the canonical author-time gate) silently
94        // accepted a malformed `:nome` / `:versao` and the failure
95        // surfaced at `helm install` / `kubectl apply` / `feira publish`
96        // time on the *first* downstream consumer to strict-parse the
97        // value, far from the source `caixa.lisp` and without any field
98        // naming the offending Caixa identity axis. The gate runs
99        // *after* [`LayoutError::MissingManifest`] (no caixa to check
100        // when the manifest is missing) and *before* every kind-coherence
101        // gate (each of which carries `caixa.nome` verbatim in its
102        // diagnostic — running them first on a structurally-invalid
103        // identity would surface a "this kind has slot X" diagnostic
104        // against an unrecoverable name). Cross-axis precedence is
105        // `:nome` → `:versao` — the canonical declaration order on
106        // [`Caixa`] and the same author-grep ordering the
107        // [`ManifestError`] family uses. Same per-axis `*Violation
108        // { caixa, issue }` envelope every peer per-axis wrap exposes
109        // ([`LayoutError::CodePathViolation`] b868442,
110        // [`LayoutError::LimitsViolation`] / [`LayoutError::BehaviorViolation`]
111        // / [`LayoutError::UpgradeViolation`] / [`LayoutError::SupervisorViolation`]
112        // / [`LayoutError::AplicacaoViolation`]).
113        caixa
114            .validate_nome()
115            .map_err(|err| LayoutError::nome_violation(caixa, err))?;
116        // `:nome`-side joint-length budget on the canonical
117        // `lareira-<nome>` chart-name shape — the second arm on the
118        // shared `:nome` axis after the bare-DNS-1123 gate above. Runs
119        // through the same [`LayoutError::NomeViolation`] envelope so
120        // every per-axis diagnostic on `:nome` carries one wrap shape,
121        // peer with the [`Caixa::validate_nome`] → `NomeInvalid`
122        // routing already at this site. The chart-name budget is the
123        // second-axis ceiling [`Caixa::validate_nome`] cannot see — a
124        // 56-byte DNS-1123-valid `:nome` passes the bare-`:nome` shape
125        // but produces a 64-byte `lareira-<nome>` chart name the
126        // apiserver / `helm lint` rejects at admission, far from the
127        // source `caixa.lisp` and naming none of the joint-length
128        // overflow's three carriers (DNS-1123 cap, prefix, `:nome`
129        // length). Closing it at this wire-up turns the
130        // [`lareira_chart_name`] doc-comment's explicit M4-admission
131        // deferral (caixa-core/src/render.rs:3198) into a build-time
132        // structural property of every emitted artifact.
133        caixa
134            .validate_nome_chart_name_budget()
135            .map_err(|err| LayoutError::nome_violation(caixa, err))?;
136        caixa
137            .validate_versao()
138            .map_err(|err| LayoutError::versao_violation(caixa, err))?;
139
140        // `:deps` / `:deps-dev` per-entry shape gate. The third Caixa-
141        // level orphan validator on the universal authoring surface (peer
142        // of [`Caixa::validate_nome`] / [`Caixa::validate_versao`] wired
143        // immediately above): [`Caixa::validate_deps`] walks every
144        // [`Dep::validate`] arm — empty / non-DNS-1123 `:nome`, empty /
145        // unparseable `:versao` requirement, malformed `:fonte` repo /
146        // pin / `:caminho`, malformed `:caracteristicas` Cargo-feature
147        // name (de68c0c) — and then closes the per-list set-not-multiset
148        // duplicate-`:nome` invariant on each of `:deps` and `:deps-dev`
149        // (359fba5). Until this wire-up landed `validate_deps` existed as
150        // `pub fn` on [`Caixa`] with full per-arm unit coverage in
151        // `manifest::tests` + `dep::tests` (validate_deps_rejects_*,
152        // 53 dep-axis tests) but no production code path called it —
153        // `feira build` (the canonical author-time gate;
154        // `caixa-feira/src/cmd/build.rs:29` routes through
155        // `StandardLayout::verify`) silently accepted a malformed `:deps`
156        // entry and the failure surfaced at the *first* downstream
157        // consumer to strict-parse it: at lacre-resolve time as a
158        // `semver::Error` not naming the offending dep (`:versao` per-
159        // entry); at `git clone` time as a fetch failure quoting the
160        // shell-escape `repo` (`:fonte :repo`); at the resolver's
161        // `HashMap<:nome>` collapse as a silent "second-wins" overwrite
162        // (within-list `:nome` duplicate); at `cargo metadata` time as a
163        // feature-name rejection on the *target* caixa rather than the
164        // dep entry referencing it (`:caracteristicas`); at `helm
165        // install` / `kubectl apply` time as an apiserver `metadata.name`
166        // rejection on the rendered `lareira-<nome>` chart's per-dep
167        // derivation (DNS-1123-violating `:deps :nome`) — each far from
168        // the source `caixa.lisp`, none naming the offending `:deps` /
169        // `:deps-dev` axis. Runs *after* the Caixa-identity gates (the
170        // diagnostic carries `caixa.nome().to_string()` verbatim, which the
171        // peer [`Caixa::validate_nome`] gate above has just guaranteed is
172        // a valid DNS-1123 label) and *before* every kind-coherence gate
173        // (the dep surface is universal — every kind has `:deps` /
174        // `:deps-dev` — so its shape diagnostic is more fundamental than
175        // the kind-coherence partitions on `:bibliotecas` / `:exe` /
176        // `:servicos` / `:membros` / `:children` / M2 slots that follow).
177        // Same per-axis `*Violation { caixa, issue }` envelope every peer
178        // per-axis wrap exposes ([`LayoutError::NomeViolation`] /
179        // [`LayoutError::VersaoViolation`] (1f74a5f),
180        // [`LayoutError::CodePathViolation`] (b868442),
181        // [`LayoutError::LimitsViolation`] / [`LayoutError::BehaviorViolation`]
182        // / [`LayoutError::UpgradeViolation`] / [`LayoutError::SupervisorViolation`]
183        // / [`LayoutError::AplicacaoViolation`]). Threads [`DepError`]
184        // Display through verbatim — every per-arm reason already names
185        // the offending dep's `:nome` (e.g. `":deps entry "caixa-teia"
186        // :versao "^bad" is not a valid semver requirement: …"`), so the
187        // wrap envelope's `issue` carries a self-locating "which dep,
188        // which axis, why" without re-shaping the per-arm parser-side
189        // reason. With this wire-up the canonical author-time gate
190        // refuses every ill-formed `:deps` / `:deps-dev` value-shape by
191        // construction — closing the second-to-last orphan-validator gap
192        // on the typed Caixa surface (`validate_restart_window` is the
193        // remaining orphan, Supervisor-axis specific and wired into the
194        // Supervisor branch below alongside `view.validate()`).
195        // Compound per-Caixa entry gate on the dep-graph axis: the
196        // layout pipeline's two-dispatch `:deps` / `:deps-dev` cascade
197        // — the per-entry + within-list duplicate-`:nome` gate (the
198        // [`crate::Dep::validate`] + [`crate::render::insert_first_seen`]
199        // cascade `Caixa::validate_deps` opened on, 359fba5) and the
200        // cross-slot self-edge gate
201        // ([`crate::dep::validate_no_self_dep`], ad4abf1) — folded
202        // onto the [`crate::Caixa::validate_deps`] substrate primitive.
203        // The two arms run in the same canonical order at the primitive
204        // (per-entry + cross-entry duplicate → cross-slot self-edge) so
205        // the fold is byte-for-byte equivalent to the pre-fold
206        // two-block cascade this call site formerly carried, pinned by
207        // the paired
208        // `validate_deps_folds_{per_entry,self_edge}_arm_matches_gate`
209        // equivalence pins and the
210        // `validate_deps_per_entry_arm_fires_before_self_edge_arm`
211        // ordering pin in the [`crate::Caixa::validate_deps`] pin
212        // family (`manifest.rs`).
213        //
214        // Same lift discipline the peer per-slot compound gates
215        // ([`crate::AplicacaoSpec::validate_contratos`] and its
216        // `:membros` / `:entrada` / `:placement` / `:politicas` peers,
217        // [`crate::MeshPolicy::validate`],
218        // [`crate::SupervisorSpec::validate_children`],
219        // [`crate::Caixa::validate_upgrade_from`] d6801df) each carry —
220        // one named substrate-primitive gate per typed slot folds every
221        // structural + cross-slot axis on that slot onto one call, so
222        // every future consumer that wants to re-check the dep-graph
223        // after a per-entry patch (the deferred
224        // `caixa.pleme.io/v1alpha1/Caixa` CR materializer's admission
225        // webhook, a future `feira validate --deps` per-caixa admission
226        // verb, a per-`:deps` overlay resolver) reaches the two-arm
227        // compound gate through one dispatch rather than re-inlining
228        // the two-dispatch cascade in lockstep with this wire-up.
229        caixa
230            .validate_deps()
231            .map_err(|err| LayoutError::deps_violation(caixa, err))?;
232
233        // `:etiquetas` per-entry empty + cross-entry duplicate gate. The
234        // fourth universal-axis Caixa-level value-shape gate (peer of
235        // [`Caixa::validate_nome`] / [`Caixa::validate_versao`] /
236        // [`Caixa::validate_deps`] wired immediately above and
237        // [`Caixa::validate_code_paths`] wired below the kind-coherence
238        // gates) on the typed Caixa surface. `:etiquetas` is the
239        // registry-search-tag axis every kind carries (universal
240        // `Vec<String>` slot on [`Caixa`]) and lands verbatim as the
241        // Helm chart `Chart.yaml` `keywords:` array on every Servico
242        // (`caixa-helm/src/lib.rs:236` folds it through a
243        // [`std::collections::BTreeSet`]). Until this wire-up landed
244        // `:etiquetas` had no shape gate at any layer — an empty entry
245        // (`(:etiquetas (""))` — the canonical paste-from-blank-doc
246        // footgun) silently rendered as `keywords: [""]` in `Chart.yaml`,
247        // and duplicate entries (`(:etiquetas ("demo" "demo"))` — the
248        // copy-paste-the-wrong-tag footgun) were silently dedup'd by
249        // the renderer's `BTreeSet` collect — a "second wins / one
250        // silently disappears" shape divergent from every peer typed-
251        // graph set gate (`:membros :caixa`, `:placement :clusters`,
252        // `:entrada :paths`, `:contratos`, `:deps :nome`,
253        // `:upgrade-from :from`, the per-instruction-class singularity
254        // gates on `:upgrade-from :instructions`). Runs *after* the
255        // peer universal `:nome` / `:versao` / `:deps` gates (declaration
256        // order on [`Caixa`] is `:nome` → `:versao` → `:edicao` →
257        // `:descricao` → `:repositorio` → `:licenca` → `:autores` →
258        // `:etiquetas` → `:deps` → `:deps-dev`, but the gate order
259        // follows the same identity-axis-first cascade the peer gates
260        // establish: `:nome` → `:versao` are the load-bearing identity
261        // axes that flow into every diagnostic's caixa prefix, and
262        // `:deps` is the universal dep surface that dominates every
263        // kind-coherence gate; `:etiquetas` runs after this trio so the
264        // diagnostic carries an already-validated `:nome` and the
265        // peer universal axes' narrower diagnostics surface first when
266        // multiple axes are malformed) and *before* the kind-coherence
267        // gates ([`Self::MeshSlotsOnNonAplicacao`] /
268        // [`Self::SupervisorSlotsOnNonSupervisor`] /
269        // [`Self::ServicoSlotsOnNonServico`] / [`Self::ForeignCodeSlot`])
270        // — `:etiquetas` is universal so its shape diagnostic is more
271        // fundamental than the kind-coherence partitions on kind-
272        // exclusive slot sets.
273        //
274        // Same per-axis `*Violation { caixa, issue }` envelope every peer
275        // per-axis wrap exposes ([`Self::NomeViolation`] /
276        // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
277        // aa77d0f, [`Self::CodePathViolation`] b868442,
278        // [`Self::RestartWindowViolation`] 10e321a). Threads
279        // [`ManifestError::EtiquetaEmpty`] / [`ManifestError::EtiquetaDuplicate`]
280        // Display through verbatim — each per-arm reason already names
281        // the offending tag (for the duplicate arm) or the structural
282        // "empty entry" defect (for the empty arm), so the wrap
283        // envelope's `issue` carries a self-locating "which axis, which
284        // entry, why" without re-shaping the per-arm reason.
285        caixa
286            .validate_etiquetas()
287            .map_err(|err| LayoutError::etiquetas_violation(caixa, err))?;
288
289        // `:autores` per-entry empty + cross-entry duplicate gate. The
290        // fifth universal-axis Caixa-level value-shape gate (peer of
291        // [`Caixa::validate_nome`] / [`Caixa::validate_versao`] /
292        // [`Caixa::validate_deps`] / [`Caixa::validate_etiquetas`] wired
293        // immediately above and [`Caixa::validate_code_paths`] wired
294        // below the kind-coherence gates) on the typed Caixa surface.
295        // `:autores` is the maintainer-axis every kind carries
296        // (universal `Vec<String>` slot on [`Caixa`]) and lands verbatim
297        // as the Helm chart `Chart.yaml` `maintainers:` array on every
298        // Servico (`caixa-helm/src/lib.rs:251` maps each entry to a
299        // `Maintainer { name, email: None }` without dedup). Until this
300        // wire-up landed `:autores` had no shape gate at any layer — an
301        // empty entry (`(:autores (""))` — the canonical paste-from-
302        // blank-doc footgun) silently rendered as
303        // `maintainers: [{name: "", email: null}]` in `Chart.yaml`, and
304        // duplicate entries (`(:autores ("pleme-io" "pleme-io"))` —
305        // the copy-paste-the-wrong-author footgun) stacked verbatim in
306        // the chart. Unlike the peer `:etiquetas` axis (where the
307        // renderer's `BTreeSet` collect silently dedups the `keywords:`
308        // array at chart render — a "second wins / one silently
309        // disappears" shape), `maintainers:` has *no* renderer-side
310        // dedup, so duplicate `:autores` entries render as two identical
311        // maintainer records by construction — a strictly worse footgun
312        // than the peer `:etiquetas` shape. Runs *after* the peer
313        // universal `:nome` / `:versao` / `:deps` / `:etiquetas` gates
314        // (the gate order follows the canonical identity-axis-first
315        // cascade the peer gates establish; `:autores` and `:etiquetas`
316        // are the two Vec-shaped universal metadata axes — they sit
317        // adjacent in the cascade after the load-bearing identity +
318        // dep trio) and *before* the kind-coherence gates
319        // ([`Self::MeshSlotsOnNonAplicacao`] /
320        // [`Self::SupervisorSlotsOnNonSupervisor`] /
321        // [`Self::ServicoSlotsOnNonServico`] / [`Self::ForeignCodeSlot`])
322        // — `:autores` is universal so its shape diagnostic is more
323        // fundamental than the kind-coherence partitions on kind-
324        // exclusive slot sets.
325        //
326        // Same per-axis `*Violation { caixa, issue }` envelope every peer
327        // per-axis wrap exposes ([`Self::NomeViolation`] /
328        // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
329        // aa77d0f, [`Self::EtiquetasViolation`] 360a499,
330        // [`Self::CodePathViolation`] b868442,
331        // [`Self::RestartWindowViolation`] 10e321a). Threads
332        // [`ManifestError::AutorEmpty`] / [`ManifestError::AutorDuplicate`]
333        // Display through verbatim — each per-arm reason already names
334        // the offending author (for the duplicate arm) or the structural
335        // "empty entry" defect (for the empty arm), so the wrap
336        // envelope's `issue` carries a self-locating "which axis, which
337        // entry, why" without re-shaping the per-arm reason.
338        caixa
339            .validate_autores()
340            .map_err(|err| LayoutError::autores_violation(caixa, err))?;
341
342        // `:repositorio` git-repo-URL shape gate. The sixth
343        // universal-axis Caixa-level value-shape gate (peer of
344        // [`Caixa::validate_nome`] / [`Caixa::validate_versao`] /
345        // [`Caixa::validate_deps`] / [`Caixa::validate_etiquetas`] /
346        // [`Caixa::validate_autores`] wired immediately above and
347        // [`Caixa::validate_code_paths`] wired below the kind-coherence
348        // gates) on the typed Caixa surface. `:repositorio` is the
349        // universal git-shaped homepage axis every kind carries
350        // (universal `Option<String>` slot on [`Caixa`]) and routes
351        // through two load-bearing substrate consumers:
352        // [`caixa-helm`] folds it verbatim into the rendered
353        // `lareira-<nome>` Helm chart's `Chart.yaml` `home:` field
354        // (`build_chart_yaml` at `caixa-helm/src/lib.rs:268`) and into
355        // the chart `README.md` `repo = …` interpolation
356        // (`caixa-helm/src/lib.rs:359`); [`caixa-flux`] folds it
357        // verbatim into the standalone `ClusterBundleOpts::for_caixa`
358        // `git_url:` field (`caixa-flux/src/lib.rs:293`), which
359        // becomes the FluxCD `GitRepository.spec.url` the cluster's
360        // source-controller polls — the load-bearing deploy-time axis.
361        // Both consumers use `Option::unwrap_or_else(|| <fallback>)`
362        // to substitute a placeholder when the slot is absent (`None`
363        // → the fallback fires); a `Some("")` *skips the fallback*
364        // and silently passes the empty string through to
365        // `Chart.yaml home: ""` / `GitRepository url: ""`. Until this
366        // wire-up landed `:repositorio` had no shape gate at any
367        // layer — empty (`(:repositorio "")` — the canonical
368        // paste-from-blank-doc footgun) and malformed (whitespace,
369        // control char / CRLF, leading `-` CLI-arg-injection,
370        // missing `:` separator) values silently landed in the
371        // rendered artifacts and broke at `helm template` / FluxCD
372        // reconcile time far from the source `caixa.lisp`.
373        //
374        // Runs *after* the peer universal `:nome` / `:versao` /
375        // `:deps` / `:etiquetas` / `:autores` gates (the gate order
376        // follows the canonical identity-axis-first cascade the peer
377        // gates establish; `:repositorio` is the universal git-URL
378        // axis — it sits adjacent to `:autores` in the cascade after
379        // the load-bearing identity + dep trio + the two Vec-shaped
380        // universal metadata axes) and *before* the kind-coherence
381        // gates ([`Self::MeshSlotsOnNonAplicacao`] /
382        // [`Self::SupervisorSlotsOnNonSupervisor`] /
383        // [`Self::ServicoSlotsOnNonServico`] / [`Self::ForeignCodeSlot`])
384        // — `:repositorio` is universal so its shape diagnostic is
385        // more fundamental than the kind-coherence partitions on
386        // kind-exclusive slot sets.
387        //
388        // Same per-axis `*Violation { caixa, issue }` envelope every
389        // peer per-axis wrap exposes ([`Self::NomeViolation`] /
390        // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
391        // aa77d0f, [`Self::EtiquetasViolation`] 360a499,
392        // [`Self::AutoresViolation`] 86c769b, [`Self::CodePathViolation`]
393        // b868442, [`Self::RestartWindowViolation`] 10e321a). Threads
394        // [`ManifestError::RepositorioEmpty`] /
395        // [`ManifestError::RepositorioInvalid`] Display through
396        // verbatim — each per-arm reason already names the offending
397        // `:repositorio` value (for the invalid arm) or the
398        // structural "empty entry" defect (for the empty arm), so
399        // the wrap envelope's `issue` carries a self-locating "which
400        // axis, which value, why" without re-shaping the per-arm
401        // reason. With this gate the two `git URL`-shaped surfaces on
402        // the typed Caixa (`:repositorio` here, `:deps :fonte :repo`
403        // peer routed through the same shared
404        // [`crate::render::is_git_repo_url`] predicate via
405        // [`crate::DepSource::validate`]) are now structurally
406        // equivalent — every value past validate is
407        // guaranteed-acceptable by the shared predicate's constraint
408        // union, by construction.
409        caixa
410            .validate_repositorio()
411            .map_err(|err| LayoutError::repositorio_violation(caixa, err))?;
412
413        // `:descricao` non-empty shape gate. The seventh universal-
414        // axis Caixa-level value-shape gate (peer of
415        // [`Caixa::validate_nome`] / [`Caixa::validate_versao`] /
416        // [`Caixa::validate_deps`] / [`Caixa::validate_etiquetas`] /
417        // [`Caixa::validate_autores`] / [`Caixa::validate_repositorio`]
418        // wired immediately above and [`Caixa::validate_code_paths`]
419        // wired below the kind-coherence gates) on the typed Caixa
420        // surface. `:descricao` is the universal free-form-prose
421        // summary axis every kind carries (universal `Option<String>`
422        // slot on [`Caixa`]) and routes through two load-bearing
423        // [`caixa-helm`] consumers: `build_chart_yaml` folds it
424        // verbatim into the rendered `lareira-<nome>` Helm chart's
425        // `Chart.yaml` `description:` field
426        // (`caixa-helm/src/lib.rs:232-235`), and `build_readme` folds
427        // it verbatim into the chart `README.md` header
428        // (`caixa-helm/src/lib.rs:333-336`). Both consumers use
429        // `Option::unwrap_or_else(|| <fallback>)` to substitute a
430        // `caixa.nome`-derived placeholder when the slot is absent
431        // (`None` → the fallback fires); a `Some("")` *skips the
432        // fallback* and silently passes the empty string through to
433        // `Chart.yaml description: ""` / a blank `README.md` header
434        // — exact same footgun shape as the peer `:repositorio`
435        // surface above. Until this wire-up landed `:descricao` had
436        // no shape gate at any layer — the empty
437        // (`(:descricao "")` — the canonical paste-from-blank-doc
438        // footgun) silently landed in the rendered artifacts and
439        // broke at `helm lint` time (`WARNING [chart.metadata.description]:
440        // description is required` on `apiVersion: v2` charts) far
441        // from the source `caixa.lisp`.
442        //
443        // Runs *after* the peer universal `:nome` / `:versao` /
444        // `:deps` / `:etiquetas` / `:autores` / `:repositorio` gates
445        // (the gate order follows the canonical identity-axis-first
446        // cascade the peer gates establish; `:descricao` is the
447        // universal free-form-prose axis — it sits adjacent to
448        // `:repositorio` in the cascade after the load-bearing
449        // identity + dep trio + the two Vec-shaped universal
450        // metadata axes + the universal git-URL axis) and *before*
451        // the kind-coherence gates ([`Self::MeshSlotsOnNonAplicacao`]
452        // / [`Self::SupervisorSlotsOnNonSupervisor`] /
453        // [`Self::ServicoSlotsOnNonServico`] /
454        // [`Self::ForeignCodeSlot`]) — `:descricao` is universal so
455        // its shape diagnostic is more fundamental than the kind-
456        // coherence partitions on kind-exclusive slot sets.
457        //
458        // Same per-axis `*Violation { caixa, issue }` envelope every
459        // peer per-axis wrap exposes ([`Self::NomeViolation`] /
460        // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
461        // aa77d0f, [`Self::EtiquetasViolation`] 360a499,
462        // [`Self::AutoresViolation`] 86c769b,
463        // [`Self::RepositorioViolation`] 577b0a9,
464        // [`Self::CodePathViolation`] b868442,
465        // [`Self::RestartWindowViolation`] 10e321a). Threads
466        // [`ManifestError::DescricaoEmpty`] Display through verbatim
467        // — the per-arm reason already names the offending
468        // `:descricao` slot + cites the renderer-side footgun, so
469        // the wrap envelope's `issue` carries a self-locating
470        // "which axis, why" without re-shaping the per-arm reason.
471        caixa
472            .validate_descricao()
473            .map_err(|err| LayoutError::descricao_violation(caixa, err))?;
474
475        // `:licenca` non-empty shape gate. The eighth universal-axis
476        // Caixa-level value-shape gate (peer of [`Caixa::validate_nome`]
477        // / [`Caixa::validate_versao`] / [`Caixa::validate_deps`] /
478        // [`Caixa::validate_etiquetas`] / [`Caixa::validate_autores`] /
479        // [`Caixa::validate_repositorio`] / [`Caixa::validate_descricao`]
480        // wired immediately above and [`Caixa::validate_code_paths`]
481        // wired below the kind-coherence gates) on the typed Caixa
482        // surface. `:licenca` is the universal SPDX-shaped license-
483        // expression axis every kind carries (universal `Option<String>`
484        // slot on [`Caixa`]) and routes through one load-bearing
485        // [`caixa-helm`] consumer: `build_readme` folds it verbatim into
486        // the rendered `lareira-<nome>` Helm chart's `README.md` `##
487        // License` section (`caixa-helm/src/lib.rs:361`) via
488        // `caixa.licenca.clone().unwrap_or_else(|| "MIT".into())`. The
489        // consumer's fallback only fires when the slot is absent (`None`
490        // → the `MIT` fallback fires); a `Some("")` *skips the
491        // fallback* and silently passes the empty string through to a
492        // chart `README.md` whose `License` section renders as a bare
493        // trailing period — exact same footgun shape as the peer
494        // `:repositorio` (577b0a9) and `:descricao` (4e6db38) surfaces
495        // above. Until this wire-up landed `:licenca` had no shape
496        // gate at any layer — the empty (`(:licenca "")` — the
497        // canonical paste-from-blank-doc footgun) silently landed in
498        // the rendered chart `README.md` far from the source
499        // `caixa.lisp`.
500        //
501        // Runs *after* the peer universal `:nome` / `:versao` /
502        // `:deps` / `:etiquetas` / `:autores` / `:repositorio` /
503        // `:descricao` gates (the gate order follows the canonical
504        // identity-axis-first cascade the peer gates establish;
505        // `:licenca` sits adjacent to `:descricao` in the cascade
506        // after the load-bearing identity + dep trio + the two
507        // Vec-shaped universal metadata axes + the universal
508        // git-URL + free-form-prose axes) and *before* the kind-
509        // coherence gates ([`Self::MeshSlotsOnNonAplicacao`] /
510        // [`Self::SupervisorSlotsOnNonSupervisor`] /
511        // [`Self::ServicoSlotsOnNonServico`] /
512        // [`Self::ForeignCodeSlot`]) — `:licenca` is universal so
513        // its shape diagnostic is more fundamental than the kind-
514        // coherence partitions on kind-exclusive slot sets.
515        //
516        // Same per-axis `*Violation { caixa, issue }` envelope every
517        // peer per-axis wrap exposes ([`Self::NomeViolation`] /
518        // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
519        // aa77d0f, [`Self::EtiquetasViolation`] 360a499,
520        // [`Self::AutoresViolation`] 86c769b,
521        // [`Self::RepositorioViolation`] 577b0a9,
522        // [`Self::DescricaoViolation`] 4e6db38,
523        // [`Self::CodePathViolation`] b868442,
524        // [`Self::RestartWindowViolation`] 10e321a). Threads
525        // [`ManifestError::LicencaEmpty`] Display through verbatim
526        // — the per-arm reason already names the offending
527        // `:licenca` slot + cites the renderer-side footgun, so
528        // the wrap envelope's `issue` carries a self-locating
529        // "which axis, why" without re-shaping the per-arm reason.
530        caixa
531            .validate_licenca()
532            .map_err(|err| LayoutError::licenca_violation(caixa, err))?;
533
534        // `:edicao` non-empty shape gate. The ninth (and last
535        // un-gated) universal-axis Caixa-level value-shape gate
536        // (peer of [`Caixa::validate_nome`] / [`Caixa::validate_versao`]
537        // / [`Caixa::validate_deps`] / [`Caixa::validate_etiquetas`] /
538        // [`Caixa::validate_autores`] / [`Caixa::validate_repositorio`]
539        // / [`Caixa::validate_descricao`] / [`Caixa::validate_licenca`]
540        // wired immediately above and [`Caixa::validate_code_paths`]
541        // wired below the kind-coherence gates) on the typed Caixa
542        // surface. `:edicao` is the universal language-edition axis
543        // every kind carries (universal `Option<String>` slot on
544        // [`Caixa`]) that selects the tatara-lisp macro surface +
545        // compatibility flags the substrate applies when building
546        // the caixa. The canonical [`Caixa::template`] scaffold every
547        // `feira init` emits carries `:edicao "2026"` verbatim
548        // (`caixa-core/src/manifest.rs:1193`) and every renderer-side
549        // fixture carries `edicao: Some("2026".into())` by
550        // construction (`caixa-helm/src/lib.rs:375`,
551        // `caixa-flux/src/lib.rs:445`, `caixa-mesh/src/lib.rs:629`,
552        // `caixa-core/src/render.rs:2510`). Until this wire-up landed
553        // `:edicao` had no shape gate at any layer — the empty
554        // (`(:edicao "")` — the canonical paste-from-blank-doc
555        // footgun) silently landed as a bare `(:edicao "")` line
556        // in the rendered caixa.lisp and a future renderer-side
557        // consumer that folds the value through
558        // `Option::unwrap_or_else` would skip its fallback (which
559        // only fires on `None`) and pass the empty edition through
560        // to the substrate's build-time edition selector far from
561        // the source `caixa.lisp` — exact same
562        // `Some("")`-skips-`unwrap_or_else` footgun shape as the
563        // peer `:repositorio` (577b0a9), `:descricao` (4e6db38),
564        // and `:licenca` (3d1e535) surfaces above.
565        //
566        // Runs *after* the peer universal `:nome` / `:versao` /
567        // `:deps` / `:etiquetas` / `:autores` / `:repositorio` /
568        // `:descricao` / `:licenca` gates (the gate order follows
569        // the canonical identity-axis-first cascade the peer gates
570        // establish; `:edicao` sits at the tail of the cascade
571        // after the load-bearing identity + dep trio + the two
572        // Vec-shaped universal metadata axes + the three universal
573        // `Option<String>` chart-metadata axes) and *before* the
574        // kind-coherence gates ([`Self::MeshSlotsOnNonAplicacao`] /
575        // [`Self::SupervisorSlotsOnNonSupervisor`] /
576        // [`Self::ServicoSlotsOnNonServico`] /
577        // [`Self::ForeignCodeSlot`]) — `:edicao` is universal so
578        // its shape diagnostic is more fundamental than the kind-
579        // coherence partitions on kind-exclusive slot sets.
580        //
581        // Same per-axis `*Violation { caixa, issue }` envelope every
582        // peer per-axis wrap exposes ([`Self::NomeViolation`] /
583        // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
584        // aa77d0f, [`Self::EtiquetasViolation`] 360a499,
585        // [`Self::AutoresViolation`] 86c769b,
586        // [`Self::RepositorioViolation`] 577b0a9,
587        // [`Self::DescricaoViolation`] 4e6db38,
588        // [`Self::LicencaViolation`] 3d1e535,
589        // [`Self::CodePathViolation`] b868442,
590        // [`Self::RestartWindowViolation`] 10e321a). Threads
591        // [`ManifestError::EdicaoEmpty`] Display through verbatim
592        // — the per-arm reason already names the offending
593        // `:edicao` slot + cites the renderer-side footgun, so
594        // the wrap envelope's `issue` carries a self-locating
595        // "which axis, why" without re-shaping the per-arm reason.
596        // With this gate every universal-axis `Option<String>`
597        // surface on the typed Caixa (`:repositorio` 577b0a9,
598        // `:descricao` 4e6db38, `:licenca` 3d1e535, `:edicao` here)
599        // now carries the same structural empty-arm gate by
600        // construction.
601        caixa
602            .validate_edicao()
603            .map_err(|err| LayoutError::edicao_violation(caixa, err))?;
604
605        // Supervisors, Aplicacaos, and Acaos don't run code; reject
606        // bibliotecas/exe/servicos declarations BEFORE checking those
607        // paths exist (which would otherwise produce a less-helpful
608        // "missing entry" error first).
609        let has_code = !caixa.bibliotecas().is_empty()
610            || !caixa.exe().is_empty()
611            || !caixa.servicos().is_empty();
612        if caixa.kind().is_supervisor() && has_code {
613            return Err(LayoutError::SupervisorOwnsCode(caixa.nome().to_string()));
614        }
615        if caixa.kind().is_aplicacao() && has_code {
616            return Err(LayoutError::AplicacaoOwnsCode(caixa.nome().to_string()));
617        }
618        // An Acao's sole payload is its `:ci` slot (CANTEIRO §7.1-C) —
619        // like Supervisor/Aplicacao it runs no code of its own, so a
620        // declared :bibliotecas/:exe/:servicos is the same "silently
621        // ignored" footgun the two gates above already close for the
622        // other two no-code kinds.
623        if caixa.kind().is_acao() && has_code {
624            return Err(LayoutError::AcaoOwnsCode(caixa.nome().to_string()));
625        }
626
627        // Kind ↔ slot coherence: the M3 mesh slots (:membros,
628        // :contratos, :politicas, :placement, :entrada) compose the
629        // typed graph of a :kind Aplicacao (MESH-COMPOSITION §III.1).
630        // `Caixa::aplicacao_view` only folds them into a validatable
631        // AplicacaoSpec when the kind is Aplicacao, and the
632        // caixa-mesh/-flux/-helm renderers only emit them for an
633        // Aplicacao — so on any *other* kind a declared mesh slot is the
634        // manifest field's documented "ignored otherwise": it silently
635        // passes verify and then vanishes (never validated, never
636        // rendered), far from the source caixa.lisp. Reject it here —
637        // before the path-existence loops — mirroring the
638        // SupervisorOwnsCode / AplicacaoOwnsCode kind-coherence gates
639        // above: a slot foreign to the kind is a build error, not a
640        // silent drop. `declared_mesh_slots` is the single typed source
641        // of the mesh-slot set + its canonical diagnostic order.
642        if !caixa.kind().is_aplicacao() {
643            let mesh_slots = caixa.declared_mesh_slots();
644            if !mesh_slots.is_empty() {
645                return Err(LayoutError::mesh_slots_on_non_aplicacao(caixa, mesh_slots));
646            }
647        }
648
649        // Kind ↔ slot coherence (mirror of the mesh-slot gate above on
650        // the supervisor-tree slot set): the supervisor slots
651        // (:estrategia, :max-restarts, :restart-window, :children)
652        // compose the typed OTP supervisor of a :kind Supervisor
653        // (INSPIRATIONS §II.2). `Caixa::supervisor_view` only folds them
654        // into a validatable SupervisorSpec when the kind is Supervisor,
655        // and the wasm-operator's hierarchical reconciler only consumes
656        // them for one — so on any *other* kind a declared supervisor
657        // slot is the manifest field's documented "ignored otherwise":
658        // it silently passes verify and then vanishes (never validated,
659        // never reconciled), far from the source caixa.lisp. Reject it
660        // here — beside the mesh-slot gate, before the path-existence
661        // loops — naming the offending kind + slot(s). `declared_
662        // supervisor_slots` is the single typed source of the
663        // supervisor-slot set + its canonical diagnostic order.
664        if !caixa.kind().is_supervisor() {
665            let supervisor_slots = caixa.declared_supervisor_slots();
666            if !supervisor_slots.is_empty() {
667                return Err(LayoutError::supervisor_slots_on_non_supervisor(
668                    caixa,
669                    supervisor_slots,
670                ));
671            }
672        }
673
674        // Kind ↔ slot coherence (mirror of the mesh-slot + supervisor-slot
675        // gates above on the M2 Servico-runtime slot set): the M2 slots
676        // (:limits, :behavior, :upgrade-from) configure the runtime of a
677        // long-running wasm component, i.e. a :kind Servico — :limits is
678        // Lunatic per-process sandboxing (INSPIRATIONS §III.1), :behavior
679        // the OTP gen_server callback set (§II.3), :upgrade-from the OTP
680        // appup hot-reload table (§II.4). The caixa-helm / caixa-flux
681        // renderers gate on `require_kind(_, Servico)` and only emit these
682        // slots for a Servico — so on any *other* kind a declared M2 slot
683        // is the manifest field's documented "ignored otherwise": its
684        // well-formedness is checked by the M2 invariant blocks below, but
685        // the value is never rendered into a chart / programs.yaml entry —
686        // it silently passes verify and then vanishes, far from the source
687        // caixa.lisp. Reject it here — beside the mesh- and supervisor-slot
688        // gates, before the M2 validate blocks (which would otherwise spend
689        // their diagnostics on a value the kind can never render) — naming
690        // the offending kind + slot(s). `declared_servico_slots` is the
691        // single typed source of the M2-slot set + its canonical
692        // diagnostic order.
693        if !caixa.kind().is_servico() {
694            let servico_slots = caixa.declared_servico_slots();
695            if !servico_slots.is_empty() {
696                return Err(LayoutError::servico_slots_on_non_servico(
697                    caixa,
698                    servico_slots,
699                ));
700            }
701        }
702
703        // Kind ↔ slot coherence (mirror of the three gates above on the
704        // Acao `:ci` slot, CANTEIRO §7.1-C): `:ci` carries a typed CI
705        // run — a canteiro_types::CiRun — that only the caixa-actions
706        // renderer decomposes + validates, and only for a :kind Acao.
707        // On any *other* kind a declared `:ci` is the manifest field's
708        // documented "ignored otherwise": it silently passes verify and
709        // then vanishes (never decomposed, never rendered), far from the
710        // source caixa.lisp. Reject it here — beside the mesh-,
711        // supervisor-, and servico-slot gates — naming the offending
712        // kind. Unlike its three siblings this axis is a single
713        // `Option` field, not a Vec-of-named-slots, so the gate reads
714        // `caixa.ci().is_some()` directly rather than reaching for a
715        // `declared_*_slots()` helper.
716        if caixa.ci().is_some() && !caixa.kind().is_acao() {
717            return Err(LayoutError::CiOnNonAcao {
718                caixa: caixa.nome().to_string(),
719                kind: caixa.kind(),
720            });
721        }
722
723        // Kind ↔ slot coherence on the fourth and final axis — the
724        // code-surface slot set (the trio M2/Supervisor/Aplicacao gates
725        // above close on the M2 runtime, supervisor-tree, and M3 mesh
726        // axes; this gate closes the symmetric "kind owns this code
727        // shape" relation on `:exe` + `:servicos`). `:exe` is the nix-
728        // built executable surface owned only by Binario; `:servicos`
729        // is the wasm-component daemon surface owned only by Servico.
730        // The caixa-helm / caixa-flux / caixa-flake renderers gate on
731        // `require_kind(_, <owning-kind>)` and only emit the slot for
732        // its owning kind — so on any *other* code-running kind a
733        // declared `:exe` / `:servicos` is the manifest field's
734        // documented "ignored otherwise" (see the field docs on
735        // `Caixa::exe` + `Caixa::servicos`): the path is validated by
736        // the per-kind path-existence loops below, but the value is
737        // never rendered into a build target or programs.yaml entry —
738        // it silently passes `feira build` and then vanishes, far from
739        // the source caixa.lisp.
740        //
741        // Reject it here — beside the M2/Supervisor/Aplicacao slot
742        // gates, after the `SupervisorOwnsCode` / `AplicacaoOwnsCode`
743        // OwnCode gates which dominate on those two no-code kinds (a
744        // Supervisor / Aplicacao with any of `:bibliotecas` / `:exe` /
745        // `:servicos` surfaces the OwnCode diagnostic first), and
746        // before the path-existence loops which would otherwise spend
747        // a less-helpful `MissingEntry` diagnostic on the foreign
748        // slot's path. `declared_foreign_code_slots` is the single
749        // typed source of the foreign-code-slot set + its canonical
750        // diagnostic order (`:exe` → `:servicos`).
751        //
752        // Mirrors the 9d37f98 / 510c00a / 760a430 kind ↔ slot
753        // coherence trio's "declared-but-inert" footgun closure on the
754        // M2 / supervisor-tree / M3 axes, now extended onto the code-
755        // surface axis — every code-running kind's exclusive code
756        // surface is structurally fenced from every other code-running
757        // kind. `:bibliotecas` is deliberately excluded from the foreign
758        // set on Binario / Servico (a `lib/` helper bundled into the
759        // nix flake's build or the wasm-component's source tree is a
760        // legitimate cross-kind authoring shape); on Biblioteca it is
761        // the native slot, and on Supervisor / Aplicacao the OwnCode
762        // gates above already close it.
763        let foreign_code_slots = caixa.declared_foreign_code_slots();
764        if !foreign_code_slots.is_empty() {
765            return Err(LayoutError::foreign_code_slot(caixa, foreign_code_slots));
766        }
767
768        // Per-entry path-shape gate on the three Caixa-level code-surface
769        // path lists (`:bibliotecas`, `:exe`, `:servicos`): each entry must
770        // be non-empty, relative, and free of `..` components — the same
771        // [`crate::render::is_sandboxed_relative_path`] discipline the
772        // peer `:behavior :on-*` (b0c8389) and
773        // `:upgrade-from :state-change :script` (26da2c7) axes already
774        // route through. Runs *after* the kind-coherence gates above (so
775        // a `:exe` on a Servico surfaces ForeignCodeSlot rather than a
776        // per-entry shape diagnostic, and a Supervisor/Aplicacao with any
777        // code surface surfaces OwnCode first) and *before* the existence
778        // loops below (so an empty / absolute / parent-escaping entry
779        // surfaces its self-locating per-slot diagnostic rather than a
780        // downstream `MissingEntry` / `ExeOutsideDir` /
781        // `ServicoOutsideDir` against the resolved sandbox-escape path).
782        caixa
783            .validate_code_paths()
784            .map_err(|err| LayoutError::code_path_violation(caixa, err))?;
785
786        if caixa.kind().requires_lib() && caixa.bibliotecas().is_empty() {
787            let expected = root
788                .join(crate::render::LAYOUT_DIR_LIB)
789                .join(format!("{}.lisp", caixa.nome()));
790            if !self.exists(&expected) {
791                return Err(LayoutError::MissingLib {
792                    caixa: caixa.nome().to_string(),
793                    expected,
794                });
795            }
796        }
797
798        if caixa.kind().requires_exe() && caixa.exe().is_empty() {
799            return Err(LayoutError::BinarioWithoutExe(caixa.nome().to_string()));
800        }
801
802        if caixa.kind().requires_servicos() && caixa.servicos().is_empty() {
803            return Err(LayoutError::ServicoWithoutServicos(
804                caixa.nome().to_string(),
805            ));
806        }
807
808        // Required-slot gate on the fifth [`CaixaKind`] arm — mirror of
809        // the `BinarioWithoutExe` / `ServicoWithoutServicos` pair above.
810        // An `Acao`'s sole payload is its `:ci` slot; an `Acao` caixa
811        // that doesn't declare one has no CI run to decompose or
812        // validate, so `feira build` refuses it here rather than
813        // letting a downstream `caixa-actions::validate` call fail with
814        // a less-helpful "no :ci" surprise far from the source
815        // caixa.lisp.
816        if caixa.kind().requires_ci() && caixa.ci().is_none() {
817            return Err(LayoutError::MissingCi(caixa.nome().to_string()));
818        }
819
820        for p in caixa.bibliotecas() {
821            let full = root.join(p);
822            if !self.exists(&full) {
823                return Err(LayoutError::MissingEntry {
824                    kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
825                    path: full,
826                });
827            }
828        }
829
830        let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
831        for p in caixa.exe() {
832            let full = root.join(p);
833            if !self.exists(&full) {
834                return Err(LayoutError::MissingEntry {
835                    kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
836                    path: full,
837                });
838            }
839            if !full.starts_with(&exe_dir) {
840                return Err(LayoutError::ExeOutsideDir(full));
841            }
842        }
843
844        let servicos_dir = root.join(crate::render::LAYOUT_DIR_SERVICOS);
845        for p in caixa.servicos() {
846            let full = root.join(p);
847            if !self.exists(&full) {
848                return Err(LayoutError::MissingEntry {
849                    kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
850                    path: full,
851                });
852            }
853            if !full.starts_with(&servicos_dir) {
854                return Err(LayoutError::ServicoOutsideDir(full));
855            }
856        }
857
858        // ── M2 typed-substrate invariants ────────────────────────────────
859
860        // Compound per-Caixa entry gate on the M2 `:limits` slot: the
861        // layout pipeline's `if let Some(l) = caixa.limits() { l.validate() }`
862        // `Option::None → Ok(()) | Some(_) → dispatch` unwrap-and-
863        // dispatch pattern — the four-axis cascade on the present-slot
864        // arm ([`crate::LimitsSpec::validate`]'s `:memory` wasm32
865        // zero-floor / below-page / above-cap / non-page-multiple;
866        // `:fuel` zero-floor / cap; `:wall-clock` zero-floor / cap;
867        // `:cpu` zero-floor / cap) folded onto the
868        // [`crate::Caixa::validate_limits`] substrate primitive. The
869        // absent-slot arm (`limits: None`, the canonical "no bound
870        // declared — engine-default applies" author shape) is the
871        // fold's identity element and passes trivially through the
872        // primitive, byte-equal to the pre-lift `if let Some(l) = …`
873        // guard this call site formerly carried. Pinned by the paired
874        // `validate_limits_folds_arm_matches_gate` equivalence pin and
875        // the `validate_limits_accepts_none` / `_accepts_clean_fixture`
876        // positive-control pins in the [`crate::Caixa::validate_limits`]
877        // pin family (`manifest.rs`).
878        //
879        // Same lift discipline the peer per-Caixa compound gates
880        // ([`crate::Caixa::validate_upgrade_from`] d6801df,
881        // [`crate::Caixa::validate_deps`] b5dd55e) each carry — one
882        // named substrate-primitive gate per typed slot folds every
883        // structural axis on that slot (plus the `Option::None`
884        // identity element for the `Option`-shaped slots) onto one
885        // call, so every future consumer that wants to re-check
886        // `:limits` after a per-`{:memory, :fuel, :wall-clock, :cpu}`
887        // patch (the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
888        // materializer's admission webhook, a future `feira validate
889        // --limits` per-caixa admission verb, a per-`:limits` overlay
890        // resolver) reaches the four-axis cascade through one dispatch
891        // rather than re-inlining the `if let Some(l) = …` unwrap-and-
892        // dispatch pattern in lockstep with this wire-up.
893        caixa
894            .validate_limits()
895            .map_err(|err| LayoutError::limits_violation(caixa, err))?;
896
897        // Compound per-Caixa entry gate on the M2 `:behavior` slot's
898        // pure value-shape surface: the layout pipeline's
899        // `if let Some(b) = caixa.behavior() { b.validate() }`
900        // `Option::None → Ok(()) | Some(_) → dispatch` unwrap-and-
901        // dispatch pattern — the six-slot value-shape cascade on the
902        // present-slot arm ([`crate::BehaviorSpec::validate`]'s per-
903        // `:on-init` / `:on-call` / `:on-cast` / `:on-info` /
904        // `:on-state-change` / `:on-terminate` non-empty / relative /
905        // no-`..`-parent-escape / terminating-`.lisp`-extension
906        // arm-set routed through the shared
907        // [`crate::render::require_sandboxed_lisp_path`] helper) —
908        // folded onto the [`crate::Caixa::validate_behavior`] substrate
909        // primitive. The absent-slot arm (`behavior: None`, the
910        // canonical "no callback declared — the runtime falls back to
911        // the wasm-engine's default per arm" author shape) is the
912        // fold's identity element and passes trivially through the
913        // primitive, byte-equal to the pre-lift `if let Some(b) = …`
914        // guard this call site formerly carried. Pinned by the paired
915        // `validate_behavior_folds_arm_matches_gate` equivalence pin
916        // and the `validate_behavior_accepts_none` /
917        // `_accepts_clean_fixture` positive-control pins in the
918        // [`crate::Caixa::validate_behavior`] pin family
919        // (`manifest.rs`).
920        //
921        // The value-shape gate runs BEFORE the on-disk callback-path
922        // existence walk below so a malformed `:behavior` slot
923        // surfaces its self-locating per-slot diagnostic (naming the
924        // offending `:on-*` slot) rather than the less-helpful
925        // "missing behavior-callback" the existence probe would raise
926        // against the resolved sandbox-escape path.
927        //
928        // Same lift discipline the peer per-Caixa compound gates
929        // ([`crate::Caixa::validate_limits`] baa4688,
930        // [`crate::Caixa::validate_upgrade_from`] d6801df,
931        // [`crate::Caixa::validate_deps`] b5dd55e) each carry — one
932        // named substrate-primitive gate per typed slot folds every
933        // structural axis on that slot (plus the `Option::None`
934        // identity element for the `Option`-shaped slots) onto one
935        // call, so every future consumer that wants to re-check
936        // `:behavior` after a per-`{:on-init, …, :on-terminate}`
937        // patch (the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
938        // materializer's admission webhook, a future `feira validate
939        // --behavior` per-caixa admission verb, a per-`:behavior`
940        // overlay resolver) reaches the six-slot cascade through one
941        // dispatch rather than re-inlining the `if let Some(b) = …`
942        // unwrap-and-dispatch pattern in lockstep with this wire-up.
943        // The paired on-disk existence walk stays open-coded at this
944        // altitude because it needs the [`LayoutInvariants::exists`]
945        // filesystem oracle the pure typed-shape surface has no
946        // reference to — mirror of the peer M2 `:upgrade-from` per-
947        // instruction script-path existence probe that stayed at this
948        // altitude after the [`crate::Caixa::validate_upgrade_from`]
949        // lift for the same reason.
950        caixa
951            .validate_behavior()
952            .map_err(|err| LayoutError::behavior_violation(caixa, err))?;
953        if let Some(b) = caixa.behavior() {
954            for p in b.declared_paths() {
955                let full = root.join(p);
956                if !self.exists(&full) {
957                    return Err(LayoutError::MissingEntry {
958                        kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
959                        path: full,
960                    });
961                }
962            }
963        }
964
965        // Compound per-Caixa entry gate on `:upgrade-from`: the layout
966        // pipeline's three-dispatch M2 `:upgrade-from` cascade — the
967        // per-entry shape + cross-entry duplicate-`:from` gate
968        // ([`crate::upgrade::validate_upgrade_from`]), the cross-slot
969        // `:from < :versao` SemVer-2 precedence gate
970        // ([`crate::upgrade::validate_upgrade_from_against_versao`]), and
971        // the cross-slot `:state-change` ↔ `:on-state-change` composition
972        // gate ([`crate::upgrade::validate_upgrade_from_against_behavior`])
973        // — folded onto the [`crate::Caixa::validate_upgrade_from`]
974        // substrate primitive. The three dispatches run in the same
975        // canonical order at the primitive (per-entry → versao → behavior)
976        // so the fold is byte-for-byte equivalent to the pre-fold
977        // three-block cascade this call site formerly carried, pinned by
978        // the paired
979        // `validate_upgrade_from_folds_{per_entry,versao,behavior}_arm_matches_gate`
980        // equivalence pins and the
981        // `validate_upgrade_from_{per_entry_arm_fires_before_versao_arm,
982        // versao_arm_fires_before_behavior_arm}` ordering pins in the
983        // [`crate::Caixa::validate_upgrade_from`] pin family
984        // (`manifest.rs`).
985        //
986        // Runs BEFORE the existing per-instruction script-path existence
987        // pass below so a malformed typed slot surfaces its own
988        // self-locating diagnostic rather than the less-helpful "missing
989        // upgrade-script" (which doesn't fire for non-script axes at all).
990        // Same lift discipline the peer per-slot compound gates
991        // ([`crate::AplicacaoSpec::validate_contratos`] and its
992        // `:membros` / `:entrada` / `:placement` / `:politicas` peers,
993        // [`crate::MeshPolicy::validate`],
994        // [`crate::SupervisorSpec::validate_children`]) each carry — one
995        // named substrate-primitive gate per typed slot folds every
996        // structural axis on that slot onto one call, so every future
997        // consumer that wants to re-check `:upgrade-from` after a
998        // per-entry patch (the deferred `caixa.pleme.io/v1alpha1/Caixa`
999        // CR materializer's admission webhook, a future `feira validate
1000        // --upgrade` per-caixa admission verb, a per-`:upgrade-from`
1001        // overlay resolver) reaches the three-arm compound gate through
1002        // one dispatch rather than re-inlining the three-dispatch
1003        // cascade in lockstep with this wire-up.
1004        //
1005        // The per-instruction on-disk existence-probe walk below stays
1006        // open-coded at the layout wire-up site — that arm needs the
1007        // filesystem oracle on the [`LayoutInvariants`] trait, not on
1008        // the pure per-Caixa typed-shape surface the compound gate
1009        // folds. Same posture [`crate::Caixa::validate_code_paths`] takes
1010        // on the sibling code-path axes: the typed-shape gate fires on
1011        // the per-Caixa surface, the on-disk existence check fires on
1012        // the [`StandardLayout`] surface.
1013        caixa
1014            .validate_upgrade_from()
1015            .map_err(|err| LayoutError::upgrade_violation(caixa, err))?;
1016        for entry in caixa.upgrade_from() {
1017            for instr in entry.instructions() {
1018                if let Some(p) = instr.declared_path() {
1019                    let full = root.join(p);
1020                    if !self.exists(&full) {
1021                        return Err(LayoutError::MissingEntry {
1022                            kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
1023                            path: full,
1024                        });
1025                    }
1026                }
1027            }
1028        }
1029
1030        // Supervisor invariants (typed shape — children, restart strategy).
1031        // The "supervisor doesn't own code" check is at the top of verify()
1032        // so it fires before the existence-check loops.
1033        if caixa.kind().is_supervisor() {
1034            // Raw `:restart-window` parse gate on the flat
1035            // `Caixa::restart_window: Option<String>` axis — the last
1036            // orphan-validator on the typed Caixa surface flagged by the
1037            // [`Self::validate_deps`] wire-up's closing comment (the
1038            // "Supervisor-axis specific" remainder) and the
1039            // [`Caixa::supervisor_view`] doc-comment's "the future
1040            // layout-side wire-up" pin. Until this gate landed
1041            // [`Caixa::validate_restart_window`] existed as `pub fn` on
1042            // [`Caixa`] with full per-arm unit coverage in `manifest::tests`
1043            // (`validate_restart_window_rejects_*` — fractional seconds,
1044            // decimal-shaped integer, half-unit minute, leading sign,
1045            // unknown unit, garbage, empty-after-trim; eight rejection
1046            // arms total), but no production code path called it —
1047            // `feira build` (the canonical author-time gate; routes
1048            // through [`StandardLayout::verify`]) silently accepted a
1049            // malformed `:restart-window` and [`Caixa::supervisor_view`]
1050            // soft-swallowed the parse failure as `restart_window: None`
1051            // (i.e. the canonical "omit the slot to express no reset"
1052            // sentinel), turning every malformed window into a never-reset
1053            // supervisor far from the source `caixa.lisp`, with no field
1054            // naming the offending `:restart-window`. The Erlang/OTP
1055            // `MaxIntensity / Period` invariant the typed [`SupervisorSpec`]
1056            // gate (`view.validate()` immediately below) enforces on the
1057            // `Option<Duration>` value never reached the gate at all on
1058            // these inputs: the parse error was already laundered to
1059            // `None`, and `None` is the canonical "never reset" shape
1060            // that always validates cleanly. Lifting the parse gate to
1061            // the layout-pipeline wire-up closes the laundering — every
1062            // value past this gate either parses through the shared
1063            // `crate::supervisor::duration_codec::parse` (and therefore
1064            // round-trips canonically) or fires the new
1065            // [`Self::RestartWindowViolation`] envelope at the source.
1066            //
1067            // Runs *inside* the `kind == Supervisor` branch (rather than
1068            // alongside the peer flat-Caixa gates `validate_nome` /
1069            // `validate_versao` / `validate_deps` / `validate_code_paths`
1070            // above the kind dispatch) because `:restart-window` is in
1071            // the Supervisor slot set per [`Caixa::declared_supervisor_slots`]
1072            // — every non-Supervisor caixa with `:restart-window` set
1073            // already errors upstream via the
1074            // [`Self::SupervisorSlotsOnNonSupervisor`] kind-coherence gate
1075            // (line 243-252), so reaching this gate on a non-Supervisor
1076            // kind would be a no-op (the field is `None` by construction).
1077            // Runs *before* `view.validate()` so the parse-side diagnostic
1078            // surfaces first on the raw-string axis — a `:restart-window
1079            // "1.5s"` lands on the more self-locating
1080            // `RestartWindowViolation` (which names the offending raw
1081            // string verbatim) rather than the laundered-to-`None`
1082            // soft-pass that the typed view would silently let through.
1083            //
1084            // Same per-axis `*Violation { caixa, issue }` envelope every
1085            // peer flat-Caixa wrap exposes ([`Self::NomeViolation`] /
1086            // [`Self::VersaoViolation`] 1f74a5f,
1087            // [`Self::DepsViolation`] aa77d0f, [`Self::CodePathViolation`]
1088            // b868442). Threads [`ManifestError::RestartWindowMalformed`]
1089            // Display through verbatim — the per-arm reason already names
1090            // the offending raw value (e.g. `":restart-window \"1.5s\" is
1091            // not a canonical duration: …"`), so the wrap's `issue`
1092            // carries a self-locating "which axis, which value, why"
1093            // without re-shaping the parser-side reason.
1094            caixa
1095                .validate_restart_window()
1096                .map_err(|err| LayoutError::restart_window_violation(caixa, err))?;
1097            // Compound per-Caixa entry gate on the Supervisor-kind
1098            // supervision-tree slot family: the layout pipeline's paired
1099            // `let view = caixa.supervisor_view().expect(...);
1100            // view.validate() … validate_no_self_supervision(...) …`
1101            // cascade — the typed-shape cascade
1102            // ([`crate::SupervisorSpec::validate`]'s per-slot gates on
1103            // `:estrategia` ↔ `:children` invariants, `:max-restarts` /
1104            // `:restart-window` bounds, per-child DNS-1123 `:caixa`
1105            // names, semver-valid `:versao` constraints, the
1106            // set-not-multiset duplicate-child gate) and the cross-slot
1107            // self-edge gate
1108            // ([`crate::supervisor::validate_no_self_supervision`], the
1109            // `:children :caixa` ≠ `:nome` invariant the typed view
1110            // cannot enforce on its own because it carries the children
1111            // but not the parent `:nome`) — folded onto the
1112            // [`crate::Caixa::validate_supervisor_shape`] substrate
1113            // primitive. The two arms run in the same canonical order at
1114            // the primitive (typed-shape cascade → cross-slot self-edge)
1115            // so the fold is byte-for-byte equivalent to the pre-fold
1116            // two-block cascade this call site formerly carried, pinned
1117            // by the paired
1118            // `validate_supervisor_shape_folds_{view,self_supervision}_arm_matches_gate`
1119            // equivalence pins and the
1120            // `validate_supervisor_shape_view_arm_fires_before_self_supervision_arm`
1121            // ordering pin in the
1122            // [`crate::Caixa::validate_supervisor_shape`] pin family
1123            // (`manifest.rs`).
1124            //
1125            // Same lift discipline the peer per-Caixa compound gates
1126            // ([`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
1127            // [`crate::Caixa::validate_upgrade_from`] d6801df,
1128            // [`crate::Caixa::validate_deps`] b5dd55e,
1129            // [`crate::Caixa::validate_limits`] baa4688,
1130            // [`crate::Caixa::validate_behavior`] 0d2877a) each carry —
1131            // one named substrate-primitive gate folds every structural +
1132            // cross-slot axis on that slot family onto one call, so every
1133            // future consumer that wants to re-check the Supervisor shape
1134            // after a per-slot patch (the wasm-operator's hierarchical
1135            // reconciliation scheduler, the M4
1136            // `mesh.pleme.io/v1alpha1/Supervisor` CR materializer's
1137            // admission webhook, a future `feira validate --supervisor`
1138            // per-caixa admission verb, a per-Supervisor overlay
1139            // resolver) reaches the two-arm compound gate through one
1140            // dispatch rather than re-inlining the two-dispatch cascade
1141            // in lockstep with this wire-up. Peer with the
1142            // [`crate::render::require_supervisor_view`] compound entry
1143            // gate every per-Supervisor renderer would route through
1144            // (which already folds the same `spec.validate()` +
1145            // `validate_no_self_supervision` two-arm cascade behind its
1146            // `require_kind` + `validate_restart_window` prelude): the
1147            // two consumers of the Supervisor-shape cascade now share
1148            // one substrate primitive on each side of the
1149            // author-time-vs-renderer split, rather than two open-coded
1150            // cascades kept in lockstep.
1151            //
1152            // `:restart-window` stays wired above through its own
1153            // per-axis `LayoutError::RestartWindowViolation` envelope
1154            // (the raw-string parse gate on the flat
1155            // `Caixa::restart_window: Option<String>` axis, distinct
1156            // from the typed view's `Duration`-shape gate) — the fold
1157            // covers the two arms that share the
1158            // `LayoutError::SupervisorViolation` envelope; the
1159            // parse-gate arm keeps its self-locating envelope so a
1160            // malformed `:restart-window` surfaces the raw-value
1161            // diagnostic rather than the laundered-to-`None` soft-pass.
1162            caixa
1163                .validate_supervisor_shape()
1164                .map_err(|err| LayoutError::supervisor_violation(caixa, err))?;
1165        }
1166
1167        // Aplicacao invariants — typed graph composition. Like
1168        // Supervisor, an Aplicacao runs no code itself.
1169        //
1170        // Compound per-Caixa entry gate on the Aplicacao-kind mesh-slot
1171        // family: the layout pipeline's paired `let view =
1172        // caixa.aplicacao_view().expect(...); view.validate() …
1173        // validate_no_self_membership(...) …` cascade — the typed-shape
1174        // cascade ([`crate::AplicacaoSpec::validate`]'s per-slot gates
1175        // on `:membros`, `:contratos`, `:entrada`, `:placement`,
1176        // `:politicas`, in that declared order) and the cross-slot
1177        // self-edge gate ([`crate::aplicacao::validate_no_self_membership`],
1178        // the `:membros :caixa` ≠ `:nome` invariant the typed view
1179        // cannot enforce on its own because it carries the membros but
1180        // not the parent `:nome`) — folded onto the
1181        // [`crate::Caixa::validate_aplicacao_shape`] substrate primitive.
1182        // The two arms run in the same canonical order at the primitive
1183        // (typed-shape cascade → cross-slot self-edge) so the fold is
1184        // byte-for-byte equivalent to the pre-fold two-block cascade
1185        // this call site formerly carried, pinned by the paired
1186        // `validate_aplicacao_shape_folds_{view,self_membership}_arm_matches_gate`
1187        // equivalence pins and the
1188        // `validate_aplicacao_shape_view_arm_fires_before_self_membership_arm`
1189        // ordering pin in the [`crate::Caixa::validate_aplicacao_shape`]
1190        // pin family (`manifest.rs`).
1191        //
1192        // Same lift discipline the peer per-slot compound gates
1193        // ([`crate::Caixa::validate_upgrade_from`] d6801df,
1194        // [`crate::Caixa::validate_deps`] b5dd55e,
1195        // [`crate::Caixa::validate_limits`] baa4688,
1196        // [`crate::Caixa::validate_behavior`] 0d2877a) each carry — one
1197        // named substrate-primitive gate folds every structural +
1198        // cross-slot axis on that slot family onto one call, so every
1199        // future consumer that wants to re-check the Aplicacao shape
1200        // after a per-slot patch (the deferred
1201        // `caixa.pleme.io/v1alpha1/Caixa` CR materializer's admission
1202        // webhook, a future `feira validate --aplicacao` per-caixa
1203        // admission verb, a per-Aplicacao overlay resolver) reaches the
1204        // two-arm compound gate through one dispatch rather than
1205        // re-inlining the two-dispatch cascade in lockstep with this
1206        // wire-up. Peer with the [`crate::render::require_aplicacao_view`]
1207        // compound entry gate every per-Aplicacao renderer routes
1208        // through (3aefefb): the two consumers of the Aplicacao-shape
1209        // cascade now share one substrate primitive on each side of the
1210        // author-time-vs-renderer split, rather than two open-coded
1211        // cascades kept in lockstep.
1212        //
1213        // The outer `if caixa.kind().is_aplicacao()` guard stays because
1214        // [`crate::Caixa::validate_aplicacao_shape`] is the fold's
1215        // identity element on non-Aplicacao kinds (returns `Ok(())`
1216        // without touching the mesh slots — same posture as
1217        // [`crate::Caixa::validate_limits`] / [`Self`]::
1218        // [`crate::Caixa::validate_behavior`] on their `Option`-shaped
1219        // slots); the guard is a redundant but zero-cost fast-path that
1220        // preserves the peer supervisor branch's `if
1221        // caixa.kind().is_supervisor()` parallel structure at this
1222        // altitude.
1223        if caixa.kind().is_aplicacao() {
1224            caixa
1225                .validate_aplicacao_shape()
1226                .map_err(|err| LayoutError::aplicacao_violation(caixa, err))?;
1227        }
1228
1229        Ok(())
1230    }
1231}
1232
1233#[derive(Debug, Error, PartialEq, Eq)]
1234pub enum LayoutError {
1235    #[error("manifest missing: {}", .0.display())]
1236    MissingManifest(PathBuf),
1237    #[error("caixa '{caixa}' is a Biblioteca but has no lib entry — expected {}", expected.display())]
1238    MissingLib { caixa: String, expected: PathBuf },
1239    #[error("caixa '{0}' is a Binario but has no :exe entries")]
1240    BinarioWithoutExe(String),
1241    #[error("caixa '{0}' is a Servico but has no :servicos entries")]
1242    ServicoWithoutServicos(String),
1243    #[error("declared {kind} entry missing: {}", path.display())]
1244    MissingEntry { kind: &'static str, path: PathBuf },
1245    #[error("exe entry outside exe/ directory: {}", .0.display())]
1246    ExeOutsideDir(PathBuf),
1247    #[error("servico entry outside servicos/ directory: {}", .0.display())]
1248    ServicoOutsideDir(PathBuf),
1249    #[error("caixa '{caixa}' has invalid :nome: {issue}")]
1250    NomeViolation { caixa: String, issue: String },
1251    #[error("caixa '{caixa}' has invalid :versao: {issue}")]
1252    VersaoViolation { caixa: String, issue: String },
1253    #[error("caixa '{caixa}' has invalid :deps / :deps-dev entry: {issue}")]
1254    DepsViolation { caixa: String, issue: String },
1255    #[error("caixa '{caixa}' has invalid :etiquetas entry: {issue}")]
1256    EtiquetasViolation { caixa: String, issue: String },
1257    #[error("caixa '{caixa}' has invalid :autores entry: {issue}")]
1258    AutoresViolation { caixa: String, issue: String },
1259    #[error("caixa '{caixa}' has invalid :repositorio: {issue}")]
1260    RepositorioViolation { caixa: String, issue: String },
1261    #[error("caixa '{caixa}' has invalid :descricao: {issue}")]
1262    DescricaoViolation { caixa: String, issue: String },
1263    #[error("caixa '{caixa}' has invalid :licenca: {issue}")]
1264    LicencaViolation { caixa: String, issue: String },
1265    #[error("caixa '{caixa}' has invalid :edicao: {issue}")]
1266    EdicaoViolation { caixa: String, issue: String },
1267    #[error("caixa '{caixa}' has invalid code-path entry: {issue}")]
1268    CodePathViolation { caixa: String, issue: String },
1269    #[error("caixa '{caixa}' has invalid :limits: {issue}")]
1270    LimitsViolation { caixa: String, issue: String },
1271    #[error("caixa '{caixa}' has invalid :behavior callback: {issue}")]
1272    BehaviorViolation { caixa: String, issue: String },
1273    #[error("caixa '{caixa}' has invalid :upgrade-from entry: {issue}")]
1274    UpgradeViolation { caixa: String, issue: String },
1275    #[error("supervisor caixa '{caixa}' violates typed shape: {issue}")]
1276    SupervisorViolation { caixa: String, issue: String },
1277    #[error("supervisor caixa '{caixa}' has invalid :restart-window: {issue}")]
1278    RestartWindowViolation { caixa: String, issue: String },
1279    #[error(
1280        "supervisor caixa '{0}' must not declare :bibliotecas, :exe, or :servicos — supervisors don't run code, they orchestrate other caixas"
1281    )]
1282    SupervisorOwnsCode(String),
1283    #[error("aplicacao caixa '{caixa}' violates typed shape: {issue}")]
1284    AplicacaoViolation { caixa: String, issue: String },
1285    #[error(
1286        "aplicacao caixa '{0}' must not declare :bibliotecas, :exe, or :servicos — aplicacaos compose Servicos, they don't run code themselves"
1287    )]
1288    AplicacaoOwnsCode(String),
1289    #[error(
1290        "acao caixa '{0}' must not declare :bibliotecas, :exe, or :servicos — acaos carry a typed CI run (:ci), they don't run code themselves"
1291    )]
1292    AcaoOwnsCode(String),
1293    #[error(
1294        "caixa '{caixa}' is :kind {kind:?} but declares Aplicacao-only mesh slot(s): {slots} — \
1295         :membros / :contratos / :politicas / :placement / :entrada compose a :kind Aplicacao's \
1296         typed graph (MESH-COMPOSITION §III.1) and are silently ignored on every other kind \
1297         (never validated, never rendered); move them to a :kind Aplicacao caixa or remove them"
1298    )]
1299    MeshSlotsOnNonAplicacao {
1300        caixa: String,
1301        kind: CaixaKind,
1302        slots: String,
1303    },
1304    #[error(
1305        "caixa '{caixa}' is :kind {kind:?} but declares Supervisor-only slot(s): {slots} — \
1306         :estrategia / :max-restarts / :restart-window / :children compose a :kind Supervisor's \
1307         typed OTP supervisor (INSPIRATIONS §II.2) and are silently ignored on every other kind \
1308         (never validated, never reconciled); move them to a :kind Supervisor caixa or remove them"
1309    )]
1310    SupervisorSlotsOnNonSupervisor {
1311        caixa: String,
1312        kind: CaixaKind,
1313        slots: String,
1314    },
1315    #[error(
1316        "caixa '{caixa}' is :kind {kind:?} but declares Servico-only slot(s): {slots} — \
1317         :limits / :behavior / :upgrade-from configure the runtime of a long-running :kind Servico \
1318         wasm component (INSPIRATIONS §III.1 / §II.3 / §II.4) and are silently ignored on every \
1319         other kind (never rendered into a chart or programs.yaml entry); move them to a :kind \
1320         Servico caixa or remove them"
1321    )]
1322    ServicoSlotsOnNonServico {
1323        caixa: String,
1324        kind: CaixaKind,
1325        slots: String,
1326    },
1327    #[error(
1328        "caixa '{caixa}' is :kind {kind:?} but declares foreign code-surface slot(s): {slots} — \
1329         :exe is the nix-built executable surface owned only by :kind Binario, :servicos is the \
1330         wasm-component + ComputeUnit daemon surface owned only by :kind Servico; \
1331         caixa-helm / caixa-flux / caixa-flake gate emission on `require_kind(_, <owning-kind>)`, \
1332         so a declared :exe / :servicos on the wrong code-running kind is silently ignored — the \
1333         path is validated by the layout's path-existence loops but never rendered into a build \
1334         target or programs.yaml entry. Move the slot to its owning kind, change :kind to match \
1335         (Binario for :exe, Servico for :servicos), or drop the slot entirely"
1336    )]
1337    ForeignCodeSlot {
1338        caixa: String,
1339        kind: CaixaKind,
1340        slots: String,
1341    },
1342    #[error("caixa '{0}' is an Acao but has no :ci slot")]
1343    MissingCi(String),
1344    #[error(
1345        "caixa '{caixa}' is :kind {kind:?} but declares the Acao-only :ci slot — \
1346         :ci carries a typed CI run (canteiro_types::CiRun, CANTEIRO §7.1-C) that only the \
1347         caixa-actions renderer validates for :kind Acao, and is silently ignored on every \
1348         other kind (never decomposed, never rendered); move it to a :kind Acao caixa or \
1349         remove it"
1350    )]
1351    CiOnNonAcao { caixa: String, kind: CaixaKind },
1352}
1353
1354// Fold the layout-pipeline per-Caixa violation wrap onto one substrate
1355// primitive per typed slot. Every `LayoutError::*Violation { caixa, issue }`
1356// variant follows the same uniform shape — `caixa = layout-Caixa's :nome`,
1357// `issue = the gate's per-arm Display` — and every wire-up in
1358// [`StandardLayout::verify`] used to open-code the identical five-line
1359// `.map_err(|err| LayoutError::XxxViolation { caixa: caixa.nome().to_string(),
1360// issue: err.to_string() })` block, once per typed slot. Sixteen distinct
1361// wrap-variants × eighteen wire-up sites is exactly the duplication the
1362// PRIME DIRECTIVE names as a bug: every future consumer that wants to add a
1363// new per-slot gate (the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
1364// materializer's admission webhook, a future per-slot `feira validate`
1365// verb, a per-slot overlay resolver) had to re-inline the five-line block
1366// in lockstep with the pre-existing wire-ups.
1367//
1368// The macro below generates one static constructor per variant of shape
1369// `fn <slot>_violation(caixa: &Caixa, err: impl Display) -> LayoutError`,
1370// so every wire-up site collapses onto one dispatch:
1371// `caixa.validate_<slot>().map_err(|err| LayoutError::<slot>_violation(caixa,
1372// err))?;`. The uniform two-slot construction (`caixa: caixa.nome()
1373// .to_string()`, `issue: err.to_string()`) is spelled once — inside the
1374// macro — rather than at every wire-up site. Every constructor is
1375// `#[must_use]` so a caller who mistakenly discards the wrapped error
1376// (rather than routing it through `?`) trips a compile warning at the
1377// wire-up site.
1378//
1379// Peer with the per-slot compound entry gates every substrate primitive
1380// on the M2/M3 typed-slot family already carries
1381// ([`crate::Caixa::validate_deps`] b5dd55e, [`crate::Caixa::validate_limits`]
1382// baa4688, [`crate::Caixa::validate_behavior`] 0d2877a,
1383// [`crate::Caixa::validate_upgrade_from`] d6801df,
1384// [`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
1385// [`crate::AplicacaoSpec::validate_contratos`],
1386// [`crate::MeshPolicy::validate`],
1387// [`crate::SupervisorSpec::validate_children`]): the author-time gates
1388// fold onto one substrate primitive per typed slot; here the layout-side
1389// error-wrap folds onto one substrate primitive per typed variant, so the
1390// two sides of the layout pipeline's per-slot cascade (the gate, the
1391// wrap) each route through one call rather than N open-coded block
1392// repetitions.
1393macro_rules! layout_violation_ctors {
1394    ($($ctor:ident => $variant:ident),* $(,)?) => {
1395        impl LayoutError {
1396            $(
1397                #[doc = concat!(
1398                    "Construct a [`LayoutError::",
1399                    stringify!($variant),
1400                    "`] wrapping `err` under `caixa.nome()`. Folds the ",
1401                    "uniform `{ caixa: caixa.nome().to_string(), issue: ",
1402                    "err.to_string() }` two-slot construction onto one ",
1403                    "substrate primitive so every ",
1404                    "[`StandardLayout::verify`] wire-up on this variant ",
1405                    "reads through one dispatch rather than the pre-lift ",
1406                    "five-line open-coded block."
1407                )]
1408                #[must_use]
1409                pub fn $ctor<E: std::fmt::Display>(caixa: &crate::Caixa, err: E) -> Self {
1410                    Self::$variant {
1411                        caixa: caixa.nome().to_string(),
1412                        issue: err.to_string(),
1413                    }
1414                }
1415            )*
1416        }
1417    };
1418}
1419
1420layout_violation_ctors! {
1421    nome_violation => NomeViolation,
1422    versao_violation => VersaoViolation,
1423    deps_violation => DepsViolation,
1424    etiquetas_violation => EtiquetasViolation,
1425    autores_violation => AutoresViolation,
1426    repositorio_violation => RepositorioViolation,
1427    descricao_violation => DescricaoViolation,
1428    licenca_violation => LicencaViolation,
1429    edicao_violation => EdicaoViolation,
1430    code_path_violation => CodePathViolation,
1431    limits_violation => LimitsViolation,
1432    behavior_violation => BehaviorViolation,
1433    upgrade_violation => UpgradeViolation,
1434    restart_window_violation => RestartWindowViolation,
1435    supervisor_violation => SupervisorViolation,
1436    aplicacao_violation => AplicacaoViolation,
1437}
1438
1439// Fold the four `LayoutError::*SlotsOn*` / `LayoutError::ForeignCodeSlot`
1440// kind-coherence wrap sites onto one substrate primitive per typed variant —
1441// the sibling of [`layout_violation_ctors!`] above on the second uniform
1442// error-envelope shape `LayoutError` carries: `{ caixa: caixa.nome(),
1443// kind: caixa.kind(), slots: <declared_*_slots()>.join(" ") }`. Every
1444// wire-up in [`StandardLayout::verify`] on this shape (four sites: the
1445// M3-mesh gate on non-Aplicacao, the supervisor-tree gate on
1446// non-Supervisor, the M2-runtime gate on non-Servico, the code-surface
1447// `ForeignCodeSlot` gate) used to open-code the identical four-field
1448// `.{ caixa: caixa.nome().to_string(), kind: caixa.kind(), slots:
1449// <declared_*_slots>.join(" ") }` block — the exact "same block re-inlined
1450// at every consumer" shape the PRIME DIRECTIVE names as a bug on the same
1451// altitude the peer [`layout_violation_ctors!`] macro just closed on the
1452// `{ caixa, issue }` sibling shape.
1453//
1454// The macro below generates one static constructor per variant of shape
1455// `fn <slot>_on_non_<owner>(caixa: &Caixa, slots: Vec<&'static str>) ->
1456// LayoutError`, so every wire-up site collapses onto one dispatch:
1457// `return Err(LayoutError::<slot>_on_non_<owner>(caixa, <declared_slots>));`.
1458// The uniform four-field construction is spelled once — inside the macro —
1459// rather than at every wire-up site. Every constructor is `#[must_use]` so
1460// a caller who mistakenly discards the constructed error (rather than
1461// routing it through `return Err(…)`) trips a compile warning at the
1462// wire-up site.
1463//
1464// Peer with the `_violation` constructor family above on the same
1465// `LayoutError` — the two together now fold every uniform-shape
1466// `LayoutError` variant carried by [`StandardLayout::verify`] onto one
1467// substrate primitive per typed variant, so the layout-side error-wrap
1468// surface reads through one dispatch per variant rather than N open-coded
1469// blocks. Every future consumer that wants to construct one of these
1470// variants outside the layout pipeline (a per-slot admission webhook, a
1471// `feira validate --kind X` verb, an overlay resolver rejecting a
1472// kind-foreign patch) reaches its variant through one call, matching the
1473// `_violation` family's substrate-primitive discipline.
1474macro_rules! layout_slot_kind_ctors {
1475    ($($ctor:ident => $variant:ident),* $(,)?) => {
1476        impl LayoutError {
1477            $(
1478                #[doc = concat!(
1479                    "Construct a [`LayoutError::",
1480                    stringify!($variant),
1481                    "`] naming the offending slot list under `caixa.nome()` ",
1482                    "at `caixa.kind()`. Folds the uniform `{ caixa: caixa.",
1483                    "nome().to_string(), kind: caixa.kind(), slots: slots.",
1484                    "join(\" \") }` four-field construction onto one substrate ",
1485                    "primitive so every [`StandardLayout::verify`] wire-up on ",
1486                    "this variant reads through one dispatch rather than the ",
1487                    "pre-lift open-coded block."
1488                )]
1489                #[must_use]
1490                pub fn $ctor(caixa: &crate::Caixa, slots: Vec<&'static str>) -> Self {
1491                    Self::$variant {
1492                        caixa: caixa.nome().to_string(),
1493                        kind: caixa.kind(),
1494                        slots: slots.join(" "),
1495                    }
1496                }
1497            )*
1498        }
1499    };
1500}
1501
1502layout_slot_kind_ctors! {
1503    mesh_slots_on_non_aplicacao => MeshSlotsOnNonAplicacao,
1504    supervisor_slots_on_non_supervisor => SupervisorSlotsOnNonSupervisor,
1505    servico_slots_on_non_servico => ServicoSlotsOnNonServico,
1506    foreign_code_slot => ForeignCodeSlot,
1507}
1508
1509#[cfg(test)]
1510mod tests {
1511    use super::*;
1512    use crate::{Caixa, CaixaKind};
1513    use std::path::PathBuf;
1514
1515    fn caixa(kind: CaixaKind) -> Caixa {
1516        Caixa {
1517            nome: "demo".into(),
1518            versao: "0.1.0".into(),
1519            kind,
1520            edicao: None,
1521            descricao: None,
1522            repositorio: None,
1523            licenca: None,
1524            autores: vec![],
1525            etiquetas: vec![],
1526            deps: vec![],
1527            deps_dev: vec![],
1528            exe: vec![],
1529            bibliotecas: vec![],
1530            servicos: vec![],
1531            // M2 typed-substrate slots default to absent.
1532            limits: None,
1533            behavior: None,
1534            upgrade_from: vec![],
1535            estrategia: None,
1536            max_restarts: None,
1537            restart_window: None,
1538            children: vec![],
1539            // M3 Aplicacao slots default to absent.
1540            membros: vec![],
1541            contratos: vec![],
1542            politicas: None,
1543            placement: None,
1544            entrada: None,
1545            ci: None,
1546        }
1547    }
1548
1549    #[test]
1550    fn missing_manifest_errors() {
1551        let layout = StandardLayout::new().with_path_exists(|_| false);
1552        let err = layout
1553            .verify(&caixa(CaixaKind::Biblioteca), Path::new("/tmp/x"))
1554            .unwrap_err();
1555        assert!(matches!(err, LayoutError::MissingManifest(_)));
1556    }
1557
1558    // ── LayoutError::*_violation constructor family ──────────────────────
1559    //
1560    // The [`layout_violation_ctors!`] macro (below the `LayoutError` enum
1561    // definition) generates one static constructor per `*Violation { caixa,
1562    // issue }` variant that folds the uniform `{ caixa: caixa.nome()
1563    // .to_string(), issue: err.to_string() }` two-slot construction onto
1564    // one substrate primitive. The per-variant equivalence pins below
1565    // (fail-before-pass-after by construction — a byte-mismatched macro
1566    // arm would trip its equivalence pin first) lock each generated
1567    // constructor to its struct-literal peer under `PartialEq`, so every
1568    // wire-up in [`StandardLayout::verify`] on that variant produces a
1569    // byte-equal `LayoutError` to the pre-lift open-coded block. The
1570    // fixture caixa fires under `caixa("demo")` so the `caixa: "demo"`
1571    // half is pinned; the fixture error fires under a fixed `&str` so the
1572    // `issue: <literal>` half is pinned; the two together pin every field
1573    // of every generated variant.
1574
1575    fn layout_violation_ctor_fixture() -> (Caixa, &'static str) {
1576        (caixa(CaixaKind::Biblioteca), "sample issue text")
1577    }
1578
1579    // `assert_eq!` uses `PartialEq::eq(&self, &other)` under the hood, so
1580    // `actual`/`expected` are only ever read, never moved into anything —
1581    // the ergonomic tradeoff (owned + move-in vs. reference + &-borrow at
1582    // every call site) favors the owned form for a test-only assertion
1583    // helper called from 16 wire-up pins. The lint targets the general
1584    // API-shape case where callers still have downstream uses for the
1585    // moved value; the assertion helper terminates on the equality check.
1586    #[allow(clippy::needless_pass_by_value)]
1587    fn assert_violation_ctor_matches(actual: LayoutError, expected: LayoutError) {
1588        assert_eq!(
1589            actual, expected,
1590            "generated constructor must produce byte-equal LayoutError to open-coded struct-literal wrap",
1591        );
1592    }
1593
1594    #[test]
1595    fn nome_violation_ctor_matches_struct_literal_wrap() {
1596        let (c, issue) = layout_violation_ctor_fixture();
1597        assert_violation_ctor_matches(
1598            LayoutError::nome_violation(&c, issue),
1599            LayoutError::NomeViolation {
1600                caixa: c.nome().to_string(),
1601                issue: issue.to_string(),
1602            },
1603        );
1604    }
1605
1606    #[test]
1607    fn versao_violation_ctor_matches_struct_literal_wrap() {
1608        let (c, issue) = layout_violation_ctor_fixture();
1609        assert_violation_ctor_matches(
1610            LayoutError::versao_violation(&c, issue),
1611            LayoutError::VersaoViolation {
1612                caixa: c.nome().to_string(),
1613                issue: issue.to_string(),
1614            },
1615        );
1616    }
1617
1618    #[test]
1619    fn deps_violation_ctor_matches_struct_literal_wrap() {
1620        let (c, issue) = layout_violation_ctor_fixture();
1621        assert_violation_ctor_matches(
1622            LayoutError::deps_violation(&c, issue),
1623            LayoutError::DepsViolation {
1624                caixa: c.nome().to_string(),
1625                issue: issue.to_string(),
1626            },
1627        );
1628    }
1629
1630    #[test]
1631    fn etiquetas_violation_ctor_matches_struct_literal_wrap() {
1632        let (c, issue) = layout_violation_ctor_fixture();
1633        assert_violation_ctor_matches(
1634            LayoutError::etiquetas_violation(&c, issue),
1635            LayoutError::EtiquetasViolation {
1636                caixa: c.nome().to_string(),
1637                issue: issue.to_string(),
1638            },
1639        );
1640    }
1641
1642    #[test]
1643    fn autores_violation_ctor_matches_struct_literal_wrap() {
1644        let (c, issue) = layout_violation_ctor_fixture();
1645        assert_violation_ctor_matches(
1646            LayoutError::autores_violation(&c, issue),
1647            LayoutError::AutoresViolation {
1648                caixa: c.nome().to_string(),
1649                issue: issue.to_string(),
1650            },
1651        );
1652    }
1653
1654    #[test]
1655    fn repositorio_violation_ctor_matches_struct_literal_wrap() {
1656        let (c, issue) = layout_violation_ctor_fixture();
1657        assert_violation_ctor_matches(
1658            LayoutError::repositorio_violation(&c, issue),
1659            LayoutError::RepositorioViolation {
1660                caixa: c.nome().to_string(),
1661                issue: issue.to_string(),
1662            },
1663        );
1664    }
1665
1666    #[test]
1667    fn descricao_violation_ctor_matches_struct_literal_wrap() {
1668        let (c, issue) = layout_violation_ctor_fixture();
1669        assert_violation_ctor_matches(
1670            LayoutError::descricao_violation(&c, issue),
1671            LayoutError::DescricaoViolation {
1672                caixa: c.nome().to_string(),
1673                issue: issue.to_string(),
1674            },
1675        );
1676    }
1677
1678    #[test]
1679    fn licenca_violation_ctor_matches_struct_literal_wrap() {
1680        let (c, issue) = layout_violation_ctor_fixture();
1681        assert_violation_ctor_matches(
1682            LayoutError::licenca_violation(&c, issue),
1683            LayoutError::LicencaViolation {
1684                caixa: c.nome().to_string(),
1685                issue: issue.to_string(),
1686            },
1687        );
1688    }
1689
1690    #[test]
1691    fn edicao_violation_ctor_matches_struct_literal_wrap() {
1692        let (c, issue) = layout_violation_ctor_fixture();
1693        assert_violation_ctor_matches(
1694            LayoutError::edicao_violation(&c, issue),
1695            LayoutError::EdicaoViolation {
1696                caixa: c.nome().to_string(),
1697                issue: issue.to_string(),
1698            },
1699        );
1700    }
1701
1702    #[test]
1703    fn code_path_violation_ctor_matches_struct_literal_wrap() {
1704        let (c, issue) = layout_violation_ctor_fixture();
1705        assert_violation_ctor_matches(
1706            LayoutError::code_path_violation(&c, issue),
1707            LayoutError::CodePathViolation {
1708                caixa: c.nome().to_string(),
1709                issue: issue.to_string(),
1710            },
1711        );
1712    }
1713
1714    #[test]
1715    fn limits_violation_ctor_matches_struct_literal_wrap() {
1716        let (c, issue) = layout_violation_ctor_fixture();
1717        assert_violation_ctor_matches(
1718            LayoutError::limits_violation(&c, issue),
1719            LayoutError::LimitsViolation {
1720                caixa: c.nome().to_string(),
1721                issue: issue.to_string(),
1722            },
1723        );
1724    }
1725
1726    #[test]
1727    fn behavior_violation_ctor_matches_struct_literal_wrap() {
1728        let (c, issue) = layout_violation_ctor_fixture();
1729        assert_violation_ctor_matches(
1730            LayoutError::behavior_violation(&c, issue),
1731            LayoutError::BehaviorViolation {
1732                caixa: c.nome().to_string(),
1733                issue: issue.to_string(),
1734            },
1735        );
1736    }
1737
1738    #[test]
1739    fn upgrade_violation_ctor_matches_struct_literal_wrap() {
1740        let (c, issue) = layout_violation_ctor_fixture();
1741        assert_violation_ctor_matches(
1742            LayoutError::upgrade_violation(&c, issue),
1743            LayoutError::UpgradeViolation {
1744                caixa: c.nome().to_string(),
1745                issue: issue.to_string(),
1746            },
1747        );
1748    }
1749
1750    #[test]
1751    fn restart_window_violation_ctor_matches_struct_literal_wrap() {
1752        let (c, issue) = layout_violation_ctor_fixture();
1753        assert_violation_ctor_matches(
1754            LayoutError::restart_window_violation(&c, issue),
1755            LayoutError::RestartWindowViolation {
1756                caixa: c.nome().to_string(),
1757                issue: issue.to_string(),
1758            },
1759        );
1760    }
1761
1762    #[test]
1763    fn supervisor_violation_ctor_matches_struct_literal_wrap() {
1764        let (c, issue) = layout_violation_ctor_fixture();
1765        assert_violation_ctor_matches(
1766            LayoutError::supervisor_violation(&c, issue),
1767            LayoutError::SupervisorViolation {
1768                caixa: c.nome().to_string(),
1769                issue: issue.to_string(),
1770            },
1771        );
1772    }
1773
1774    #[test]
1775    fn aplicacao_violation_ctor_matches_struct_literal_wrap() {
1776        let (c, issue) = layout_violation_ctor_fixture();
1777        assert_violation_ctor_matches(
1778            LayoutError::aplicacao_violation(&c, issue),
1779            LayoutError::AplicacaoViolation {
1780                caixa: c.nome().to_string(),
1781                issue: issue.to_string(),
1782            },
1783        );
1784    }
1785
1786    #[test]
1787    fn violation_ctor_routes_issue_through_display_impl() {
1788        // Pin the fold's `issue = err.to_string()` half against any type
1789        // implementing `Display` — a per-arm error type from a foreign
1790        // module (here, `std::io::Error`) threads through byte-equal to
1791        // the struct-literal `.to_string()` construction, so the fold
1792        // does not silently collapse onto `&str`-only inputs.
1793        let c = caixa(CaixaKind::Biblioteca);
1794        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "sample io display source");
1795        let expected_issue = io_err.to_string();
1796        let actual = LayoutError::deps_violation(&c, &io_err);
1797        assert_eq!(
1798            actual,
1799            LayoutError::DepsViolation {
1800                caixa: c.nome().to_string(),
1801                issue: expected_issue,
1802            },
1803        );
1804    }
1805
1806    #[test]
1807    fn violation_ctor_routes_caixa_prefix_through_nome_accessor() {
1808        // Pin the fold's `caixa = caixa.nome().to_string()` half against
1809        // a non-default `:nome` — the accessor threads the caller's
1810        // `:nome` verbatim into the wrap envelope, so the fold does not
1811        // silently collapse onto the default `"demo"` fixture nome.
1812        let mut c = caixa(CaixaKind::Biblioteca);
1813        c.nome = "alt-nome".into();
1814        let actual = LayoutError::behavior_violation(&c, "sample issue");
1815        assert_eq!(
1816            actual,
1817            LayoutError::BehaviorViolation {
1818                caixa: "alt-nome".to_string(),
1819                issue: "sample issue".to_string(),
1820            },
1821        );
1822    }
1823
1824    // ── LayoutError kind-coherence constructor family ────────────────────
1825    //
1826    // The [`layout_slot_kind_ctors!`] macro (sibling of
1827    // [`layout_violation_ctors!`] beside the `LayoutError` enum definition)
1828    // generates one static constructor per `*SlotsOn*` / `ForeignCodeSlot`
1829    // variant that folds the uniform `{ caixa: caixa.nome().to_string(),
1830    // kind: caixa.kind(), slots: slots.join(" ") }` four-field construction
1831    // onto one substrate primitive. The per-variant equivalence pins below
1832    // (fail-before-pass-after by construction — a byte-mismatched macro arm
1833    // would trip its equivalence pin first) lock each generated constructor
1834    // to its struct-literal peer under `PartialEq`, so every wire-up in
1835    // [`StandardLayout::verify`] on that variant produces a byte-equal
1836    // `LayoutError` to the pre-lift open-coded block. The three cross-axis
1837    // pins that follow (non-default `:nome`, non-default kind, non-trivial
1838    // slots list) route each of the three constructor input axes through
1839    // its declared accessor / arg, so the fold does not silently collapse
1840    // onto a fixture default on any axis.
1841
1842    fn layout_slot_kind_ctor_fixture() -> (Caixa, Vec<&'static str>) {
1843        (caixa(CaixaKind::Biblioteca), vec![":membros", ":contratos"])
1844    }
1845
1846    // Same rationale as `assert_violation_ctor_matches` above: the helper
1847    // terminates on the equality check, so the owned-arg lint's general
1848    // API-shape target does not apply.
1849    #[allow(clippy::needless_pass_by_value)]
1850    fn assert_slot_kind_ctor_matches(actual: LayoutError, expected: LayoutError) {
1851        assert_eq!(
1852            actual, expected,
1853            "generated constructor must produce byte-equal LayoutError to open-coded struct-literal wrap",
1854        );
1855    }
1856
1857    #[test]
1858    fn mesh_slots_on_non_aplicacao_ctor_matches_struct_literal_wrap() {
1859        let (c, slots) = layout_slot_kind_ctor_fixture();
1860        assert_slot_kind_ctor_matches(
1861            LayoutError::mesh_slots_on_non_aplicacao(&c, slots.clone()),
1862            LayoutError::MeshSlotsOnNonAplicacao {
1863                caixa: c.nome().to_string(),
1864                kind: c.kind(),
1865                slots: slots.join(" "),
1866            },
1867        );
1868    }
1869
1870    #[test]
1871    fn supervisor_slots_on_non_supervisor_ctor_matches_struct_literal_wrap() {
1872        let (c, slots) = layout_slot_kind_ctor_fixture();
1873        assert_slot_kind_ctor_matches(
1874            LayoutError::supervisor_slots_on_non_supervisor(&c, slots.clone()),
1875            LayoutError::SupervisorSlotsOnNonSupervisor {
1876                caixa: c.nome().to_string(),
1877                kind: c.kind(),
1878                slots: slots.join(" "),
1879            },
1880        );
1881    }
1882
1883    #[test]
1884    fn servico_slots_on_non_servico_ctor_matches_struct_literal_wrap() {
1885        let (c, slots) = layout_slot_kind_ctor_fixture();
1886        assert_slot_kind_ctor_matches(
1887            LayoutError::servico_slots_on_non_servico(&c, slots.clone()),
1888            LayoutError::ServicoSlotsOnNonServico {
1889                caixa: c.nome().to_string(),
1890                kind: c.kind(),
1891                slots: slots.join(" "),
1892            },
1893        );
1894    }
1895
1896    #[test]
1897    fn foreign_code_slot_ctor_matches_struct_literal_wrap() {
1898        let (c, slots) = layout_slot_kind_ctor_fixture();
1899        assert_slot_kind_ctor_matches(
1900            LayoutError::foreign_code_slot(&c, slots.clone()),
1901            LayoutError::ForeignCodeSlot {
1902                caixa: c.nome().to_string(),
1903                kind: c.kind(),
1904                slots: slots.join(" "),
1905            },
1906        );
1907    }
1908
1909    #[test]
1910    fn slot_kind_ctor_routes_caixa_prefix_through_nome_accessor() {
1911        // Pin the fold's `caixa = caixa.nome().to_string()` half against a
1912        // non-default `:nome` — the accessor threads the caller's `:nome`
1913        // verbatim into the wrap envelope, so the fold does not silently
1914        // collapse onto the default `"demo"` fixture nome. Peer of the
1915        // sibling `violation_ctor_routes_caixa_prefix_through_nome_accessor`
1916        // pin on the `{ caixa, issue }` envelope; extended here onto the
1917        // `{ caixa, kind, slots }` envelope so both `LayoutError`-shape
1918        // constructor families guarantee the `:nome`-derived-caixa slot
1919        // routes through [`Caixa::nome`] rather than a hard-coded string.
1920        let mut c = caixa(CaixaKind::Biblioteca);
1921        c.nome = "alt-nome".into();
1922        let actual = LayoutError::mesh_slots_on_non_aplicacao(&c, vec![":membros"]);
1923        assert_eq!(
1924            actual,
1925            LayoutError::MeshSlotsOnNonAplicacao {
1926                caixa: "alt-nome".to_string(),
1927                kind: CaixaKind::Biblioteca,
1928                slots: ":membros".to_string(),
1929            },
1930        );
1931    }
1932
1933    #[test]
1934    fn slot_kind_ctor_routes_kind_through_caixa_kind_accessor() {
1935        // Pin the fold's `kind = caixa.kind()` half against a non-default
1936        // kind — the accessor threads the caller's `:kind` verbatim into
1937        // the wrap envelope, so the fold does not silently collapse onto
1938        // one hard-coded kind. Sweeps every non-Aplicacao / non-Supervisor
1939        // / non-Servico kind the corresponding gate can fire on so the
1940        // pin covers the kind-derivation axis on every downstream variant.
1941        for kind in [
1942            CaixaKind::Biblioteca,
1943            CaixaKind::Binario,
1944            CaixaKind::Servico,
1945            CaixaKind::Supervisor,
1946            CaixaKind::Aplicacao,
1947            CaixaKind::Acao,
1948        ] {
1949            let c = caixa(kind);
1950            let actual = LayoutError::foreign_code_slot(&c, vec![":exe"]);
1951            assert_eq!(
1952                actual,
1953                LayoutError::ForeignCodeSlot {
1954                    caixa: c.nome().to_string(),
1955                    kind,
1956                    slots: ":exe".to_string(),
1957                },
1958                "foreign_code_slot ctor must thread `caixa.kind()` verbatim on every kind",
1959            );
1960        }
1961    }
1962
1963    #[test]
1964    fn slot_kind_ctor_routes_slots_through_join_separator() {
1965        // Pin the fold's `slots = slots.join(" ")` half against a
1966        // multi-entry slots list — the join threads exactly one ASCII
1967        // space between entries, in caller-supplied order, so the fold
1968        // does not silently collapse onto a fixed separator (`", "`, `";
1969        // "`, `"\n"`), a sorted order, or a single-entry pass-through.
1970        // Uses the M2 servico-slot vocabulary since these are what the
1971        // corresponding `servico_slots_on_non_servico` gate reports.
1972        let c = caixa(CaixaKind::Biblioteca);
1973        let actual = LayoutError::servico_slots_on_non_servico(
1974            &c,
1975            vec![":limits", ":behavior", ":upgrade-from"],
1976        );
1977        assert_eq!(
1978            actual,
1979            LayoutError::ServicoSlotsOnNonServico {
1980                caixa: c.nome().to_string(),
1981                kind: c.kind(),
1982                slots: ":limits :behavior :upgrade-from".to_string(),
1983            },
1984        );
1985    }
1986
1987    #[test]
1988    fn biblioteca_needs_default_lib_path() {
1989        let root = PathBuf::from("/tmp/x");
1990        let expect_manifest = root.join("caixa.lisp");
1991        let layout = StandardLayout::new().with_path_exists(move |p| p == expect_manifest);
1992        let err = layout
1993            .verify(&caixa(CaixaKind::Biblioteca), &root)
1994            .unwrap_err();
1995        assert!(matches!(err, LayoutError::MissingLib { .. }));
1996    }
1997
1998    #[test]
1999    fn biblioteca_passes_when_default_lib_exists() {
2000        let root = PathBuf::from("/tmp/x");
2001        let manifest = root.join("caixa.lisp");
2002        let default_lib = root.join("lib").join("demo.lisp");
2003        let layout =
2004            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2005        layout
2006            .verify(&caixa(CaixaKind::Biblioteca), &root)
2007            .expect("should pass");
2008    }
2009
2010    #[test]
2011    fn binario_without_exe_errors() {
2012        let root = PathBuf::from("/tmp/x");
2013        let manifest = root.join("caixa.lisp");
2014        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
2015        let err = layout
2016            .verify(&caixa(CaixaKind::Binario), &root)
2017            .unwrap_err();
2018        assert!(matches!(err, LayoutError::BinarioWithoutExe(_)));
2019    }
2020
2021    #[test]
2022    fn exe_outside_dir_errors() {
2023        // A relative entry that lives under the caixa root but *not*
2024        // under `exe/` — the canonical case the `starts_with(exe_dir)`
2025        // fence catches. The prior parent-escape shape this test used
2026        // (`"../sibling/tool"`) is now caught at validate time by
2027        // [`Caixa::validate_code_paths`] with the narrower
2028        // [`crate::ManifestError::CodePathParentEscape`] diagnostic
2029        // (see the layout-level integration pin
2030        // `code_path_violation_on_parent_escape_fires_before_existence_check`),
2031        // so this fence pin uses a non-`..` non-absolute shape outside
2032        // `exe/` to preserve coverage of the ExeOutsideDir surface.
2033        let root = PathBuf::from("/tmp/x");
2034        let manifest = root.join("caixa.lisp");
2035        let outside = root.join("lib/tool");
2036        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == outside);
2037        let mut c = caixa(CaixaKind::Binario);
2038        c.exe = vec!["lib/tool".into()];
2039        let err = layout.verify(&c, &root).unwrap_err();
2040        assert!(matches!(err, LayoutError::ExeOutsideDir(_)));
2041    }
2042
2043    // ── code-path shape gate (lifted to layout-level verify) ─────────────
2044
2045    #[test]
2046    fn code_path_violation_on_empty_bibliotecas_entry() {
2047        let root = PathBuf::from("/tmp/x");
2048        let manifest = root.join("caixa.lisp");
2049        let default_lib = root.join("lib").join("demo.lisp");
2050        let layout =
2051            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2052        let mut c = caixa(CaixaKind::Biblioteca);
2053        c.bibliotecas = vec![String::new()];
2054        let err = layout.verify(&c, &root).unwrap_err();
2055        // The wire-up wraps `ManifestError` Display into the
2056        // CodePathViolation envelope (peer of LimitsViolation /
2057        // BehaviorViolation / UpgradeViolation), so the issue string
2058        // names the offending slot at the source.
2059        let LayoutError::CodePathViolation { caixa, issue } = err else {
2060            panic!("expected LayoutError::CodePathViolation, got {err:?}");
2061        };
2062        assert_eq!(caixa, "demo");
2063        assert!(
2064            issue.contains(":bibliotecas"),
2065            "issue must name the offending slot: {issue}",
2066        );
2067    }
2068
2069    #[test]
2070    fn code_path_violation_on_absolute_servicos_entry() {
2071        let root = PathBuf::from("/tmp/x");
2072        let manifest = root.join("caixa.lisp");
2073        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
2074        let mut c = caixa(CaixaKind::Servico);
2075        c.servicos = vec!["/etc/servicos/escape.yaml".into()];
2076        let err = layout.verify(&c, &root).unwrap_err();
2077        let LayoutError::CodePathViolation { caixa, issue } = err else {
2078            panic!("expected LayoutError::CodePathViolation, got {err:?}");
2079        };
2080        assert_eq!(caixa, "demo");
2081        assert!(
2082            issue.contains(":servicos"),
2083            "issue must name the offending slot: {issue}",
2084        );
2085        assert!(
2086            issue.contains("/etc/servicos/escape.yaml"),
2087            "issue must quote the offending path: {issue}",
2088        );
2089    }
2090
2091    #[test]
2092    fn code_path_violation_on_parent_escape_fires_before_existence_check() {
2093        // The new gate runs BEFORE the existence loops, so a
2094        // parent-escaping `:exe` entry surfaces CodePathViolation
2095        // (naming `:exe` at the source) rather than the downstream
2096        // ExeOutsideDir / MissingEntry against the resolved sandbox-
2097        // escape path. Even if the resolved escape target exists
2098        // on disk (which we simulate here by claiming it does), the
2099        // shape diagnostic wins.
2100        let root = PathBuf::from("/tmp/x");
2101        let manifest = root.join("caixa.lisp");
2102        let resolved_escape = root.join("exe/../../escape.lisp");
2103        let layout =
2104            StandardLayout::new().with_path_exists(move |p| p == manifest || p == resolved_escape);
2105        let mut c = caixa(CaixaKind::Binario);
2106        c.exe = vec!["exe/../../escape.lisp".into()];
2107        let err = layout.verify(&c, &root).unwrap_err();
2108        let LayoutError::CodePathViolation { caixa, issue } = err else {
2109            panic!("expected LayoutError::CodePathViolation, got {err:?}");
2110        };
2111        assert_eq!(caixa, "demo");
2112        assert!(
2113            issue.contains(":exe"),
2114            "issue must name the offending slot: {issue}",
2115        );
2116    }
2117
2118    // ── etiquetas universal-axis gate wired into verify ─────────────────
2119    //
2120    // Pins the layout-pipeline wire-up of [`Caixa::validate_etiquetas`]:
2121    // the fourth universal-axis Caixa-level value-shape gate (peer of
2122    // `validate_nome` / `validate_versao` / `validate_deps` /
2123    // `validate_code_paths`), wired before the kind-coherence gates so
2124    // a structurally-invalid `:etiquetas` entry on any kind surfaces
2125    // the per-axis `EtiquetasViolation { caixa, issue }` envelope at
2126    // the source rather than silently rendering as `keywords: [""]`
2127    // in `Chart.yaml` (Servico kind, via caixa-helm's `BTreeSet`
2128    // collect) or silently dedup'ing at chart render (every kind).
2129    // Until this wire-up landed `:etiquetas` had no shape gate at any
2130    // layer — the registry-search-tag axis was the largest universal
2131    // authoring surface on the typed Caixa surface with no validate
2132    // discipline.
2133    //
2134    // Same per-axis `*Violation { caixa, issue }` envelope every peer
2135    // per-axis wrap exposes; the wire-up runs after `validate_deps`
2136    // (universal axis ordering: `:nome` → `:versao` → `:deps` →
2137    // `:etiquetas`) and before every kind-coherence gate
2138    // (`:etiquetas` is universal so its shape diagnostic is more
2139    // fundamental than the partition-on-kind diagnostics).
2140
2141    #[test]
2142    fn etiquetas_violation_on_empty_entry() {
2143        // Canonical paste-from-blank-doc footgun on every kind. The
2144        // wrap envelope wraps [`ManifestError::EtiquetaEmpty`]'s
2145        // Display through verbatim, so the issue string names the
2146        // offending `:etiquetas` axis at the source — the author can
2147        // grep their caixa.lisp for `:etiquetas` and fix the empty
2148        // entry in one edit. Mirrors the peer
2149        // `code_path_violation_on_empty_bibliotecas_entry` shape
2150        // (b868442) on the `:bibliotecas` axis.
2151        let root = PathBuf::from("/tmp/x");
2152        let manifest = root.join("caixa.lisp");
2153        let default_lib = root.join("lib").join("demo.lisp");
2154        let layout =
2155            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2156        let mut c = caixa(CaixaKind::Biblioteca);
2157        c.etiquetas = vec![String::new()];
2158        let err = layout.verify(&c, &root).unwrap_err();
2159        let LayoutError::EtiquetasViolation { caixa, issue } = err else {
2160            panic!("expected LayoutError::EtiquetasViolation, got {err:?}");
2161        };
2162        assert_eq!(caixa, "demo");
2163        assert!(
2164            issue.contains(":etiquetas"),
2165            "issue must name the offending slot: {issue}",
2166        );
2167    }
2168
2169    #[test]
2170    fn etiquetas_violation_on_duplicate_entry() {
2171        // Canonical copy-paste-the-wrong-tag footgun. Without the wire-
2172        // up the duplicate was silently dedup'd by caixa-helm's
2173        // `BTreeSet` collect at chart render — a "second wins / one
2174        // silently disappears" shape. The wrap envelope names the
2175        // offending tag verbatim through the inner
2176        // [`ManifestError::EtiquetaDuplicate`]'s Display.
2177        let root = PathBuf::from("/tmp/x");
2178        let manifest = root.join("caixa.lisp");
2179        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
2180        let mut c = caixa(CaixaKind::Servico);
2181        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
2182        c.etiquetas = vec!["demo".into(), "demo".into()];
2183        // The servicos path doesn't exist in this fixture, but the
2184        // `:etiquetas` gate fires before the existence loop (universal
2185        // axis dominates kind-specific existence checks). Wire is
2186        // intact iff the wrap envelope surfaces first.
2187        let err = layout.verify(&c, &root).unwrap_err();
2188        let LayoutError::EtiquetasViolation { caixa, issue } = err else {
2189            panic!("expected LayoutError::EtiquetasViolation, got {err:?}");
2190        };
2191        assert_eq!(caixa, "demo");
2192        assert!(
2193            issue.contains("demo"),
2194            "issue must quote the offending tag: {issue}",
2195        );
2196    }
2197
2198    #[test]
2199    fn etiquetas_violation_fires_before_kind_coherence_mesh_slot() {
2200        // Cross-axis precedence pin: a Biblioteca with malformed
2201        // `:etiquetas` *and* declared mesh slots (`:membros`) surfaces
2202        // the universal `:etiquetas` diagnostic first, not the
2203        // kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
2204        // `:etiquetas` is universal (every kind owns the slot), so its
2205        // shape diagnostic is more fundamental than the partition-on-
2206        // kind diagnostic. Mirrors the peer
2207        // `deps_violation_fires_before_*` precedence pins (aa77d0f) on
2208        // the universal `:deps` axis vs the same kind-coherence gates.
2209        let root = PathBuf::from("/tmp/x");
2210        let manifest = root.join("caixa.lisp");
2211        let default_lib = root.join("lib").join("demo.lisp");
2212        let layout =
2213            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2214        let mut c = caixa(CaixaKind::Biblioteca);
2215        c.etiquetas = vec![String::new()];
2216        c.membros = vec![crate::aplicacao::Membro {
2217            caixa: "x".into(),
2218            versao: "^0.1".into(),
2219        }];
2220        let err = layout.verify(&c, &root).unwrap_err();
2221        assert!(
2222            matches!(err, LayoutError::EtiquetasViolation { .. }),
2223            "got {err:?}",
2224        );
2225    }
2226
2227    #[test]
2228    fn etiquetas_violation_fires_after_deps_violation() {
2229        // Cross-axis precedence pin (inside the universal-axis trio):
2230        // a caixa with both a malformed `:deps` entry *and* a malformed
2231        // `:etiquetas` entry surfaces `DepsViolation` first — `:deps`
2232        // is the third universal axis in declaration order
2233        // (`:nome` → `:versao` → `:deps` → `:etiquetas`) and runs first
2234        // in `verify`. Mirrors the peer
2235        // `nome_violation_fires_before_versao_violation` shape on the
2236        // identity-axis pair.
2237        let root = PathBuf::from("/tmp/x");
2238        let manifest = root.join("caixa.lisp");
2239        let default_lib = root.join("lib").join("demo.lisp");
2240        let layout =
2241            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2242        let mut c = caixa(CaixaKind::Biblioteca);
2243        c.deps = vec![crate::Dep::simple("Caixa-Teia", "^0.1")]; // uppercase :nome
2244        c.etiquetas = vec![String::new()];
2245        let err = layout.verify(&c, &root).unwrap_err();
2246        assert!(
2247            matches!(err, LayoutError::DepsViolation { .. }),
2248            "got {err:?}",
2249        );
2250    }
2251
2252    #[test]
2253    fn etiquetas_violation_accepts_canonical_template() {
2254        // Positive control sanity pin: the canonical `Caixa::template`
2255        // shape (`:etiquetas ()` — empty list) passes the gate
2256        // trivially. Mirrors the peer
2257        // `validate_code_paths_accepts_canonical_template` pin.
2258        let root = PathBuf::from("/tmp/x");
2259        let manifest = root.join("caixa.lisp");
2260        let default_lib = root.join("lib").join("demo.lisp");
2261        let layout =
2262            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2263        let c = caixa(CaixaKind::Biblioteca);
2264        layout.verify(&c, &root).expect("template must pass");
2265    }
2266
2267    #[test]
2268    fn etiquetas_violation_on_non_chart_keyword_shape() {
2269        // Canonical CSV-list-separator-confusion footgun: the author
2270        // confused the CSV-style separator with the `:etiquetas` list
2271        // grammar. The shape gate fires past the empty + duplicate
2272        // arms via [`Caixa::validate_etiquetas`]'s new
2273        // `is_chart_keyword_shape` cascade, and the layout envelope
2274        // wraps [`ManifestError::EtiquetaInvalid`]'s Display through
2275        // verbatim — the issue string names both the offending slot
2276        // and the offending value (debug-escaped). Peer with the
2277        // `autores_violation_on_non_chart_maintainer_shape` pin on
2278        // the sibling universal-axis `Vec<String>` surface — the
2279        // second layout pin on the Vec<String> per-entry shape
2280        // cascade.
2281        let root = PathBuf::from("/tmp/x");
2282        let manifest = root.join("caixa.lisp");
2283        let default_lib = root.join("lib").join("demo.lisp");
2284        let layout =
2285            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2286        let mut c = caixa(CaixaKind::Biblioteca);
2287        c.etiquetas = vec!["mesh,http,grpc".into()];
2288        let err = layout.verify(&c, &root).unwrap_err();
2289        let LayoutError::EtiquetasViolation { caixa, issue } = err else {
2290            panic!("expected LayoutError::EtiquetasViolation, got {err:?}");
2291        };
2292        assert_eq!(caixa, "demo");
2293        assert!(
2294            issue.contains(":etiquetas"),
2295            "issue must name the offending slot: {issue}",
2296        );
2297        assert!(
2298            issue.contains("mesh,http,grpc"),
2299            "issue must quote the offending value: {issue}",
2300        );
2301    }
2302
2303    // ── autores universal-axis gate wired into verify ───────────────────
2304    //
2305    // Pins the layout-pipeline wire-up of [`Caixa::validate_autores`]:
2306    // the fifth universal-axis Caixa-level value-shape gate (peer of
2307    // `validate_nome` / `validate_versao` / `validate_deps` /
2308    // `validate_etiquetas` / `validate_code_paths`), wired immediately
2309    // after `validate_etiquetas` so the two Vec-shaped universal
2310    // metadata axes sit adjacent in the cascade. Until this wire-up
2311    // landed `:autores` had no shape gate at any layer — the
2312    // maintainer-axis was the second largest universal authoring
2313    // surface on the typed Caixa surface with no validate discipline,
2314    // and unlike `:etiquetas` (caixa-helm dedups the rendered
2315    // `keywords:` array via `BTreeSet` collect at chart render),
2316    // `maintainers:` has *no* renderer-side dedup, so duplicate
2317    // `:autores` entries render verbatim as two identical
2318    // `Maintainer { name, email: None }` records — a strictly worse
2319    // footgun than the peer `:etiquetas` shape.
2320
2321    #[test]
2322    fn autores_violation_on_empty_entry() {
2323        // Canonical paste-from-blank-doc footgun on every kind. The
2324        // wrap envelope wraps [`ManifestError::AutorEmpty`]'s Display
2325        // through verbatim, so the issue string names the offending
2326        // `:autores` axis at the source — the author can grep their
2327        // caixa.lisp for `:autores` and fix the empty entry in one
2328        // edit. Mirrors the peer `etiquetas_violation_on_empty_entry`
2329        // shape (360a499) on the `:etiquetas` axis.
2330        let root = PathBuf::from("/tmp/x");
2331        let manifest = root.join("caixa.lisp");
2332        let default_lib = root.join("lib").join("demo.lisp");
2333        let layout =
2334            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2335        let mut c = caixa(CaixaKind::Biblioteca);
2336        c.autores = vec![String::new()];
2337        let err = layout.verify(&c, &root).unwrap_err();
2338        let LayoutError::AutoresViolation { caixa, issue } = err else {
2339            panic!("expected LayoutError::AutoresViolation, got {err:?}");
2340        };
2341        assert_eq!(caixa, "demo");
2342        assert!(
2343            issue.contains(":autores"),
2344            "issue must name the offending slot: {issue}",
2345        );
2346    }
2347
2348    #[test]
2349    fn autores_violation_on_duplicate_entry() {
2350        // Canonical copy-paste-the-wrong-author footgun. Unlike the
2351        // peer `:etiquetas` axis (silently dedup'd by caixa-helm's
2352        // `BTreeSet` collect at chart render), `:autores` duplicates
2353        // stack verbatim in the rendered `maintainers:` — the gate
2354        // closes the footgun at validate time before any renderer
2355        // sees it. The wrap envelope names the offending author
2356        // verbatim through the inner [`ManifestError::AutorDuplicate`]'s
2357        // Display.
2358        let root = PathBuf::from("/tmp/x");
2359        let manifest = root.join("caixa.lisp");
2360        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
2361        let mut c = caixa(CaixaKind::Servico);
2362        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
2363        c.autores = vec!["pleme-io".into(), "pleme-io".into()];
2364        // The servicos path doesn't exist in this fixture, but the
2365        // `:autores` gate fires before the existence loop (universal
2366        // axis dominates kind-specific existence checks). Wire is
2367        // intact iff the wrap envelope surfaces first.
2368        let err = layout.verify(&c, &root).unwrap_err();
2369        let LayoutError::AutoresViolation { caixa, issue } = err else {
2370            panic!("expected LayoutError::AutoresViolation, got {err:?}");
2371        };
2372        assert_eq!(caixa, "demo");
2373        assert!(
2374            issue.contains("pleme-io"),
2375            "issue must quote the offending author: {issue}",
2376        );
2377    }
2378
2379    #[test]
2380    fn autores_violation_fires_before_kind_coherence_mesh_slot() {
2381        // Cross-axis precedence pin: a Biblioteca with malformed
2382        // `:autores` *and* declared mesh slots (`:membros`) surfaces
2383        // the universal `:autores` diagnostic first, not the
2384        // kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
2385        // `:autores` is universal (every kind owns the slot), so its
2386        // shape diagnostic is more fundamental than the partition-on-
2387        // kind diagnostic. Mirrors the peer
2388        // `etiquetas_violation_fires_before_kind_coherence_mesh_slot`
2389        // pin (360a499) on the `:etiquetas` axis vs the same kind-
2390        // coherence gates.
2391        let root = PathBuf::from("/tmp/x");
2392        let manifest = root.join("caixa.lisp");
2393        let default_lib = root.join("lib").join("demo.lisp");
2394        let layout =
2395            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2396        let mut c = caixa(CaixaKind::Biblioteca);
2397        c.autores = vec![String::new()];
2398        c.membros = vec![crate::aplicacao::Membro {
2399            caixa: "x".into(),
2400            versao: "^0.1".into(),
2401        }];
2402        let err = layout.verify(&c, &root).unwrap_err();
2403        assert!(
2404            matches!(err, LayoutError::AutoresViolation { .. }),
2405            "got {err:?}",
2406        );
2407    }
2408
2409    #[test]
2410    fn autores_violation_fires_after_etiquetas_violation() {
2411        // Cross-axis precedence pin (inside the Vec-shaped universal
2412        // metadata pair): a caixa with both a malformed `:etiquetas`
2413        // entry *and* a malformed `:autores` entry surfaces
2414        // `EtiquetasViolation` first — `:etiquetas` is the fourth
2415        // universal axis in the cascade and runs before `:autores`,
2416        // peer with the canonical identity-axis-first cascade the
2417        // peer gates establish. Mirrors the peer
2418        // `etiquetas_violation_fires_after_deps_violation` precedence
2419        // pin (360a499) on the dep-axis-before-tag-axis pair.
2420        let root = PathBuf::from("/tmp/x");
2421        let manifest = root.join("caixa.lisp");
2422        let default_lib = root.join("lib").join("demo.lisp");
2423        let layout =
2424            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2425        let mut c = caixa(CaixaKind::Biblioteca);
2426        c.etiquetas = vec![String::new()];
2427        c.autores = vec![String::new()];
2428        let err = layout.verify(&c, &root).unwrap_err();
2429        assert!(
2430            matches!(err, LayoutError::EtiquetasViolation { .. }),
2431            "got {err:?}",
2432        );
2433    }
2434
2435    #[test]
2436    fn autores_violation_accepts_canonical_template() {
2437        // Positive control sanity pin: the canonical `Caixa::template`
2438        // shape (`:autores ()` — empty list) passes the gate trivially.
2439        // Mirrors the peer `etiquetas_violation_accepts_canonical_template`
2440        // pin (360a499).
2441        let root = PathBuf::from("/tmp/x");
2442        let manifest = root.join("caixa.lisp");
2443        let default_lib = root.join("lib").join("demo.lisp");
2444        let layout =
2445            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2446        let c = caixa(CaixaKind::Biblioteca);
2447        layout.verify(&c, &root).expect("template must pass");
2448    }
2449
2450    #[test]
2451    fn autores_violation_on_non_chart_maintainer_shape() {
2452        // Canonical paste-from-multiline-doc footgun: the author
2453        // pasted a multi-line block of author records into one
2454        // `:autores` entry instead of splitting into one entry per
2455        // author. The shape gate fires past the empty + duplicate arms
2456        // via [`Caixa::validate_autores`]'s new
2457        // `is_chart_maintainer_name_shape` cascade, and the layout
2458        // envelope wraps [`ManifestError::AutorInvalid`]'s Display
2459        // through verbatim — the issue string names both the offending
2460        // slot and the offending value (debug-escaped). Peer with the
2461        // `descricao_violation_on_non_chart_shape` pin on the sibling
2462        // universal-axis `Option<String>` surface and the
2463        // `licenca_violation_on_non_spdx_shape` /
2464        // `edicao_violation_on_non_year_shape` peers — and the first
2465        // layout pin on the Vec<String> per-entry shape cascade.
2466        let root = PathBuf::from("/tmp/x");
2467        let manifest = root.join("caixa.lisp");
2468        let default_lib = root.join("lib").join("demo.lisp");
2469        let layout =
2470            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2471        let mut c = caixa(CaixaKind::Biblioteca);
2472        c.autores = vec!["alice\nbob".into()];
2473        let err = layout.verify(&c, &root).unwrap_err();
2474        let LayoutError::AutoresViolation { caixa, issue } = err else {
2475            panic!("expected LayoutError::AutoresViolation, got {err:?}");
2476        };
2477        assert_eq!(caixa, "demo");
2478        assert!(
2479            issue.contains(":autores"),
2480            "issue must name the offending slot: {issue}",
2481        );
2482        assert!(
2483            issue.contains("alice\\nbob"),
2484            "issue must quote the offending value (debug-escaped): {issue}",
2485        );
2486    }
2487
2488    // ── repositorio universal-axis gate wired into verify ────────────────
2489    //
2490    // Pins the layout-pipeline wire-up of [`Caixa::validate_repositorio`]:
2491    // the sixth universal-axis Caixa-level value-shape gate (peer of
2492    // `validate_nome` / `validate_versao` / `validate_deps` /
2493    // `validate_etiquetas` / `validate_autores` / `validate_code_paths`),
2494    // wired immediately after `validate_autores` so the universal
2495    // git-URL axis sits adjacent to the two Vec-shaped universal
2496    // metadata axes (`:etiquetas`, `:autores`) in the cascade. Until
2497    // this wire-up landed `:repositorio` had no shape gate at any
2498    // layer — the universal git-shaped homepage axis was the third
2499    // largest universal authoring surface on the typed Caixa with no
2500    // validate discipline, routing the same string through two
2501    // load-bearing substrate consumers (`caixa-helm`'s `Chart.yaml
2502    // home:` field and `caixa-flux`'s FluxCD `GitRepository.spec.url`)
2503    // via `Option::unwrap_or_else` fallbacks that only fire on `None` —
2504    // a `Some("")` silently passed every fallback and rendered as an
2505    // empty URL in both consumers, breaking at `helm template` /
2506    // FluxCD reconcile time far from the source `caixa.lisp`. The
2507    // gate closes the divergence and makes the two `git URL`-shaped
2508    // surfaces on the typed Caixa (`:repositorio` here, `:deps :fonte
2509    // :repo` peer routed through the same shared
2510    // `crate::render::is_git_repo_url` predicate) structurally
2511    // equivalent by construction.
2512
2513    #[test]
2514    fn repositorio_violation_on_empty_some() {
2515        // Canonical paste-from-blank-doc footgun on every kind. The
2516        // wrap envelope wraps [`ManifestError::RepositorioEmpty`]'s
2517        // Display through verbatim, so the issue string names the
2518        // offending `:repositorio` axis at the source — the author
2519        // can grep their caixa.lisp for `:repositorio ""` and fix the
2520        // empty value in one edit. Mirrors the peer
2521        // `autores_violation_on_empty_entry` shape (86c769b) on the
2522        // `:autores` axis.
2523        let root = PathBuf::from("/tmp/x");
2524        let manifest = root.join("caixa.lisp");
2525        let default_lib = root.join("lib").join("demo.lisp");
2526        let layout =
2527            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2528        let mut c = caixa(CaixaKind::Biblioteca);
2529        c.repositorio = Some(String::new());
2530        let err = layout.verify(&c, &root).unwrap_err();
2531        let LayoutError::RepositorioViolation { caixa, issue } = err else {
2532            panic!("expected LayoutError::RepositorioViolation, got {err:?}");
2533        };
2534        assert_eq!(caixa, "demo");
2535        assert!(
2536            issue.contains(":repositorio"),
2537            "issue must name the offending slot: {issue}",
2538        );
2539    }
2540
2541    #[test]
2542    fn repositorio_violation_on_malformed_shape() {
2543        // Canonical CLI-argument-injection footgun: a leading `-`
2544        // value (`-upload-pack=evil`) escapes the `git clone <repo>`
2545        // subprocess argument boundary at clone time. The shared
2546        // `is_git_repo_url` predicate — the same parser the peer
2547        // `:deps :fonte :repo` axis routes through via
2548        // `DepSource::validate` — refuses every leading-`-` shape at
2549        // validate time. The wrap envelope names the offending value
2550        // verbatim through the inner [`ManifestError::RepositorioInvalid`]'s
2551        // Display.
2552        let root = PathBuf::from("/tmp/x");
2553        let manifest = root.join("caixa.lisp");
2554        let default_lib = root.join("lib").join("demo.lisp");
2555        let layout =
2556            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2557        let mut c = caixa(CaixaKind::Biblioteca);
2558        c.repositorio = Some("-upload-pack=evil".into());
2559        let err = layout.verify(&c, &root).unwrap_err();
2560        let LayoutError::RepositorioViolation { caixa, issue } = err else {
2561            panic!("expected LayoutError::RepositorioViolation, got {err:?}");
2562        };
2563        assert_eq!(caixa, "demo");
2564        assert!(
2565            issue.contains("-upload-pack=evil"),
2566            "issue must quote the offending value: {issue}",
2567        );
2568    }
2569
2570    #[test]
2571    fn repositorio_violation_fires_before_kind_coherence_mesh_slot() {
2572        // Cross-axis precedence pin: a Biblioteca with malformed
2573        // `:repositorio` *and* declared mesh slots (`:membros`)
2574        // surfaces the universal `:repositorio` diagnostic first, not
2575        // the kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
2576        // `:repositorio` is universal (every kind owns the slot), so
2577        // its shape diagnostic is more fundamental than the
2578        // partition-on-kind diagnostic. Mirrors the peer
2579        // `autores_violation_fires_before_kind_coherence_mesh_slot`
2580        // pin (86c769b) on the `:autores` axis vs the same
2581        // kind-coherence gates.
2582        let root = PathBuf::from("/tmp/x");
2583        let manifest = root.join("caixa.lisp");
2584        let default_lib = root.join("lib").join("demo.lisp");
2585        let layout =
2586            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2587        let mut c = caixa(CaixaKind::Biblioteca);
2588        c.repositorio = Some(String::new());
2589        c.membros = vec![crate::aplicacao::Membro {
2590            caixa: "x".into(),
2591            versao: "^0.1".into(),
2592        }];
2593        let err = layout.verify(&c, &root).unwrap_err();
2594        assert!(
2595            matches!(err, LayoutError::RepositorioViolation { .. }),
2596            "got {err:?}",
2597        );
2598    }
2599
2600    #[test]
2601    fn repositorio_violation_fires_after_autores_violation() {
2602        // Cross-axis precedence pin (inside the universal metadata
2603        // trio): a caixa with both a malformed `:autores` entry *and*
2604        // a malformed `:repositorio` value surfaces `AutoresViolation`
2605        // first — `:autores` is the fifth universal axis in the
2606        // cascade and runs before `:repositorio`, peer with the
2607        // canonical identity-axis-first cascade the peer gates
2608        // establish. Mirrors the peer
2609        // `autores_violation_fires_after_etiquetas_violation`
2610        // precedence pin (86c769b) on the tag-axis-before-author-axis
2611        // pair.
2612        let root = PathBuf::from("/tmp/x");
2613        let manifest = root.join("caixa.lisp");
2614        let default_lib = root.join("lib").join("demo.lisp");
2615        let layout =
2616            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2617        let mut c = caixa(CaixaKind::Biblioteca);
2618        c.autores = vec![String::new()];
2619        c.repositorio = Some(String::new());
2620        let err = layout.verify(&c, &root).unwrap_err();
2621        assert!(
2622            matches!(err, LayoutError::AutoresViolation { .. }),
2623            "got {err:?}",
2624        );
2625    }
2626
2627    #[test]
2628    fn repositorio_violation_accepts_canonical_template() {
2629        // Positive control sanity pin: the canonical `Caixa::template`
2630        // shape (omits `:repositorio` entirely → `None` on the typed
2631        // surface) passes the gate trivially — the gate is a no-op
2632        // when the author didn't author a value. Mirrors the peer
2633        // `autores_violation_accepts_canonical_template` pin (86c769b).
2634        let root = PathBuf::from("/tmp/x");
2635        let manifest = root.join("caixa.lisp");
2636        let default_lib = root.join("lib").join("demo.lisp");
2637        let layout =
2638            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2639        let c = caixa(CaixaKind::Biblioteca);
2640        layout.verify(&c, &root).expect("template must pass");
2641    }
2642
2643    #[test]
2644    fn repositorio_violation_accepts_canonical_github_shorthand() {
2645        // Positive control pin on the canonical pleme-io `:repositorio`
2646        // shape: the `github:org/repo` shorthand the README quickstart
2647        // and the `caixa-helm` / `caixa-mesh` / `caixa-flux` fixtures
2648        // all use passes the gate end-to-end. Closes the structural
2649        // equivalence between this surface and the peer `:deps :fonte
2650        // :repo` axis — both consume `crate::render::is_git_repo_url`
2651        // and both must agree on the same accepted shape set.
2652        let root = PathBuf::from("/tmp/x");
2653        let manifest = root.join("caixa.lisp");
2654        let default_lib = root.join("lib").join("demo.lisp");
2655        let layout =
2656            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2657        let mut c = caixa(CaixaKind::Biblioteca);
2658        c.repositorio = Some("github:pleme-io/hello-rio".into());
2659        layout.verify(&c, &root).expect("canonical shape must pass");
2660    }
2661
2662    // ── descricao universal-axis gate wired into verify ──────────────────
2663    //
2664    // Pins the layout-pipeline wire-up of [`Caixa::validate_descricao`]:
2665    // the seventh universal-axis Caixa-level value-shape gate (peer of
2666    // `validate_nome` / `validate_versao` / `validate_deps` /
2667    // `validate_etiquetas` / `validate_autores` / `validate_repositorio` /
2668    // `validate_code_paths`), wired immediately after `validate_repositorio`
2669    // so the universal free-form-prose axis sits adjacent to the
2670    // universal git-URL axis in the cascade. Until this wire-up landed
2671    // `:descricao` had no shape gate at any layer — the empty
2672    // `Some("")` silently passed both `caixa-helm` consumers'
2673    // `Option::unwrap_or_else(|| <fallback>)` (which only fire on
2674    // `None`) and rendered as `Chart.yaml description: ""` plus a
2675    // blank `README.md` header, breaking at `helm lint` time
2676    // (`WARNING [chart.metadata.description]: description is required`
2677    // on `apiVersion: v2` charts) far from the source `caixa.lisp`.
2678    // Closes the same `Some("")` skips-`unwrap_or_else` footgun the
2679    // peer `:repositorio` gate (577b0a9) closed, on the universal
2680    // free-form-prose summary axis.
2681
2682    #[test]
2683    fn descricao_violation_on_empty_some() {
2684        // Canonical paste-from-blank-doc footgun on every kind. The
2685        // wrap envelope wraps [`ManifestError::DescricaoEmpty`]'s
2686        // Display through verbatim, so the issue string names the
2687        // offending `:descricao` axis at the source — the author can
2688        // grep their caixa.lisp for `:descricao ""` and fix the empty
2689        // value in one edit. Mirrors the peer
2690        // `repositorio_violation_on_empty_some` shape (577b0a9) on
2691        // the sibling `Option<String>` `:repositorio` axis.
2692        let root = PathBuf::from("/tmp/x");
2693        let manifest = root.join("caixa.lisp");
2694        let default_lib = root.join("lib").join("demo.lisp");
2695        let layout =
2696            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2697        let mut c = caixa(CaixaKind::Biblioteca);
2698        c.descricao = Some(String::new());
2699        let err = layout.verify(&c, &root).unwrap_err();
2700        let LayoutError::DescricaoViolation { caixa, issue } = err else {
2701            panic!("expected LayoutError::DescricaoViolation, got {err:?}");
2702        };
2703        assert_eq!(caixa, "demo");
2704        assert!(
2705            issue.contains(":descricao"),
2706            "issue must name the offending slot: {issue}",
2707        );
2708    }
2709
2710    #[test]
2711    fn descricao_violation_fires_before_kind_coherence_mesh_slot() {
2712        // Cross-axis precedence pin: a Biblioteca with empty
2713        // `:descricao` *and* declared mesh slots (`:membros`)
2714        // surfaces the universal `:descricao` diagnostic first, not
2715        // the kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
2716        // `:descricao` is universal (every kind owns the slot), so
2717        // its shape diagnostic is more fundamental than the
2718        // partition-on-kind diagnostic. Mirrors the peer
2719        // `repositorio_violation_fires_before_kind_coherence_mesh_slot`
2720        // pin (577b0a9) on the `:repositorio` axis vs the same
2721        // kind-coherence gates.
2722        let root = PathBuf::from("/tmp/x");
2723        let manifest = root.join("caixa.lisp");
2724        let default_lib = root.join("lib").join("demo.lisp");
2725        let layout =
2726            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2727        let mut c = caixa(CaixaKind::Biblioteca);
2728        c.descricao = Some(String::new());
2729        c.membros = vec![crate::aplicacao::Membro {
2730            caixa: "x".into(),
2731            versao: "^0.1".into(),
2732        }];
2733        let err = layout.verify(&c, &root).unwrap_err();
2734        assert!(
2735            matches!(err, LayoutError::DescricaoViolation { .. }),
2736            "got {err:?}",
2737        );
2738    }
2739
2740    #[test]
2741    fn descricao_violation_fires_after_repositorio_violation() {
2742        // Cross-axis precedence pin (inside the universal metadata
2743        // cascade): a caixa with both a malformed `:repositorio` *and*
2744        // an empty `:descricao` surfaces `RepositorioViolation`
2745        // first — `:repositorio` is the sixth universal axis in the
2746        // cascade and runs before `:descricao`, peer with the
2747        // canonical identity-axis-first cascade the peer gates
2748        // establish. Mirrors the peer
2749        // `repositorio_violation_fires_after_autores_violation`
2750        // precedence pin (577b0a9) on the autores-axis-before-
2751        // repositorio-axis pair.
2752        let root = PathBuf::from("/tmp/x");
2753        let manifest = root.join("caixa.lisp");
2754        let default_lib = root.join("lib").join("demo.lisp");
2755        let layout =
2756            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2757        let mut c = caixa(CaixaKind::Biblioteca);
2758        c.repositorio = Some(String::new());
2759        c.descricao = Some(String::new());
2760        let err = layout.verify(&c, &root).unwrap_err();
2761        assert!(
2762            matches!(err, LayoutError::RepositorioViolation { .. }),
2763            "got {err:?}",
2764        );
2765    }
2766
2767    #[test]
2768    fn descricao_violation_accepts_none() {
2769        // Positive control sanity pin: a caixa that omits
2770        // `:descricao` entirely (the canonical `Caixa::template` shape
2771        // carries `Some("FIXME — describe this caixa")`, but the
2772        // layout-test fixture defaults to `None`) passes the gate
2773        // trivially — the gate is a no-op when the author didn't
2774        // author a value. Mirrors the peer
2775        // `repositorio_violation_accepts_canonical_template` pin
2776        // (577b0a9).
2777        let root = PathBuf::from("/tmp/x");
2778        let manifest = root.join("caixa.lisp");
2779        let default_lib = root.join("lib").join("demo.lisp");
2780        let layout =
2781            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2782        let c = caixa(CaixaKind::Biblioteca);
2783        layout.verify(&c, &root).expect("None must pass");
2784    }
2785
2786    #[test]
2787    fn descricao_violation_accepts_canonical_summary() {
2788        // Positive control pin on the canonical pleme-io `:descricao`
2789        // shape: a short free-form prose summary the `caixa-helm` /
2790        // `caixa-flux` / `caixa-mesh` fixtures all carry passes the
2791        // gate end-to-end.
2792        let root = PathBuf::from("/tmp/x");
2793        let manifest = root.join("caixa.lisp");
2794        let default_lib = root.join("lib").join("demo.lisp");
2795        let layout =
2796            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2797        let mut c = caixa(CaixaKind::Biblioteca);
2798        c.descricao = Some("Canonical Rust→wasm32-wasip2 caixa Servico.".into());
2799        layout
2800            .verify(&c, &root)
2801            .expect("canonical summary must pass");
2802    }
2803
2804    #[test]
2805    fn descricao_violation_on_non_chart_shape() {
2806        // Shape-predicate wire-up pin: a malformed `:descricao` value
2807        // that's a non-empty `Some(s)` but carries a paste-from-
2808        // multiline-doc embedded newline surfaces the
2809        // `DescricaoViolation` envelope via the manifest-layer
2810        // `ManifestError::DescricaoInvalid` arm. Mirrors the peer
2811        // `descricao_violation_on_empty_some` shape on the empty arm
2812        // of the same axis and the peer
2813        // `licenca_violation_on_non_spdx_shape` shape on the sibling
2814        // `:licenca` axis. Until this gate landed a value like
2815        // `"Checkout\nflow."` (an embedded newline) or `"Checkout
2816        // flow. "` (a trailing whitespace) silently passed
2817        // `StandardLayout::verify` and landed in the rendered
2818        // Chart.yaml `description:` field as a YAML-illegal
2819        // multi-line scalar or a silently-trimmed whitespace
2820        // round-trip far from the source caixa.lisp.
2821        let root = PathBuf::from("/tmp/x");
2822        let manifest = root.join("caixa.lisp");
2823        let default_lib = root.join("lib").join("demo.lisp");
2824        let layout =
2825            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2826        let mut c = caixa(CaixaKind::Biblioteca);
2827        c.descricao = Some("Checkout\nflow.".into());
2828        let err = layout.verify(&c, &root).unwrap_err();
2829        let LayoutError::DescricaoViolation { caixa, issue } = err else {
2830            panic!("expected LayoutError::DescricaoViolation, got {err:?}");
2831        };
2832        assert_eq!(caixa, "demo");
2833        assert!(
2834            issue.contains(":descricao"),
2835            "issue must name the offending slot: {issue}",
2836        );
2837        // The wrapped `ManifestError::DescricaoInvalid` Display uses
2838        // `{descricao:?}` (Debug) so the embedded newline surfaces
2839        // debug-escaped as `\n` in the issue string.
2840        assert!(
2841            issue.contains("Checkout\\nflow."),
2842            "issue must quote the offending value (debug-escaped): {issue}",
2843        );
2844    }
2845
2846    // ── :licenca empty-Some shape wired into verify (universal axis) ──
2847    //
2848    // Until this wire-up landed `Caixa::validate_licenca` did not
2849    // exist — the universal SPDX-shaped license-expression axis had
2850    // no shape gate at any layer, so an empty `Some("")` silently
2851    // passed `Caixa::from_lisp` and `StandardLayout::verify` and
2852    // landed as a bare trailing period in the rendered
2853    // `lareira-<nome>` chart's `README.md` `## License` section via
2854    // the `caixa-helm` consumer's `caixa.licenca.clone().unwrap_or_else(||
2855    // "MIT".into())` (which only fires on `None`) at
2856    // `caixa-helm/src/lib.rs:361`. Closes the same `Some("")`
2857    // skips-`unwrap_or_else` footgun the peer `:repositorio`
2858    // (577b0a9) and `:descricao` (4e6db38) gates closed, on the
2859    // universal license-expression axis.
2860
2861    #[test]
2862    fn licenca_violation_on_empty_some() {
2863        // Canonical paste-from-blank-doc footgun on every kind. The
2864        // wrap envelope wraps [`ManifestError::LicencaEmpty`]'s
2865        // Display through verbatim, so the issue string names the
2866        // offending `:licenca` axis at the source — the author can
2867        // grep their caixa.lisp for `:licenca ""` and fix the empty
2868        // value in one edit. Mirrors the peer
2869        // `descricao_violation_on_empty_some` shape (4e6db38) on
2870        // the sibling `Option<String>` `:licenca` axis.
2871        let root = PathBuf::from("/tmp/x");
2872        let manifest = root.join("caixa.lisp");
2873        let default_lib = root.join("lib").join("demo.lisp");
2874        let layout =
2875            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2876        let mut c = caixa(CaixaKind::Biblioteca);
2877        c.licenca = Some(String::new());
2878        let err = layout.verify(&c, &root).unwrap_err();
2879        let LayoutError::LicencaViolation { caixa, issue } = err else {
2880            panic!("expected LayoutError::LicencaViolation, got {err:?}");
2881        };
2882        assert_eq!(caixa, "demo");
2883        assert!(
2884            issue.contains(":licenca"),
2885            "issue must name the offending slot: {issue}",
2886        );
2887    }
2888
2889    #[test]
2890    fn licenca_violation_fires_before_kind_coherence_mesh_slot() {
2891        // Cross-axis precedence pin: a Biblioteca with empty
2892        // `:licenca` *and* declared mesh slots (`:membros`)
2893        // surfaces the universal `:licenca` diagnostic first, not
2894        // the kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
2895        // `:licenca` is universal (every kind owns the slot), so
2896        // its shape diagnostic is more fundamental than the
2897        // partition-on-kind diagnostic. Mirrors the peer
2898        // `descricao_violation_fires_before_kind_coherence_mesh_slot`
2899        // pin (4e6db38) on the `:descricao` axis vs the same
2900        // kind-coherence gates.
2901        let root = PathBuf::from("/tmp/x");
2902        let manifest = root.join("caixa.lisp");
2903        let default_lib = root.join("lib").join("demo.lisp");
2904        let layout =
2905            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2906        let mut c = caixa(CaixaKind::Biblioteca);
2907        c.licenca = Some(String::new());
2908        c.membros = vec![crate::aplicacao::Membro {
2909            caixa: "x".into(),
2910            versao: "^0.1".into(),
2911        }];
2912        let err = layout.verify(&c, &root).unwrap_err();
2913        assert!(
2914            matches!(err, LayoutError::LicencaViolation { .. }),
2915            "got {err:?}",
2916        );
2917    }
2918
2919    #[test]
2920    fn licenca_violation_fires_after_descricao_violation() {
2921        // Cross-axis precedence pin (inside the universal metadata
2922        // cascade): a caixa with both an empty `:descricao` *and*
2923        // an empty `:licenca` surfaces `DescricaoViolation`
2924        // first — `:descricao` is the seventh universal axis in the
2925        // cascade and runs before `:licenca`, peer with the
2926        // canonical identity-axis-first cascade the peer gates
2927        // establish. Mirrors the peer
2928        // `descricao_violation_fires_after_repositorio_violation`
2929        // precedence pin (4e6db38) on the repositorio-axis-before-
2930        // descricao-axis pair.
2931        let root = PathBuf::from("/tmp/x");
2932        let manifest = root.join("caixa.lisp");
2933        let default_lib = root.join("lib").join("demo.lisp");
2934        let layout =
2935            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2936        let mut c = caixa(CaixaKind::Biblioteca);
2937        c.descricao = Some(String::new());
2938        c.licenca = Some(String::new());
2939        let err = layout.verify(&c, &root).unwrap_err();
2940        assert!(
2941            matches!(err, LayoutError::DescricaoViolation { .. }),
2942            "got {err:?}",
2943        );
2944    }
2945
2946    #[test]
2947    fn licenca_violation_accepts_none() {
2948        // Positive control sanity pin: a caixa that omits `:licenca`
2949        // entirely (the layout-test fixture defaults to `None`)
2950        // passes the gate trivially — the gate is a no-op when the
2951        // author didn't author a value. Mirrors the peer
2952        // `descricao_violation_accepts_none` pin (4e6db38).
2953        let root = PathBuf::from("/tmp/x");
2954        let manifest = root.join("caixa.lisp");
2955        let default_lib = root.join("lib").join("demo.lisp");
2956        let layout =
2957            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2958        let c = caixa(CaixaKind::Biblioteca);
2959        layout.verify(&c, &root).expect("None must pass");
2960    }
2961
2962    #[test]
2963    fn licenca_violation_accepts_canonical_expression() {
2964        // Positive control pin on the canonical pleme-io `:licenca`
2965        // shape: a non-empty SPDX expression the `caixa-helm` /
2966        // `caixa-flux` / `caixa-mesh` fixtures all carry (`"MIT"`)
2967        // passes the gate end-to-end.
2968        let root = PathBuf::from("/tmp/x");
2969        let manifest = root.join("caixa.lisp");
2970        let default_lib = root.join("lib").join("demo.lisp");
2971        let layout =
2972            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
2973        let mut c = caixa(CaixaKind::Biblioteca);
2974        c.licenca = Some("Apache-2.0 OR MIT".into());
2975        layout
2976            .verify(&c, &root)
2977            .expect("canonical SPDX expression must pass");
2978    }
2979
2980    #[test]
2981    fn licenca_violation_on_non_spdx_shape() {
2982        // Shape-predicate wire-up pin: a malformed `:licenca` value
2983        // that's a non-empty `Some(s)` but falls outside the SPDX
2984        // expression alphabet floor surfaces the `LicencaViolation`
2985        // envelope via the manifest-layer `ManifestError::LicencaInvalid`
2986        // arm. Mirrors the peer `licenca_violation_on_empty_some`
2987        // shape on the empty arm of the same axis and the peer
2988        // `edicao_violation_on_non_year_shape` shape on the sibling
2989        // `:edicao` axis. Until this gate landed a value like
2990        // `"Apache_2.0"` (an underscore-instead-of-hyphen typo) or
2991        // `"MIT, Apache-2.0"` (a comma-instead-of-`OR`-keyword
2992        // colloquial idiom) silently passed `StandardLayout::verify`
2993        // and landed in the rendered chart `README.md` `## License`
2994        // section + a future SPDX-aware Chart.yaml `license:`
2995        // emitter would refuse the value at `helm lint` time far
2996        // from the source caixa.lisp.
2997        let root = PathBuf::from("/tmp/x");
2998        let manifest = root.join("caixa.lisp");
2999        let default_lib = root.join("lib").join("demo.lisp");
3000        let layout =
3001            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
3002        let mut c = caixa(CaixaKind::Biblioteca);
3003        c.licenca = Some("Apache_2.0".into());
3004        let err = layout.verify(&c, &root).unwrap_err();
3005        let LayoutError::LicencaViolation { caixa, issue } = err else {
3006            panic!("expected LayoutError::LicencaViolation, got {err:?}");
3007        };
3008        assert_eq!(caixa, "demo");
3009        assert!(
3010            issue.contains(":licenca"),
3011            "issue must name the offending slot: {issue}",
3012        );
3013        assert!(
3014            issue.contains("Apache_2.0"),
3015            "issue must quote the offending value: {issue}",
3016        );
3017    }
3018
3019    // ── :edicao empty-Some shape wired into verify (universal axis) ──
3020    //
3021    // Until this wire-up landed `Caixa::validate_edicao` did not
3022    // exist — the universal language-edition axis had no shape gate
3023    // at any layer, so an empty `Some("")` silently passed
3024    // `Caixa::from_lisp` and `StandardLayout::verify` and landed as a
3025    // bare `(:edicao "")` line in the rendered caixa.lisp, ready for
3026    // a future renderer-side consumer's `Option::unwrap_or_else`
3027    // (which only fires on `None`) to skip its fallback. Closes the
3028    // same `Some("")`-skips-`unwrap_or_else` footgun the peer
3029    // `:repositorio` (577b0a9), `:descricao` (4e6db38), and
3030    // `:licenca` (3d1e535) gates closed, on the universal language-
3031    // edition axis — the last un-gated universal-axis
3032    // `Option<String>` Caixa-level value-shape surface.
3033
3034    #[test]
3035    fn edicao_violation_on_empty_some() {
3036        // Canonical paste-from-blank-doc footgun on every kind. The
3037        // wrap envelope wraps [`ManifestError::EdicaoEmpty`]'s
3038        // Display through verbatim, so the issue string names the
3039        // offending `:edicao` axis at the source — the author can
3040        // grep their caixa.lisp for `:edicao ""` and fix the empty
3041        // value in one edit. Mirrors the peer
3042        // `licenca_violation_on_empty_some` shape (3d1e535) on the
3043        // sibling `Option<String>` `:edicao` axis.
3044        let root = PathBuf::from("/tmp/x");
3045        let manifest = root.join("caixa.lisp");
3046        let default_lib = root.join("lib").join("demo.lisp");
3047        let layout =
3048            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
3049        let mut c = caixa(CaixaKind::Biblioteca);
3050        c.edicao = Some(String::new());
3051        let err = layout.verify(&c, &root).unwrap_err();
3052        let LayoutError::EdicaoViolation { caixa, issue } = err else {
3053            panic!("expected LayoutError::EdicaoViolation, got {err:?}");
3054        };
3055        assert_eq!(caixa, "demo");
3056        assert!(
3057            issue.contains(":edicao"),
3058            "issue must name the offending slot: {issue}",
3059        );
3060    }
3061
3062    #[test]
3063    fn edicao_violation_fires_before_kind_coherence_mesh_slot() {
3064        // Cross-axis precedence pin: a Biblioteca with empty
3065        // `:edicao` *and* declared mesh slots (`:membros`) surfaces
3066        // the universal `:edicao` diagnostic first, not the
3067        // kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
3068        // `:edicao` is universal (every kind owns the slot), so
3069        // its shape diagnostic is more fundamental than the
3070        // partition-on-kind diagnostic. Mirrors the peer
3071        // `licenca_violation_fires_before_kind_coherence_mesh_slot`
3072        // pin (3d1e535) on the `:licenca` axis vs the same
3073        // kind-coherence gates.
3074        let root = PathBuf::from("/tmp/x");
3075        let manifest = root.join("caixa.lisp");
3076        let default_lib = root.join("lib").join("demo.lisp");
3077        let layout =
3078            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
3079        let mut c = caixa(CaixaKind::Biblioteca);
3080        c.edicao = Some(String::new());
3081        c.membros = vec![crate::aplicacao::Membro {
3082            caixa: "x".into(),
3083            versao: "^0.1".into(),
3084        }];
3085        let err = layout.verify(&c, &root).unwrap_err();
3086        assert!(
3087            matches!(err, LayoutError::EdicaoViolation { .. }),
3088            "got {err:?}",
3089        );
3090    }
3091
3092    #[test]
3093    fn edicao_violation_fires_after_licenca_violation() {
3094        // Cross-axis precedence pin (inside the universal metadata
3095        // cascade): a caixa with both an empty `:licenca` *and* an
3096        // empty `:edicao` surfaces `LicencaViolation` first —
3097        // `:licenca` is the eighth universal axis in the cascade
3098        // and runs before `:edicao`, peer with the canonical
3099        // identity-axis-first cascade the peer gates establish.
3100        // Mirrors the peer
3101        // `licenca_violation_fires_after_descricao_violation`
3102        // precedence pin (3d1e535) on the descricao-axis-before-
3103        // licenca-axis pair.
3104        let root = PathBuf::from("/tmp/x");
3105        let manifest = root.join("caixa.lisp");
3106        let default_lib = root.join("lib").join("demo.lisp");
3107        let layout =
3108            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
3109        let mut c = caixa(CaixaKind::Biblioteca);
3110        c.licenca = Some(String::new());
3111        c.edicao = Some(String::new());
3112        let err = layout.verify(&c, &root).unwrap_err();
3113        assert!(
3114            matches!(err, LayoutError::LicencaViolation { .. }),
3115            "got {err:?}",
3116        );
3117    }
3118
3119    #[test]
3120    fn edicao_violation_accepts_none() {
3121        // Positive control sanity pin: a caixa that omits `:edicao`
3122        // entirely (the layout-test fixture defaults to `None`)
3123        // passes the gate trivially — the gate is a no-op when the
3124        // author didn't author a value. Mirrors the peer
3125        // `licenca_violation_accepts_none` pin (3d1e535).
3126        let root = PathBuf::from("/tmp/x");
3127        let manifest = root.join("caixa.lisp");
3128        let default_lib = root.join("lib").join("demo.lisp");
3129        let layout =
3130            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
3131        let c = caixa(CaixaKind::Biblioteca);
3132        layout.verify(&c, &root).expect("None must pass");
3133    }
3134
3135    #[test]
3136    fn edicao_violation_accepts_canonical_value() {
3137        // Positive control pin on the canonical pleme-io `:edicao`
3138        // shape: the `"2026"` edition every `caixa-helm` /
3139        // `caixa-flux` / `caixa-mesh` / `caixa-core/src/render.rs`
3140        // fixture carries by construction passes the gate end-to-end.
3141        let root = PathBuf::from("/tmp/x");
3142        let manifest = root.join("caixa.lisp");
3143        let default_lib = root.join("lib").join("demo.lisp");
3144        let layout =
3145            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
3146        let mut c = caixa(CaixaKind::Biblioteca);
3147        c.edicao = Some("2026".into());
3148        layout
3149            .verify(&c, &root)
3150            .expect("canonical edition must pass");
3151    }
3152
3153    #[test]
3154    fn edicao_violation_on_non_year_shape() {
3155        // Shape-predicate wire-up pin: a malformed `:edicao` value
3156        // that's a non-empty `Some(s)` but not a 4-digit ASCII
3157        // decimal year surfaces the `EdicaoViolation` envelope via
3158        // the manifest-layer `ManifestError::EdicaoInvalid` arm.
3159        // Mirrors the peer `edicao_violation_on_empty_some` shape
3160        // on the empty arm of the same axis. Until this gate landed
3161        // a value like `"v2026"` (a familiar git-tag idiom that
3162        // doesn't apply to the year-shaped edition axis) silently
3163        // passed `StandardLayout::verify` and broke at the
3164        // substrate's build-time edition selector far from the
3165        // source caixa.lisp.
3166        let root = PathBuf::from("/tmp/x");
3167        let manifest = root.join("caixa.lisp");
3168        let default_lib = root.join("lib").join("demo.lisp");
3169        let layout =
3170            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
3171        let mut c = caixa(CaixaKind::Biblioteca);
3172        c.edicao = Some("v2026".into());
3173        let err = layout.verify(&c, &root).unwrap_err();
3174        let LayoutError::EdicaoViolation { caixa, issue } = err else {
3175            panic!("expected LayoutError::EdicaoViolation, got {err:?}");
3176        };
3177        assert_eq!(caixa, "demo");
3178        assert!(
3179            issue.contains(":edicao"),
3180            "issue must name the offending slot: {issue}",
3181        );
3182        assert!(
3183            issue.contains("v2026"),
3184            "issue must quote the offending value: {issue}",
3185        );
3186    }
3187
3188    // ── Caixa-identity gates (`:nome`, `:versao`) wired into verify ────
3189    //
3190    // Until this wire-up landed `Caixa::validate_nome` and
3191    // `Caixa::validate_versao` lived as `pub fn` on `Caixa` with full
3192    // per-arm unit coverage in `manifest::tests`, but no production
3193    // path called them — `feira build` silently accepted malformed
3194    // `:nome` / `:versao` and the failure surfaced at `helm install` /
3195    // `kubectl apply` / `feira publish` / lacre-resolve / `:upgrade-from
3196    // :from` matching time, far from the source `caixa.lisp`. The
3197    // following pins fence the layout-pipeline wire-up: every layout
3198    // verify on a structurally-invalid Caixa identity axis surfaces
3199    // the per-axis `*Violation { caixa, issue }` envelope before any
3200    // kind-coherence, code-path, or downstream gate sees it.
3201
3202    #[test]
3203    fn nome_violation_on_uppercase() {
3204        let root = PathBuf::from("/tmp/x");
3205        let manifest = root.join("caixa.lisp");
3206        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3207        let mut c = caixa(CaixaKind::Biblioteca);
3208        c.nome = "MyApp".into();
3209        let err = layout.verify(&c, &root).unwrap_err();
3210        let LayoutError::NomeViolation { caixa, issue } = err else {
3211            panic!("expected LayoutError::NomeViolation, got {err:?}");
3212        };
3213        assert_eq!(caixa, "MyApp");
3214        assert!(
3215            issue.contains("MyApp"),
3216            "issue must quote the offending nome: {issue}",
3217        );
3218    }
3219
3220    #[test]
3221    fn nome_violation_on_underscore() {
3222        let root = PathBuf::from("/tmp/x");
3223        let manifest = root.join("caixa.lisp");
3224        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3225        let mut c = caixa(CaixaKind::Biblioteca);
3226        c.nome = "my_app".into();
3227        let err = layout.verify(&c, &root).unwrap_err();
3228        assert!(
3229            matches!(err, LayoutError::NomeViolation { ref caixa, .. } if caixa == "my_app"),
3230            "got {err:?}",
3231        );
3232    }
3233
3234    #[test]
3235    fn nome_violation_on_empty() {
3236        // Empty `:nome` surfaces NomeViolation wrapping the narrower
3237        // `ManifestError::NomeEmpty` arm — the empty-first cascade the
3238        // peer per-axis name gates already use.
3239        let root = PathBuf::from("/tmp/x");
3240        let manifest = root.join("caixa.lisp");
3241        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3242        let mut c = caixa(CaixaKind::Biblioteca);
3243        c.nome = String::new();
3244        let err = layout.verify(&c, &root).unwrap_err();
3245        let LayoutError::NomeViolation { caixa, issue } = err else {
3246            panic!("expected LayoutError::NomeViolation, got {err:?}");
3247        };
3248        assert!(caixa.is_empty());
3249        assert!(
3250            issue.contains(":nome is empty"),
3251            "issue must surface the empty-arm diagnostic: {issue}",
3252        );
3253    }
3254
3255    #[test]
3256    fn versao_violation_on_missing_patch() {
3257        // `"0.1"` — the canonical "I shortened it" footgun. Helm /
3258        // OCI / lacre-resolve / `:upgrade-from :from` all strict-parse
3259        // through `semver::Version::parse`, which refuses a two-part
3260        // shape.
3261        let root = PathBuf::from("/tmp/x");
3262        let manifest = root.join("caixa.lisp");
3263        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3264        let mut c = caixa(CaixaKind::Biblioteca);
3265        c.versao = "0.1".into();
3266        let err = layout.verify(&c, &root).unwrap_err();
3267        let LayoutError::VersaoViolation { caixa, issue } = err else {
3268            panic!("expected LayoutError::VersaoViolation, got {err:?}");
3269        };
3270        assert_eq!(caixa, "demo");
3271        assert!(
3272            issue.contains("0.1"),
3273            "issue must quote the offending versao: {issue}",
3274        );
3275    }
3276
3277    #[test]
3278    fn versao_violation_on_git_tag_shape() {
3279        // `"v0.1.0"` — the git-tag-shape-leaking-into-versao typo.
3280        let root = PathBuf::from("/tmp/x");
3281        let manifest = root.join("caixa.lisp");
3282        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3283        let mut c = caixa(CaixaKind::Biblioteca);
3284        c.versao = "v0.1.0".into();
3285        let err = layout.verify(&c, &root).unwrap_err();
3286        assert!(
3287            matches!(err, LayoutError::VersaoViolation { ref issue, .. }
3288                if issue.contains("v0.1.0")),
3289            "got {err:?}",
3290        );
3291    }
3292
3293    #[test]
3294    fn versao_violation_on_empty() {
3295        let root = PathBuf::from("/tmp/x");
3296        let manifest = root.join("caixa.lisp");
3297        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3298        let mut c = caixa(CaixaKind::Biblioteca);
3299        c.versao = String::new();
3300        let err = layout.verify(&c, &root).unwrap_err();
3301        let LayoutError::VersaoViolation { caixa, issue } = err else {
3302            panic!("expected LayoutError::VersaoViolation, got {err:?}");
3303        };
3304        assert_eq!(caixa, "demo");
3305        assert!(
3306            issue.contains(":versao is empty"),
3307            "issue must surface the empty-arm diagnostic: {issue}",
3308        );
3309    }
3310
3311    #[test]
3312    fn nome_violation_fires_before_versao_violation() {
3313        // Precedence pin: when both `:nome` and `:versao` are malformed,
3314        // `:nome` surfaces first — the canonical declaration-order
3315        // precedence the `ManifestError` family establishes, the same
3316        // grep-order the author follows when fixing in `caixa.lisp`.
3317        let root = PathBuf::from("/tmp/x");
3318        let manifest = root.join("caixa.lisp");
3319        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3320        let mut c = caixa(CaixaKind::Biblioteca);
3321        c.nome = "MyApp".into();
3322        c.versao = "0.1".into();
3323        let err = layout.verify(&c, &root).unwrap_err();
3324        assert!(
3325            matches!(err, LayoutError::NomeViolation { .. }),
3326            "got {err:?} — nome must fire before versao",
3327        );
3328    }
3329
3330    #[test]
3331    fn nome_violation_fires_before_kind_coherence() {
3332        // Precedence pin: a Biblioteca caixa with a malformed `:nome`
3333        // AND a declared mesh slot surfaces NomeViolation, not
3334        // MeshSlotsOnNonAplicacao — the identity-axis gate is more
3335        // fundamental than the kind-coherence gate (which carries
3336        // `caixa.nome` verbatim in its diagnostic, and so depends on the
3337        // name being structurally valid to render a useful message).
3338        let root = PathBuf::from("/tmp/x");
3339        let manifest = root.join("caixa.lisp");
3340        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3341        let mut c = caixa(CaixaKind::Biblioteca);
3342        c.nome = "MyApp".into();
3343        c.membros = vec![crate::aplicacao::Membro {
3344            caixa: "x".into(),
3345            versao: "^0.1".into(),
3346        }];
3347        let err = layout.verify(&c, &root).unwrap_err();
3348        assert!(
3349            matches!(err, LayoutError::NomeViolation { .. }),
3350            "got {err:?} — nome must fire before MeshSlotsOnNonAplicacao",
3351        );
3352    }
3353
3354    #[test]
3355    fn nome_violation_fires_before_owncode() {
3356        // Precedence pin: a Supervisor with a malformed `:nome` AND
3357        // declared `:bibliotecas` surfaces NomeViolation, not
3358        // SupervisorOwnsCode — same rationale as above.
3359        let root = PathBuf::from("/tmp/x");
3360        let manifest = root.join("caixa.lisp");
3361        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3362        let mut c = caixa(CaixaKind::Supervisor);
3363        c.nome = "MyApp".into();
3364        c.bibliotecas = vec!["lib/x.lisp".into()];
3365        let err = layout.verify(&c, &root).unwrap_err();
3366        assert!(
3367            matches!(err, LayoutError::NomeViolation { .. }),
3368            "got {err:?} — nome must fire before SupervisorOwnsCode",
3369        );
3370    }
3371
3372    #[test]
3373    fn versao_violation_fires_before_kind_coherence() {
3374        // Precedence pin: a Biblioteca with a valid `:nome` but a
3375        // malformed `:versao` AND a declared servico slot surfaces
3376        // VersaoViolation before ServicoSlotsOnNonServico.
3377        let root = PathBuf::from("/tmp/x");
3378        let manifest = root.join("caixa.lisp");
3379        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3380        let mut c = caixa(CaixaKind::Biblioteca);
3381        c.versao = "v0.1.0".into();
3382        c.limits = Some(crate::LimitsSpec {
3383            memory: Some(64 * 1024 * 1024),
3384            ..Default::default()
3385        });
3386        let err = layout.verify(&c, &root).unwrap_err();
3387        assert!(
3388            matches!(err, LayoutError::VersaoViolation { .. }),
3389            "got {err:?} — versao must fire before ServicoSlotsOnNonServico",
3390        );
3391    }
3392
3393    #[test]
3394    fn nome_violation_fires_before_missing_lib() {
3395        // Precedence pin: a Biblioteca with a malformed `:nome` and no
3396        // lib entry surfaces NomeViolation, not MissingLib — the
3397        // identity-axis gate is more fundamental than the layout's
3398        // `lib/<nome>.lisp` default-path check (which derives the
3399        // expected path from `:nome` itself, so would surface a
3400        // misleading "expected lib/MyApp.lisp" diagnostic against an
3401        // unrecoverable name).
3402        let root = PathBuf::from("/tmp/x");
3403        let manifest = root.join("caixa.lisp");
3404        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3405        let mut c = caixa(CaixaKind::Biblioteca);
3406        c.nome = "MyApp".into();
3407        let err = layout.verify(&c, &root).unwrap_err();
3408        assert!(
3409            matches!(err, LayoutError::NomeViolation { .. }),
3410            "got {err:?} — nome must fire before MissingLib",
3411        );
3412    }
3413
3414    #[test]
3415    fn nome_versao_violations_fire_after_missing_manifest() {
3416        // Precedence pin: `MissingManifest` still dominates — there's
3417        // no caixa to identity-check when the manifest is missing.
3418        let root = PathBuf::from("/tmp/x");
3419        let layout = StandardLayout::new().with_path_exists(|_| false);
3420        let mut c = caixa(CaixaKind::Biblioteca);
3421        c.nome = "MyApp".into();
3422        c.versao = "0.1".into();
3423        let err = layout.verify(&c, &root).unwrap_err();
3424        assert!(
3425            matches!(err, LayoutError::MissingManifest(_)),
3426            "got {err:?} — MissingManifest must dominate identity gates",
3427        );
3428    }
3429
3430    #[test]
3431    fn valid_nome_versao_passes_to_downstream_gates() {
3432        // Sanity pin: the canonical "demo" / "0.1.0" identity passes
3433        // both axes; downstream gates (MissingLib here) take over.
3434        let root = PathBuf::from("/tmp/x");
3435        let manifest = root.join("caixa.lisp");
3436        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3437        let err = layout
3438            .verify(&caixa(CaixaKind::Biblioteca), &root)
3439            .unwrap_err();
3440        assert!(
3441            matches!(err, LayoutError::MissingLib { .. }),
3442            "got {err:?} — valid identity must pass to MissingLib",
3443        );
3444    }
3445
3446    // ── :deps / :deps-dev shape gate (lifted to layout-level verify) ─────
3447    //
3448    // Until this wire-up landed `Caixa::validate_deps` lived as `pub fn`
3449    // on `Caixa` with full per-arm unit coverage in `manifest::tests` +
3450    // `dep::tests` but no production path called it — `feira build`
3451    // silently accepted a malformed `:deps` / `:deps-dev` entry and the
3452    // failure surfaced at lacre-resolve / `git clone` / `cargo metadata`
3453    // / `helm install` time on the *first* downstream consumer to
3454    // strict-parse the value, far from the source `caixa.lisp` and
3455    // without any field naming the offending `:deps` axis. The following
3456    // pins fence the layout-pipeline wire-up: every layout verify on a
3457    // structurally-invalid `:deps` value-shape surfaces the per-axis
3458    // `DepsViolation { caixa, issue }` envelope (peer of
3459    // `NomeViolation` / `VersaoViolation` / `CodePathViolation` /
3460    // `LimitsViolation` / `BehaviorViolation` / `UpgradeViolation` /
3461    // `SupervisorViolation` / `AplicacaoViolation`) before any kind-
3462    // coherence, code-path, or downstream gate sees it.
3463
3464    #[test]
3465    fn deps_violation_on_empty_dep_nome() {
3466        // Empty `:nome` on a `:deps` entry surfaces the narrower
3467        // `DepError::NomeEmpty` arm through the wrap envelope.
3468        use crate::Dep;
3469        let root = PathBuf::from("/tmp/x");
3470        let manifest = root.join("caixa.lisp");
3471        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3472        let mut c = caixa(CaixaKind::Biblioteca);
3473        c.deps = vec![Dep::simple("", "^0.1")];
3474        let err = layout.verify(&c, &root).unwrap_err();
3475        let LayoutError::DepsViolation { caixa, issue } = err else {
3476            panic!("expected LayoutError::DepsViolation, got {err:?}");
3477        };
3478        assert_eq!(caixa, "demo");
3479        assert!(
3480            issue.contains(":deps") && issue.contains(":nome"),
3481            "issue must name the offending slot + axis: {issue}",
3482        );
3483    }
3484
3485    #[test]
3486    fn deps_violation_on_uppercase_dep_nome() {
3487        // Uppercase `:nome` on a `:deps` entry surfaces
3488        // `DepError::NomeInvalid` (DNS-1123 violation) through the wrap.
3489        use crate::Dep;
3490        let root = PathBuf::from("/tmp/x");
3491        let manifest = root.join("caixa.lisp");
3492        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3493        let mut c = caixa(CaixaKind::Biblioteca);
3494        c.deps = vec![Dep::simple("Caixa-Teia", "^0.1")];
3495        let err = layout.verify(&c, &root).unwrap_err();
3496        let LayoutError::DepsViolation { caixa, issue } = err else {
3497            panic!("expected LayoutError::DepsViolation, got {err:?}");
3498        };
3499        assert_eq!(caixa, "demo");
3500        assert!(
3501            issue.contains("Caixa-Teia"),
3502            "issue must quote the offending dep nome verbatim: {issue}",
3503        );
3504    }
3505
3506    #[test]
3507    fn deps_violation_on_unparseable_dep_versao() {
3508        // Unparseable `:versao` requirement on a `:deps` entry surfaces
3509        // `DepError::VersaoInvalid` through the wrap — the canonical
3510        // "the semver::Error reached the resolver, far from the source"
3511        // footgun closed at author time.
3512        use crate::Dep;
3513        let root = PathBuf::from("/tmp/x");
3514        let manifest = root.join("caixa.lisp");
3515        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3516        let mut c = caixa(CaixaKind::Biblioteca);
3517        c.deps = vec![Dep::simple("caixa-teia", "not-a-req")];
3518        let err = layout.verify(&c, &root).unwrap_err();
3519        let LayoutError::DepsViolation { caixa, issue } = err else {
3520            panic!("expected LayoutError::DepsViolation, got {err:?}");
3521        };
3522        assert_eq!(caixa, "demo");
3523        assert!(
3524            issue.contains("caixa-teia") && issue.contains("not-a-req"),
3525            "issue must quote the dep nome + offending versao: {issue}",
3526        );
3527    }
3528
3529    #[test]
3530    fn deps_violation_on_duplicate_nome_in_deps() {
3531        // Within-list `:deps :nome` duplicate surfaces
3532        // `DepError::DuplicateNome { list: ":deps" }` through the wrap.
3533        use crate::Dep;
3534        let root = PathBuf::from("/tmp/x");
3535        let manifest = root.join("caixa.lisp");
3536        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3537        let mut c = caixa(CaixaKind::Biblioteca);
3538        c.deps = vec![
3539            Dep::simple("caixa-teia", "^0.1"),
3540            Dep::simple("caixa-teia", "^0.2"),
3541        ];
3542        let err = layout.verify(&c, &root).unwrap_err();
3543        let LayoutError::DepsViolation { caixa, issue } = err else {
3544            panic!("expected LayoutError::DepsViolation, got {err:?}");
3545        };
3546        assert_eq!(caixa, "demo");
3547        assert!(
3548            issue.contains("caixa-teia") && issue.contains(":deps"),
3549            "issue must quote the duplicated nome + list: {issue}",
3550        );
3551    }
3552
3553    #[test]
3554    fn deps_violation_on_duplicate_nome_in_deps_dev() {
3555        // Within-list `:deps-dev :nome` duplicate surfaces the same
3556        // diagnostic on the dev-only axis — neither list is a
3557        // second-class citizen of the typed surface.
3558        use crate::Dep;
3559        let root = PathBuf::from("/tmp/x");
3560        let manifest = root.join("caixa.lisp");
3561        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3562        let mut c = caixa(CaixaKind::Biblioteca);
3563        c.deps_dev = vec![
3564            Dep::simple("caixa-teia", "^0.1"),
3565            Dep::simple("caixa-teia", "^0.2"),
3566        ];
3567        let err = layout.verify(&c, &root).unwrap_err();
3568        let LayoutError::DepsViolation { caixa, issue } = err else {
3569            panic!("expected LayoutError::DepsViolation, got {err:?}");
3570        };
3571        assert_eq!(caixa, "demo");
3572        assert!(
3573            issue.contains(":deps-dev"),
3574            "issue must name the offending list: {issue}",
3575        );
3576    }
3577
3578    #[test]
3579    fn deps_violation_in_deps_fires_before_deps_dev() {
3580        // Precedence pin: when *both* `:deps` and `:deps-dev` carry a
3581        // malformed entry, the `:deps` walk fires first — the canonical
3582        // declaration-order precedence `Caixa::validate_deps` establishes
3583        // (the same author-grep ordering the typed-graph peers use on
3584        // every other Vec-shaped surface).
3585        use crate::Dep;
3586        let root = PathBuf::from("/tmp/x");
3587        let manifest = root.join("caixa.lisp");
3588        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3589        let mut c = caixa(CaixaKind::Biblioteca);
3590        c.deps = vec![Dep::simple("Bad-In-Deps", "^0.1")];
3591        c.deps_dev = vec![Dep::simple("Bad-In-Deps-Dev", "^0.1")];
3592        let err = layout.verify(&c, &root).unwrap_err();
3593        let LayoutError::DepsViolation { caixa: _, issue } = err else {
3594            panic!("expected LayoutError::DepsViolation, got {err:?}");
3595        };
3596        assert!(
3597            issue.contains("Bad-In-Deps") && !issue.contains("Bad-In-Deps-Dev"),
3598            "issue must name the :deps offender, not :deps-dev: {issue}",
3599        );
3600    }
3601
3602    #[test]
3603    fn deps_violation_fires_after_versao_violation() {
3604        // Precedence pin: when both the top-level `:versao` and a `:deps`
3605        // entry are malformed, the Caixa-identity gate fires first — the
3606        // canonical declaration order on `Caixa` (`:nome` → `:versao` →
3607        // ... → `:deps`) and the same identity-axis-dominates-content-
3608        // axis discipline the peer `validate_nome` / `validate_versao`
3609        // wire-up established (1f74a5f). A malformed `:versao` would
3610        // otherwise quote `caixa.nome` against a downstream-shaped
3611        // diagnostic.
3612        use crate::Dep;
3613        let root = PathBuf::from("/tmp/x");
3614        let manifest = root.join("caixa.lisp");
3615        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3616        let mut c = caixa(CaixaKind::Biblioteca);
3617        c.versao = "v0.1.0".into();
3618        c.deps = vec![Dep::simple("Bad-Dep", "^0.1")];
3619        let err = layout.verify(&c, &root).unwrap_err();
3620        assert!(
3621            matches!(err, LayoutError::VersaoViolation { .. }),
3622            "got {err:?} — versao must fire before DepsViolation",
3623        );
3624    }
3625
3626    #[test]
3627    fn deps_violation_fires_before_kind_coherence() {
3628        // Precedence pin: a Supervisor with a malformed `:deps` entry
3629        // AND declared `:bibliotecas` (the canonical SupervisorOwnsCode
3630        // shape) surfaces DepsViolation, not SupervisorOwnsCode — the
3631        // dep surface is universal across all kinds and its shape gate
3632        // is more fundamental than the kind-coherence partitions on
3633        // `:bibliotecas` / `:exe` / `:servicos`. The author can fix the
3634        // dep typo without first being told to move their `:bibliotecas`
3635        // off a Supervisor.
3636        use crate::Dep;
3637        let root = PathBuf::from("/tmp/x");
3638        let manifest = root.join("caixa.lisp");
3639        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3640        let mut c = caixa(CaixaKind::Supervisor);
3641        c.deps = vec![Dep::simple("Bad-Dep", "^0.1")];
3642        c.bibliotecas = vec!["lib/x.lisp".into()];
3643        let err = layout.verify(&c, &root).unwrap_err();
3644        assert!(
3645            matches!(err, LayoutError::DepsViolation { .. }),
3646            "got {err:?} — DepsViolation must fire before SupervisorOwnsCode",
3647        );
3648    }
3649
3650    #[test]
3651    fn deps_violation_fires_after_missing_manifest() {
3652        // Precedence pin: `MissingManifest` still dominates — there's no
3653        // caixa to deps-check when the manifest is missing.
3654        use crate::Dep;
3655        let root = PathBuf::from("/tmp/x");
3656        let layout = StandardLayout::new().with_path_exists(|_| false);
3657        let mut c = caixa(CaixaKind::Biblioteca);
3658        c.deps = vec![Dep::simple("Bad-Dep", "^0.1")];
3659        let err = layout.verify(&c, &root).unwrap_err();
3660        assert!(
3661            matches!(err, LayoutError::MissingManifest(_)),
3662            "got {err:?} — MissingManifest must dominate the deps gate",
3663        );
3664    }
3665
3666    #[test]
3667    fn deps_violation_on_self_dep_in_deps() {
3668        // Cross-slot self-edge: a caixa whose `:deps` lists its own
3669        // `:nome` is rejected at the layout wire-up, the diagnostic
3670        // surfaces through the `DepsViolation` envelope with both the
3671        // offending list tag (`":deps"`) and the parent's `:nome`
3672        // verbatim. Until this wire-up landed the self-dep silently
3673        // passed `feira build` and the resolver's lacre-pipeline
3674        // closure walk either rejected mid-traversal (infinite
3675        // recursion detected far from the source caixa.lisp) or, on
3676        // the unbounded path, recursed until it exhausted its stack.
3677        // Mirrors the supervision-tree
3678        // [`supervisor_violation_on_self_supervision`] and the
3679        // Aplicacao-membership self-edge wire-up tests on the peer
3680        // typed-name-graph axes.
3681        use crate::Dep;
3682        let root = PathBuf::from("/tmp/x");
3683        let manifest = root.join("caixa.lisp");
3684        let default_lib = root.join("lib").join("demo.lisp");
3685        let layout =
3686            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
3687        let mut c = caixa(CaixaKind::Biblioteca);
3688        c.deps = vec![Dep::simple("demo", "^0.1")];
3689        let err = layout.verify(&c, &root).unwrap_err();
3690        let LayoutError::DepsViolation { caixa, issue } = err else {
3691            panic!("expected LayoutError::DepsViolation, got {err:?}");
3692        };
3693        assert_eq!(caixa, "demo");
3694        assert!(
3695            issue.contains(":deps") && issue.contains("demo"),
3696            "issue must name the offending list + parent :nome: {issue}",
3697        );
3698    }
3699
3700    #[test]
3701    fn deps_violation_on_self_dep_in_deps_dev() {
3702        // Same cross-slot self-edge gate on the `:deps-dev` axis —
3703        // neither dep list is a second-class citizen of the typed
3704        // surface. The diagnostic names `:deps-dev` so the author can
3705        // grep their caixa.lisp for the offending block directly.
3706        use crate::Dep;
3707        let root = PathBuf::from("/tmp/x");
3708        let manifest = root.join("caixa.lisp");
3709        let default_lib = root.join("lib").join("demo.lisp");
3710        let layout =
3711            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
3712        let mut c = caixa(CaixaKind::Biblioteca);
3713        c.deps_dev = vec![Dep::simple("demo", "^0.1")];
3714        let err = layout.verify(&c, &root).unwrap_err();
3715        let LayoutError::DepsViolation { caixa, issue } = err else {
3716            panic!("expected LayoutError::DepsViolation, got {err:?}");
3717        };
3718        assert_eq!(caixa, "demo");
3719        assert!(
3720            issue.contains(":deps-dev"),
3721            "issue must name the offending list: {issue}",
3722        );
3723    }
3724
3725    #[test]
3726    fn self_dep_fires_after_per_entry_dep_shape() {
3727        // Precedence pin: the per-entry shape gates of
3728        // [`Caixa::validate_deps`] (DNS-1123 / SemVer / fonte / etc.)
3729        // fire first on a self-dep entry whose `:nome` is malformed.
3730        // Same ordering posture every peer cross-slot gate uses
3731        // (`validate_no_self_supervision` after `SupervisorSpec::validate`,
3732        // `validate_no_self_membership` after `AplicacaoSpec::validate`).
3733        // A malformed self-dep `:nome` surfaces the narrower
3734        // per-entry diagnostic (which already names the parser-side
3735        // reason) before the self-edge gate sees the entry.
3736        use crate::Dep;
3737        let root = PathBuf::from("/tmp/x");
3738        let manifest = root.join("caixa.lisp");
3739        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3740        let mut c = caixa(CaixaKind::Biblioteca);
3741        // The parent is "demo" (DNS-1123 valid); the dep is "DEMO"
3742        // (DNS-1123 invalid). The per-entry shape gate fires on the
3743        // upper-case nome, masking the self-edge gate (and that's the
3744        // canonical precedence — fix the dep shape first, then the
3745        // structural self-edge becomes the next live diagnostic).
3746        c.deps = vec![Dep::simple("DEMO", "^0.1")];
3747        let err = layout.verify(&c, &root).unwrap_err();
3748        let LayoutError::DepsViolation { caixa: _, issue } = err else {
3749            panic!("expected LayoutError::DepsViolation, got {err:?}");
3750        };
3751        assert!(
3752            issue.contains("DNS-1123"),
3753            "issue must be the per-entry shape diagnostic, not the self-edge gate: {issue}",
3754        );
3755    }
3756
3757    #[test]
3758    fn valid_deps_pass_to_downstream_gates() {
3759        // Positive control pin: the canonical authoring shape (one
3760        // `:deps` entry naming a DNS-1123 nome + Cargo-shaped requirement,
3761        // one `:deps-dev` entry on a distinct nome) passes the dep gate;
3762        // downstream gates (MissingLib here) take over. Drift here =
3763        // a future tighten that rejects any canonical shape surfaces as
3764        // a regression at this layout-level pin.
3765        use crate::Dep;
3766        let root = PathBuf::from("/tmp/x");
3767        let manifest = root.join("caixa.lisp");
3768        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3769        let mut c = caixa(CaixaKind::Biblioteca);
3770        c.deps = vec![Dep::simple("caixa-teia", "^0.1")];
3771        c.deps_dev = vec![Dep::simple("caixa-lint", "^0.2")];
3772        let err = layout.verify(&c, &root).unwrap_err();
3773        assert!(
3774            matches!(err, LayoutError::MissingLib { .. }),
3775            "got {err:?} — valid deps must pass to MissingLib",
3776        );
3777    }
3778
3779    #[test]
3780    fn code_path_gate_runs_after_foreign_code_slot_gate() {
3781        // Precedence pin: a Servico that declares `:exe` (foreign code
3782        // surface) surfaces ForeignCodeSlot, *not* a per-entry path
3783        // shape diagnostic, even when the `:exe` entry is itself
3784        // malformed. The kind-coherence gate is the load-bearing
3785        // diagnostic at this site — once the slot is moved off the
3786        // wrong kind, the per-entry shape gate becomes the next live
3787        // diagnostic.
3788        let root = PathBuf::from("/tmp/x");
3789        let manifest = root.join("caixa.lisp");
3790        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3791        let mut c = caixa(CaixaKind::Servico);
3792        c.servicos = vec!["servicos/ok.yaml".into()];
3793        c.exe = vec!["/etc/foreign".into()];
3794        let err = layout.verify(&c, &root).unwrap_err();
3795        assert!(
3796            matches!(err, LayoutError::ForeignCodeSlot { .. }),
3797            "expected ForeignCodeSlot (kind-coherence wins over per-entry shape), got {err:?}",
3798        );
3799    }
3800
3801    // ── M2 typed-substrate invariants ────────────────────────────────────
3802
3803    #[test]
3804    fn behavior_callback_path_must_exist() {
3805        use crate::BehaviorSpec;
3806        use std::path::PathBuf;
3807        let root = PathBuf::from("/tmp/x");
3808        let manifest = root.join("caixa.lisp");
3809        let mut c = caixa(CaixaKind::Servico);
3810        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
3811        let svc = root.join("servicos/demo.computeunit.yaml");
3812        c.behavior = Some(BehaviorSpec {
3813            on_init: Some(PathBuf::from("lib/init.lisp")),
3814            ..Default::default()
3815        });
3816        let manifest_clone = manifest.clone();
3817        let svc_clone = svc.clone();
3818        let layout =
3819            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
3820        let err = layout.verify(&c, &root).unwrap_err();
3821        assert!(matches!(
3822            err,
3823            LayoutError::MissingEntry { kind, .. }
3824                if kind == crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK
3825        ));
3826
3827        // Now declare the path exists — passes.
3828        let init = root.join("lib/init.lisp");
3829        let layout =
3830            StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc || p == init);
3831        layout.verify(&c, &root).unwrap();
3832    }
3833
3834    #[test]
3835    fn behavior_absolute_callback_is_violation_not_missing() {
3836        // An absolute path silently subverts `root.join(p)` (Path::join
3837        // replaces the base when the right side is absolute). Before
3838        // BehaviorSpec::validate ran, an `:on-init "/etc/passwd"` would
3839        // surface as a confusing "missing behavior-callback /etc/passwd"
3840        // — or, worse, pass when /etc/passwd happens to exist. Now it's
3841        // a value-shape error naming the slot.
3842        use crate::BehaviorSpec;
3843        let root = PathBuf::from("/tmp/x");
3844        let manifest = root.join("caixa.lisp");
3845        let svc = root.join("servicos/demo.computeunit.yaml");
3846        let mut c = caixa(CaixaKind::Servico);
3847        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
3848        c.behavior = Some(BehaviorSpec {
3849            on_init: Some(PathBuf::from("/etc/passwd")),
3850            ..Default::default()
3851        });
3852        // Path exists check would *succeed* on /etc/passwd (proving the
3853        // sandbox bypass) — value-shape pass must fire first.
3854        let layout = StandardLayout::new()
3855            .with_path_exists(move |p| p == manifest || p == svc || p == Path::new("/etc/passwd"));
3856        let err = layout.verify(&c, &root).unwrap_err();
3857        assert!(
3858            matches!(err, LayoutError::BehaviorViolation { ref caixa, .. } if caixa == "demo"),
3859            "got {err:?}",
3860        );
3861    }
3862
3863    #[test]
3864    fn behavior_empty_callback_is_violation() {
3865        use crate::BehaviorSpec;
3866        let root = PathBuf::from("/tmp/x");
3867        let manifest = root.join("caixa.lisp");
3868        let svc = root.join("servicos/demo.computeunit.yaml");
3869        let mut c = caixa(CaixaKind::Servico);
3870        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
3871        c.behavior = Some(BehaviorSpec {
3872            on_call: Some(PathBuf::new()),
3873            ..Default::default()
3874        });
3875        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
3876        let err = layout.verify(&c, &root).unwrap_err();
3877        assert!(matches!(err, LayoutError::BehaviorViolation { .. }));
3878    }
3879
3880    #[test]
3881    fn upgrade_from_duplicate_surfaces_as_upgrade_violation() {
3882        // Wiring pin: the cross-entry duplicate-`:from` gate in
3883        // `validate_upgrade_from` lands on the same
3884        // `LayoutError::UpgradeViolation` axis the per-entry
3885        // `UpgradeFromEntry::validate` already does (26da2c7), so a
3886        // caixa.lisp with two `(:from "0.1.0" …)` blocks surfaces at
3887        // `feira build` time naming the offending caixa rather than
3888        // silently passing into the wasm-operator's non-deterministic
3889        // dispatch. Mirrors `behavior_empty_callback_is_violation` on
3890        // the peer M2 typed slot.
3891        use crate::{UpgradeFromEntry, UpgradeInstruction};
3892        let root = PathBuf::from("/tmp/x");
3893        let manifest = root.join("caixa.lisp");
3894        let svc = root.join("servicos/demo.computeunit.yaml");
3895        let mut c = caixa(CaixaKind::Servico);
3896        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
3897        c.upgrade_from = vec![
3898            UpgradeFromEntry {
3899                from: "0.1.0".into(),
3900                instructions: vec![UpgradeInstruction::Restart],
3901            },
3902            UpgradeFromEntry {
3903                from: "0.1.0".into(),
3904                instructions: vec![UpgradeInstruction::Restart],
3905            },
3906        ];
3907        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
3908        let err = layout.verify(&c, &root).unwrap_err();
3909        let LayoutError::UpgradeViolation { caixa, issue } = err else {
3910            panic!("expected LayoutError::UpgradeViolation for duplicate `:from`, got {err:?}");
3911        };
3912        assert_eq!(caixa, "demo");
3913        assert!(
3914            issue.contains("0.1.0"),
3915            "UpgradeViolation issue must name the offending `:from` verbatim, got {issue:?}"
3916        );
3917    }
3918
3919    #[test]
3920    fn upgrade_from_downgrade_surfaces_as_upgrade_violation() {
3921        // Wiring pin: the cross-slot precedence gate in
3922        // `validate_upgrade_from_against_versao` lands on the same
3923        // `LayoutError::UpgradeViolation` axis the per-entry and
3924        // cross-entry gates already do (26da2c7, 7c6aef2), so a
3925        // caixa.lisp whose `:upgrade-from :from` is greater than the
3926        // caixa's own `:versao` surfaces at `feira build` time
3927        // naming the offending caixa rather than silently passing
3928        // into the wasm-operator's `:from`-match dispatch where the
3929        // entry would sit dormant forever. Mirrors
3930        // `upgrade_from_duplicate_surfaces_as_upgrade_violation` on
3931        // the peer cross-entry gate.
3932        use crate::{UpgradeFromEntry, UpgradeInstruction};
3933        let root = PathBuf::from("/tmp/x");
3934        let manifest = root.join("caixa.lisp");
3935        let svc = root.join("servicos/demo.computeunit.yaml");
3936        let mut c = caixa(CaixaKind::Servico);
3937        c.versao = "0.1.5".into();
3938        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
3939        c.upgrade_from = vec![UpgradeFromEntry {
3940            from: "0.2.0".into(),
3941            instructions: vec![UpgradeInstruction::Restart],
3942        }];
3943        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
3944        let err = layout.verify(&c, &root).unwrap_err();
3945        let LayoutError::UpgradeViolation { caixa, issue } = err else {
3946            panic!(
3947                "expected LayoutError::UpgradeViolation for downgrade-shaped `:from`, got {err:?}"
3948            );
3949        };
3950        assert_eq!(caixa, "demo");
3951        assert!(
3952            issue.contains("0.2.0") && issue.contains("0.1.5"),
3953            "UpgradeViolation issue must name both `:from` and `:versao` verbatim, got {issue:?}"
3954        );
3955    }
3956
3957    #[test]
3958    fn upgrade_from_equal_to_versao_surfaces_as_upgrade_violation() {
3959        // Self-upgrade no-op arm: `:from "0.1.0"` while
3960        // `:versao "0.1.0"` declares "upgrade from myself to
3961        // myself", which the operator's dispatch either skips
3962        // silently or trivially "succeeds" with no observable
3963        // transition. Surfaces at validate time naming both values
3964        // so the author can fix in one edit.
3965        use crate::{UpgradeFromEntry, UpgradeInstruction};
3966        let root = PathBuf::from("/tmp/x");
3967        let manifest = root.join("caixa.lisp");
3968        let svc = root.join("servicos/demo.computeunit.yaml");
3969        let mut c = caixa(CaixaKind::Servico);
3970        c.versao = "0.1.0".into();
3971        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
3972        c.upgrade_from = vec![UpgradeFromEntry {
3973            from: "0.1.0".into(),
3974            instructions: vec![UpgradeInstruction::Restart],
3975        }];
3976        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
3977        let err = layout.verify(&c, &root).unwrap_err();
3978        let LayoutError::UpgradeViolation { caixa, issue } = err else {
3979            panic!(
3980                "expected LayoutError::UpgradeViolation for self-upgrade `:from == :versao`, got \
3981                 {err:?}"
3982            );
3983        };
3984        assert_eq!(caixa, "demo");
3985        assert!(
3986            issue.contains("0.1.0"),
3987            "UpgradeViolation issue must name the equal `:from`/`:versao` verbatim, got {issue:?}"
3988        );
3989    }
3990
3991    #[test]
3992    fn upgrade_from_strict_upgrade_passes_layout() {
3993        // Positive control for the precedence gate at the
3994        // LayoutInvariants level: a valid `:from < :versao` chain
3995        // (`0.1.0 → 0.2.0`) must not regress into a false-positive
3996        // `UpgradeViolation`. Mirrors `behavior_callback_path_must_exist`'s
3997        // positive-control arm.
3998        use crate::{UpgradeFromEntry, UpgradeInstruction};
3999        let root = PathBuf::from("/tmp/x");
4000        let manifest = root.join("caixa.lisp");
4001        let svc = root.join("servicos/demo.computeunit.yaml");
4002        let mut c = caixa(CaixaKind::Servico);
4003        c.versao = "0.2.0".into();
4004        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
4005        c.upgrade_from = vec![UpgradeFromEntry {
4006            from: "0.1.0".into(),
4007            instructions: vec![UpgradeInstruction::Restart],
4008        }];
4009        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
4010        layout.verify(&c, &root).unwrap();
4011    }
4012
4013    #[test]
4014    fn upgrade_script_path_must_exist() {
4015        use crate::{BehaviorSpec, UpgradeFromEntry, UpgradeInstruction};
4016        use std::path::PathBuf;
4017        let root = PathBuf::from("/tmp/x");
4018        let manifest = root.join("caixa.lisp");
4019        let svc = root.join("servicos/demo.computeunit.yaml");
4020        let on_state_change = root.join("lib/migrations.lisp");
4021        let mut c = caixa(CaixaKind::Servico);
4022        // `:versao` past the entry's `:from` so the cross-slot
4023        // precedence gate (`FromNotBeforeVersao`) lets this case
4024        // through to the path-existence pass under test.
4025        c.versao = "0.2.0".into();
4026        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
4027        // `:on-state-change` declared so the cross-slot composition
4028        // gate (`validate_upgrade_from_against_behavior`) lets the
4029        // `:state-change` entry through to the path-existence pass
4030        // under test. Without the callback the missing-callback gate
4031        // would surface first and the path-existence pass wouldn't be
4032        // exercised.
4033        c.behavior = Some(BehaviorSpec {
4034            on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
4035            ..Default::default()
4036        });
4037        // A `:load-module` precedes the `:state-change` so the entry
4038        // satisfies the within-entry state-change-ordering gate
4039        // (`StateChangeWithoutPriorLoad`) and the path-existence pass
4040        // under test is the gate actually exercised. `:load-module`
4041        // carries no on-disk path, so it adds no existence requirement.
4042        c.upgrade_from = vec![UpgradeFromEntry {
4043            from: "0.1.0".into(),
4044            instructions: vec![
4045                UpgradeInstruction::LoadModule {
4046                    module: "demo".into(),
4047                },
4048                UpgradeInstruction::StateChange {
4049                    script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
4050                },
4051            ],
4052        }];
4053        let manifest_clone = manifest.clone();
4054        let svc_clone = svc.clone();
4055        let on_state_change_clone = on_state_change.clone();
4056        let layout = StandardLayout::new().with_path_exists(move |p| {
4057            p == manifest_clone || p == svc_clone || p == on_state_change_clone
4058        });
4059        let err = layout.verify(&c, &root).unwrap_err();
4060        assert!(matches!(
4061            err,
4062            LayoutError::MissingEntry { kind, .. }
4063                if kind == crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT
4064        ));
4065    }
4066
4067    #[test]
4068    fn layout_missing_entry_kind_m2_consts_pin_canonical_kebab_case_labels() {
4069        // Byte-identity pin: the two per-M2-slot leaf-kind labels the
4070        // [`LayoutError::MissingEntry`] `kind: &'static str`
4071        // discriminator surfaces under (the M2 `:behavior` per-callback
4072        // on-disk-leaf axis, the M2 `:upgrade-from :instructions`
4073        // per-`:state-change` script-path on-disk-leaf axis) route
4074        // through the lifted [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`]
4075        // / [`crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT`]
4076        // consts, so a future rebrand that reaches the const but not
4077        // the production emit / test probe (or vice versa) surfaces
4078        // here at build time rather than at runtime as a downstream
4079        // [`LayoutError::MissingEntry`] `kind: <stale-label>`
4080        // diagnostic mismatch far from the rename's commit. Mirror of
4081        // the peer
4082        // [`crate::aplicacao::tests::contrato_author_key_consts_pin_canonical_kebab_case_labels`]
4083        // (f50c875) and
4084        // [`crate::upgrade::tests::upgrade_instruction_kind_consts_pin_canonical_kebab_case_tags`]
4085        // (56120ef) byte-identity pins on the sibling M3 `:contratos`
4086        // per-entry endpoint-label + M2 `:upgrade-from :instructions`
4087        // per-variant kind-tag axes.
4088        assert_eq!(
4089            crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
4090            "behavior-callback"
4091        );
4092        assert_eq!(
4093            crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
4094            "upgrade-script"
4095        );
4096    }
4097
4098    #[test]
4099    fn layout_missing_entry_kind_m0_consts_pin_canonical_code_slot_labels() {
4100        // Byte-identity pin: the three per-M0-code-slot leaf-kind
4101        // labels the [`LayoutError::MissingEntry`] `kind: &'static
4102        // str` discriminator surfaces under (the `:bibliotecas`
4103        // per-entry axis, the `:exe` per-entry axis, the `:servicos`
4104        // per-entry axis) route through the lifted
4105        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`] /
4106        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] /
4107        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] consts,
4108        // so a future rebrand that reaches the const but not the
4109        // production emit (or vice versa) surfaces here at build time
4110        // rather than at runtime as a downstream
4111        // [`LayoutError::MissingEntry`] `kind: <stale-label>`
4112        // diagnostic mismatch far from the rename's commit. Mirror of
4113        // the peer M2-tier pin
4114        // [`layout_missing_entry_kind_m2_consts_pin_canonical_kebab_case_labels`]
4115        // (95c9c4c) on the sibling `:behavior` / `:upgrade-from`
4116        // per-slot leaf-kind axes.
4117        assert_eq!(
4118            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
4119            "biblioteca"
4120        );
4121        assert_eq!(crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE, "exe");
4122        assert_eq!(crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO, "servico");
4123    }
4124
4125    #[test]
4126    fn layout_missing_entry_kind_m0_consts_align_with_caixa_kind_as_str() {
4127        // Cross-axis byte-identity pin: the two `:kind`-namesake M0
4128        // leaf-kind labels ([`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`]
4129        // = `"biblioteca"`,
4130        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] =
4131        // `"servico"`) must equal [`crate::CaixaKind::Biblioteca`] /
4132        // [`crate::CaixaKind::Servico`]'s
4133        // [`crate::CaixaKind::as_str`] outputs verbatim — the
4134        // substrate's canonical human-readable-kind axis and the
4135        // layout diagnostic's per-slot leaf-kind axis share one
4136        // vocabulary for these two arms by design (both label the
4137        // caixa's code-producing shape by its Portuguese-native
4138        // idiom), so drift between the two lands as a build-time
4139        // pattern-arm miss here rather than as a runtime diagnostic
4140        // that reads inconsistently across `feira build`'s
4141        // per-invocation output.
4142        //
4143        // The third M0 arm ([`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`]
4144        // = `"exe"`) is deliberately *distinct* from
4145        // [`crate::CaixaKind::Binario`]'s [`crate::CaixaKind::as_str`]
4146        // output (`"binario"`) — the `:exe` code slot names the
4147        // per-directory leaf-kind at the `exe/` subtree, whereas
4148        // [`crate::CaixaKind::Binario`] names the caixa's own runtime
4149        // kind. Two axes, two labels — the inequality assertion here
4150        // pins the split so a future accidental collapse of the two
4151        // onto one scalar (a rebrand that reroutes either axis to
4152        // match the other) trips at build time.
4153        assert_eq!(
4154            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
4155            CaixaKind::Biblioteca.as_str()
4156        );
4157        assert_eq!(
4158            crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
4159            CaixaKind::Servico.as_str()
4160        );
4161        assert_ne!(
4162            crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
4163            CaixaKind::Binario.as_str(),
4164            "`exe` leaf-kind label names the per-directory code-slot \
4165             axis; `binario` names the caixa-kind axis — the two must \
4166             not silently collapse onto one scalar"
4167        );
4168    }
4169
4170    #[test]
4171    fn layout_missing_entry_kind_consts_are_pairwise_distinct() {
4172        // Distinctness pin: the five [`LayoutError::MissingEntry`]
4173        // `kind: &'static str` accept-set members
4174        // ([`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`] /
4175        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] /
4176        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] on the
4177        // M0 code-slot arms plus
4178        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`]
4179        // / [`crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT`]
4180        // on the M2 slot arms) must be pairwise distinct — an
4181        // accidental copy-paste flip that reroutes one label's byte-
4182        // string to also match another silently collapses two
4183        // per-slot diagnostics onto one, so an operator running
4184        // `feira build` reads `kind: "biblioteca"` for what should
4185        // have surfaced as a `:behavior :on-init` script-not-found
4186        // diagnostic (or vice versa). This pin catches any such
4187        // flip at build time. Mirror of the peer
4188        // [`crate::render::tests::m2_limits_key_consts_are_pairwise_distinct`]
4189        // / peer distinctness pins on other closed-set typed axes.
4190        let entries: &[(&str, &str)] = &[
4191            (
4192                "LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA",
4193                crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
4194            ),
4195            (
4196                "LAYOUT_MISSING_ENTRY_KIND_EXE",
4197                crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
4198            ),
4199            (
4200                "LAYOUT_MISSING_ENTRY_KIND_SERVICO",
4201                crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
4202            ),
4203            (
4204                "LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK",
4205                crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
4206            ),
4207            (
4208                "LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT",
4209                crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
4210            ),
4211        ];
4212        for (i, (name_a, value_a)) in entries.iter().enumerate() {
4213            for (name_b, value_b) in entries.iter().skip(i + 1) {
4214                assert_ne!(
4215                    value_a, value_b,
4216                    "LAYOUT_MISSING_ENTRY_KIND_* consts must be \
4217                     pairwise-distinct byte-strings — {name_a} and \
4218                     {name_b} both resolve to {value_a:?}"
4219                );
4220            }
4221        }
4222    }
4223
4224    #[test]
4225    fn layout_dir_consts_pin_canonical_directory_names() {
4226        // Scalar-value pin for the three [`crate::render::LAYOUT_DIR_*`]
4227        // consts naming the CSE-invariant per-[`CaixaKind`]
4228        // on-disk-directory-name axes the substrate's layout invariants
4229        // pin (`lib/` for [`CaixaKind::Biblioteca`], `exe/` for
4230        // [`CaixaKind::Binario`], `servicos/` for [`CaixaKind::Servico`]).
4231        // A future rebrand of any of the three on-disk directory landing
4232        // conventions must reach this pin — the const-edit lands on one
4233        // arm, the assertion here re-pins the new byte-string, and every
4234        // downstream consumer (the caixa-feira `init` / `fmt` / `lint` /
4235        // `tofu` scaffolders, the [`crate::LayoutInvariants::verify`]
4236        // sandbox reconstruction, the future
4237        // `feira app deploy`-cluster scaffolder) picks up the new
4238        // directory name at build time. Mirror of the peer
4239        // [`layout_missing_entry_kind_m0_consts_pin_canonical_code_slot_labels`]
4240        // (fe2a898) on the sibling
4241        // [`crate::LayoutError::MissingEntry`] `kind:` discriminator
4242        // axis this on-disk-directory axis composes with.
4243        assert_eq!(crate::render::LAYOUT_DIR_LIB, "lib");
4244        assert_eq!(crate::render::LAYOUT_DIR_EXE, "exe");
4245        assert_eq!(crate::render::LAYOUT_DIR_SERVICOS, "servicos");
4246    }
4247
4248    #[test]
4249    fn layout_dir_consts_are_pairwise_distinct() {
4250        // Distinctness pin: the three per-[`CaixaKind`]
4251        // on-disk-directory-name arms must resolve to pairwise-distinct
4252        // byte-strings — a future accidental copy-paste flip that
4253        // reroutes any one of the three onto another's value silently
4254        // collapses two per-kind on-disk sandboxes onto one, so
4255        // [`crate::LayoutInvariants::verify`] would gate a
4256        // [`CaixaKind::Binario`] caixa's `:exe` entries against the
4257        // wrong sub-tree (or a `:kind Servico` caixa's `:servicos`
4258        // entries against `lib/` and pass every entry `feira build`
4259        // should have rejected as [`crate::LayoutError::ServicoOutsideDir`]).
4260        // Mirror of the peer
4261        // [`layout_missing_entry_kind_consts_are_pairwise_distinct`]
4262        // (fe2a898) on the sibling leaf-kind label accept-set.
4263        let entries: &[(&str, &str)] = &[
4264            ("LAYOUT_DIR_LIB", crate::render::LAYOUT_DIR_LIB),
4265            ("LAYOUT_DIR_EXE", crate::render::LAYOUT_DIR_EXE),
4266            ("LAYOUT_DIR_SERVICOS", crate::render::LAYOUT_DIR_SERVICOS),
4267        ];
4268        for (i, (name_a, value_a)) in entries.iter().enumerate() {
4269            for (name_b, value_b) in entries.iter().skip(i + 1) {
4270                assert_ne!(
4271                    value_a, value_b,
4272                    "LAYOUT_DIR_* consts must be pairwise-distinct \
4273                     byte-strings — {name_a} and {name_b} both resolve \
4274                     to {value_a:?}"
4275                );
4276            }
4277        }
4278    }
4279
4280    #[test]
4281    fn layout_dir_exe_matches_layout_missing_entry_kind_exe() {
4282        // Cross-axis byte-identity pin: [`crate::render::LAYOUT_DIR_EXE`]
4283        // (the on-disk-directory-name arm) equals
4284        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] (the
4285        // [`LayoutError::MissingEntry`] `kind:` leaf-kind categorization
4286        // arm) verbatim — the M0 `:kind Binario` on-disk-directory axis
4287        // and the [`crate::LayoutError::MissingEntry`] `kind:` leaf-kind
4288        // discriminator name the same three-byte sub-tree (`exe/`), a
4289        // coincidence [`crate::LayoutInvariants::verify`] itself relies
4290        // on: it joins `root` with [`crate::render::LAYOUT_DIR_EXE`] to
4291        // reconstruct `exe_dir` and emits [`crate::LayoutError::MissingEntry
4292        // { kind: LAYOUT_MISSING_ENTRY_KIND_EXE, path: <under exe_dir> }`]
4293        // for every non-resolving entry. Making the coincidence
4294        // load-bearing means a future rebrand touching either axis
4295        // without the other (a per-consumer disambiguation collapsing
4296        // the leaf-kind label onto `"binary"` while the directory stays
4297        // `"exe"`, or vice versa) trips at caixa-core build time rather
4298        // than surfacing at runtime as a mismatched
4299        // [`crate::LayoutInvariants::verify`] diagnostic whose `kind:`
4300        // reads one label while the `path:` sits under a differently-named
4301        // sub-tree.
4302        assert_eq!(
4303            crate::render::LAYOUT_DIR_EXE,
4304            crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
4305            "LAYOUT_DIR_EXE must equal LAYOUT_MISSING_ENTRY_KIND_EXE — \
4306             both name the M0 `:kind Binario` sub-tree by the same \
4307             three-byte scalar"
4308        );
4309    }
4310
4311    #[test]
4312    fn layout_dir_bib_is_distinct_from_layout_missing_entry_kind_bib() {
4313        // Cross-axis distinctness pin: [`crate::render::LAYOUT_DIR_LIB`]
4314        // (`"lib"`, the Cargo-style abbreviated on-disk directory name)
4315        // is *deliberately* distinct from
4316        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`]
4317        // (`"biblioteca"`, the full-form Portuguese-native leaf-kind
4318        // label) — the substrate splits the on-disk convention terse
4319        // (`lib/`) from the diagnostic vocabulary full (`biblioteca`),
4320        // matching Cargo's `src/lib.rs` abbreviation of the `library`
4321        // crate-type discriminator. A future accidental collapse of the
4322        // two axes onto one scalar (a rebrand aligning either arm with
4323        // the other for schema-clarity, an English-uniformity pass that
4324        // renames `LAYOUT_DIR_LIB` to `LAYOUT_DIR_BIBLIOTECA` or the
4325        // diagnostic label to `"lib"`) would silently reroute either
4326        // consumer onto the other's byte-string. This pin catches the
4327        // collapse at build time. Peer of the sibling
4328        // [`layout_missing_entry_kind_m0_consts_align_with_caixa_kind_as_str`]
4329        // (fe2a898) that pins the analogous *equality* between the M0
4330        // `:kind Biblioteca` diagnostic-label arm and
4331        // [`crate::CaixaKind::Biblioteca`]'s [`crate::CaixaKind::as_str`]
4332        // output — the two pins jointly encode the "which of the three
4333        // Biblioteca-related scalars are load-bearing-equal, which are
4334        // load-bearing-distinct" invariant across the substrate's
4335        // per-kind vocabulary.
4336        assert_ne!(
4337            crate::render::LAYOUT_DIR_LIB,
4338            crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
4339            "LAYOUT_DIR_LIB (`\"lib\"`) and LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA \
4340             (`\"biblioteca\"`) name two distinct axes — the on-disk \
4341             directory convention (Cargo-style abbreviated) and the \
4342             layout-diagnostic leaf-kind label (full-form Portuguese) — \
4343             and must not silently collapse onto one scalar"
4344        );
4345    }
4346
4347    #[test]
4348    fn layout_dir_servicos_is_distinct_from_layout_missing_entry_kind_servico() {
4349        // Cross-axis distinctness pin: [`crate::render::LAYOUT_DIR_SERVICOS`]
4350        // (`"servicos"`, the Portuguese-*plural* on-disk directory
4351        // name) is *deliberately* distinct from
4352        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`]
4353        // (`"servico"`, the singular leaf-kind label) — the on-disk
4354        // sub-tree houses one-or-more ComputeUnit YAML descriptors per
4355        // caixa (hence the plural), the diagnostic label names the
4356        // caixa's own kind (singular). A future accidental collapse
4357        // onto one scalar (a per-consumer disambiguation aligning the
4358        // two, a hypothetical English-uniformity pass renaming
4359        // `"servicos"` → `"services"` while retaining `"servico"` on
4360        // the diagnostic arm — or vice versa) would silently reroute
4361        // either consumer onto the other's byte-string. Peer of the
4362        // sibling [`layout_dir_bib_is_distinct_from_layout_missing_entry_kind_bib`]
4363        // pin on the M0 `:kind Biblioteca` split axis; two of the three
4364        // per-kind on-disk / leaf-kind splits carry a distinctness
4365        // pin here, the third ([`crate::render::LAYOUT_DIR_EXE`] vs
4366        // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`]) carries an
4367        // equality pin under
4368        // [`layout_dir_exe_matches_layout_missing_entry_kind_exe`].
4369        assert_ne!(
4370            crate::render::LAYOUT_DIR_SERVICOS,
4371            crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
4372            "LAYOUT_DIR_SERVICOS (`\"servicos\"`, plural on-disk sub-tree) \
4373             and LAYOUT_MISSING_ENTRY_KIND_SERVICO (`\"servico\"`, singular \
4374             leaf-kind label) name two distinct axes and must not silently \
4375             collapse onto one scalar"
4376        );
4377    }
4378
4379    #[test]
4380    fn layout_invariants_reconstruct_sandbox_roots_through_lifted_layout_dir_consts() {
4381        // Production-through-const pin: [`LayoutInvariants::verify`]
4382        // routes its three per-kind sandbox-root joins
4383        // (`root.join(LAYOUT_DIR_LIB)` for the `:kind Biblioteca`
4384        // default `lib/<nome>.lisp` reconstruction, `root.join(LAYOUT_DIR_EXE)`
4385        // for the [`LayoutError::ExeOutsideDir`] gate,
4386        // `root.join(LAYOUT_DIR_SERVICOS)` for the
4387        // [`LayoutError::ServicoOutsideDir`] gate) through the three
4388        // lifted consts, not through inline `"lib"` / `"exe"` /
4389        // `"servicos"` `&str` literals. This test drives the
4390        // [`LayoutError::ExeOutsideDir`] arm through a `:kind Binario`
4391        // caixa whose declared `:exe` entry deliberately escapes
4392        // `root.join(LAYOUT_DIR_EXE)` (a sibling `bin/tool` path) —
4393        // if the production emit reads the wrong const (or reverts to
4394        // an inline literal that drifts from the const) the diagnostic
4395        // arm surfaces the wrong variant, catching the drift at build
4396        // time rather than as a per-invocation runtime mismatch.
4397        //
4398        // Mirror of the peer production-through-const pin
4399        // [`crate::dep::tests::validate_no_self_dep_deps_field_routes_through_dep_author_key`]
4400        // (4da6fba) on the sibling M0 `:deps` `list:` diagnostic axis.
4401        use std::path::PathBuf;
4402        let root = PathBuf::from("/tmp/x");
4403        let manifest = root.join("caixa.lisp");
4404        let bin_entry_outside = root.join("bin/tool");
4405        let mut c = caixa(CaixaKind::Binario);
4406        c.exe = vec!["bin/tool".into()];
4407        let manifest_clone = manifest.clone();
4408        let outside_clone = bin_entry_outside.clone();
4409        let layout = StandardLayout::new()
4410            .with_path_exists(move |p| p == manifest_clone || p == outside_clone);
4411        let err = layout.verify(&c, &root).unwrap_err();
4412        match err {
4413            LayoutError::ExeOutsideDir(path) => {
4414                assert_eq!(
4415                    path, bin_entry_outside,
4416                    "ExeOutsideDir must carry the resolved `:exe` entry that \
4417                     escapes `root.join(LAYOUT_DIR_EXE)`"
4418                );
4419                // Byte-identity check: the escape must be against the
4420                // lifted `LAYOUT_DIR_EXE` sub-tree, not a stale inline
4421                // literal — a future const-edit that drifts from `"exe"`
4422                // reroutes `exe_dir` off the sandbox `bin/tool` escapes
4423                // from, and this pattern-arm miss re-surfaces here.
4424                assert!(
4425                    !path.starts_with(root.join(crate::render::LAYOUT_DIR_EXE)),
4426                    "resolved `:exe` entry {path:?} must escape the \
4427                     `root.join(LAYOUT_DIR_EXE)` sub-tree the production \
4428                     emit uses to gate the [`LayoutError::ExeOutsideDir`] arm"
4429                );
4430            }
4431            other => panic!("expected ExeOutsideDir, got {other:?}"),
4432        }
4433    }
4434
4435    #[test]
4436    fn upgrade_state_change_without_behavior_callback_surfaces_as_upgrade_violation() {
4437        // Wiring pin for the cross-slot composition gate
4438        // (`validate_upgrade_from_against_behavior`): a caixa whose
4439        // `:upgrade-from` declares a `(:state-change "lib/m.lisp")`
4440        // instruction but does not declare `:behavior :on-state-change`
4441        // surfaces at `feira build` time as a `LayoutError::UpgradeViolation`
4442        // naming the offending caixa + the entry's `:from` + the
4443        // offending script — not at hot-upgrade dispatch when the
4444        // operator reaches for the missing callback. Mirrors
4445        // `upgrade_from_downgrade_surfaces_as_upgrade_violation` on the
4446        // peer `:from` ↔ `:versao` cross-slot precedence gate.
4447        use crate::{UpgradeFromEntry, UpgradeInstruction};
4448        use std::path::PathBuf;
4449        let root = PathBuf::from("/tmp/x");
4450        let manifest = root.join("caixa.lisp");
4451        let svc = root.join("servicos/demo.computeunit.yaml");
4452        let mut c = caixa(CaixaKind::Servico);
4453        c.versao = "0.2.0".into();
4454        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
4455        // `:behavior` is None (the canonical "I added the upgrade path
4456        // but never declared :behavior" footgun the gate closes); a
4457        // peer arm covers the BehaviorSpec-Some-but-on-state-change-
4458        // None shape in `upgrade::tests::behavior_gate_rejects_state_
4459        // change_when_on_state_change_is_none`.
4460        c.upgrade_from = vec![UpgradeFromEntry {
4461            from: "0.1.0".into(),
4462            instructions: vec![
4463                UpgradeInstruction::LoadModule {
4464                    module: "demo".into(),
4465                },
4466                UpgradeInstruction::StateChange {
4467                    script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
4468                },
4469            ],
4470        }];
4471        let manifest_clone = manifest.clone();
4472        let svc_clone = svc.clone();
4473        let layout =
4474            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
4475        let err = layout.verify(&c, &root).unwrap_err();
4476        match err {
4477            LayoutError::UpgradeViolation { caixa, issue } => {
4478                assert_eq!(caixa, "demo", "diagnostic must name the offending caixa");
4479                assert!(
4480                    issue.contains(crate::render::M2_BEHAVIOR_AUTHOR_KEY_ON_STATE_CHANGE),
4481                    "diagnostic must name the missing callback slot for self-locating fix, \
4482                     got {issue:?}"
4483                );
4484                assert!(
4485                    issue.contains("0.1.0"),
4486                    "diagnostic must name the offending entry's :from, got {issue:?}"
4487                );
4488                assert!(
4489                    issue.contains("v01-to-v02.lisp"),
4490                    "diagnostic must name the offending :script for self-locating fix, \
4491                     got {issue:?}"
4492                );
4493            }
4494            other => panic!("expected UpgradeViolation, got {other:?}"),
4495        }
4496    }
4497
4498    #[test]
4499    fn supervisor_must_have_children() {
4500        use crate::RestartStrategy;
4501        let root = PathBuf::from("/tmp/x");
4502        let manifest = root.join("caixa.lisp");
4503        let manifest_clone = manifest.clone();
4504        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
4505        let mut c = caixa(CaixaKind::Supervisor);
4506        c.estrategia = Some(RestartStrategy::OneForOne);
4507        c.max_restarts = Some(5);
4508        // No children → should fail
4509        let err = layout.verify(&c, &root).unwrap_err();
4510        assert!(matches!(err, LayoutError::SupervisorViolation { .. }));
4511    }
4512
4513    #[test]
4514    fn supervisor_self_referential_child_is_violation() {
4515        // A Supervisor whose `:children` names its own `:nome` is a
4516        // one-node supervision cycle. The cross-slot gate fires at
4517        // verify time, surfacing as a SupervisorViolation that names the
4518        // offending supervisor — not at the cluster apply far from
4519        // source. The `caixa()` helper's `:nome` is "demo".
4520        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
4521        let root = PathBuf::from("/tmp/x");
4522        let manifest = root.join("caixa.lisp");
4523        let manifest_clone = manifest.clone();
4524        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
4525        let mut c = caixa(CaixaKind::Supervisor);
4526        c.estrategia = Some(RestartStrategy::OneForOne);
4527        c.max_restarts = Some(5);
4528        c.children = vec![
4529            ChildSpec {
4530                caixa: "worker".into(),
4531                versao: "^0.1".into(),
4532                restart: RestartPolicy::Permanent,
4533            },
4534            ChildSpec {
4535                caixa: "demo".into(),
4536                versao: "^0.1".into(),
4537                restart: RestartPolicy::Permanent,
4538            },
4539        ];
4540        let err = layout.verify(&c, &root).unwrap_err();
4541        let LayoutError::SupervisorViolation { caixa, issue } = err else {
4542            panic!("expected SupervisorViolation for self-referential child, got {err:?}");
4543        };
4544        assert_eq!(caixa, "demo");
4545        assert!(
4546            issue.contains("demo") && issue.contains("itself"),
4547            "issue must name the self-supervising caixa, got {issue:?}"
4548        );
4549    }
4550
4551    #[test]
4552    fn supervisor_distinct_children_pass_self_supervision_gate() {
4553        // Positive control: a Supervisor whose children are all distinct
4554        // from its own `:nome` verifies cleanly.
4555        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
4556        let root = PathBuf::from("/tmp/x");
4557        let manifest = root.join("caixa.lisp");
4558        let manifest_clone = manifest.clone();
4559        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
4560        let mut c = caixa(CaixaKind::Supervisor);
4561        c.estrategia = Some(RestartStrategy::OneForOne);
4562        c.max_restarts = Some(5);
4563        c.children = vec![ChildSpec {
4564            caixa: "worker".into(),
4565            versao: "^0.1".into(),
4566            restart: RestartPolicy::Permanent,
4567        }];
4568        layout.verify(&c, &root).unwrap();
4569    }
4570
4571    #[test]
4572    fn cross_slot_self_edge_gates_route_parent_nome_through_lifted_accessor() {
4573        // Composition pin: every cross-slot self-edge gate fired from
4574        // `LayoutInvariants::verify` — the supervision-tree arm's
4575        // `crate::supervisor::validate_no_self_supervision` call, the
4576        // Aplicacao arm's `crate::aplicacao::validate_no_self_membership`
4577        // call, and the dep-graph arm's
4578        // `crate::dep::validate_no_self_dep` call — must key its
4579        // `parent_nome` arg off the typed [`Caixa::nome`] accessor, not
4580        // the raw `&caixa.nome` `&String`-borrow of the underlying
4581        // field.
4582        //
4583        // Structurally: a rename of the storage field or a hypothetical
4584        // accessor rebrand (a per-cluster alias table pinned through a
4585        // future `:placement`-scoped slot, the M4 CR materializer's
4586        // per-CR namespace-qualified rewrite, a `:nome-suffix` overlay
4587        // the MESH-COMPOSITION §III.2 roadmap acknowledges) would land
4588        // through the accessor by construction; a raw-borrow bypass
4589        // would silently disagree with every peer consumer that already
4590        // routes through `caixa.nome()` (the caixa-mesh 980c059,
4591        // caixa-helm 22461ef, caixa-flux 162e2e2, caixa-crd 61d3429,
4592        // caixa-feira ef83332 raw-borrow converges), reintroducing the
4593        // drift surface the sibling converges closed. Each arm fires
4594        // its per-kind `LayoutError` variant (`SupervisorViolation` /
4595        // `AplicacaoViolation` / `DepsViolation`) whose `caixa` field
4596        // carries the offending parent name verbatim through
4597        // `caixa.nome().clone()`; asserting the field equals
4598        // `caixa.nome()` on the mutated fixture pins the accessor-
4599        // routed parent-nome projection at every call site — a future
4600        // silent detour that had the gate observe a stale / aliased
4601        // name at the arg boundary would surface here as a
4602        // `caixa != "demo"` inequality.
4603        //
4604        // Peer of the sibling per-caixa-crate `nome`-arg raw-borrow
4605        // convergence pin discipline (54bf2f3 / 22461ef / 162e2e2 on the
4606        // renderer crates; ef83332 on the CLI) — extends the "one typed
4607        // dispatch per `:nome` consumer" discipline onto the substrate's
4608        // own [`LayoutInvariants::verify`] cross-slot self-edge gate
4609        // wire-up on all three typed-name-graph kinds.
4610        use crate::{
4611            ChildSpec, Dep, Membro, Placement, PlacementStrategy, RestartPolicy, RestartStrategy,
4612        };
4613        let root = PathBuf::from("/tmp/x");
4614        let manifest = root.join("caixa.lisp");
4615        let manifest_clone = manifest.clone();
4616        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
4617
4618        // Supervisor arm — the `caixa()` helper's `:nome` is "demo",
4619        // and the accessor's return `caixa.nome()` must equal the
4620        // parent-nome that the self-supervision gate observes.
4621        let mut sup = caixa(CaixaKind::Supervisor);
4622        sup.estrategia = Some(RestartStrategy::OneForOne);
4623        sup.max_restarts = Some(5);
4624        sup.children = vec![ChildSpec {
4625            caixa: "demo".into(),
4626            versao: "^0.1".into(),
4627            restart: RestartPolicy::Permanent,
4628        }];
4629        let parent_nome_via_accessor = sup.nome();
4630        assert_eq!(
4631            parent_nome_via_accessor, "demo",
4632            "the caixa() fixture helper's `:nome` must be \"demo\" — \
4633             the accessor's return is the pin's ground truth for the \
4634             cross-slot gate's parent-nome arg",
4635        );
4636        let err = layout.verify(&sup, &root).unwrap_err();
4637        let LayoutError::SupervisorViolation { caixa: c_nome, .. } = err else {
4638            panic!("expected SupervisorViolation for self-referential child, got {err:?}");
4639        };
4640        assert_eq!(
4641            c_nome, parent_nome_via_accessor,
4642            "the SupervisorViolation's `caixa` field must equal \
4643             `sup.nome()` — the cross-slot self-supervision gate's \
4644             `parent_nome` arg must route through the lifted \
4645             [`Caixa::nome`] accessor, not the raw `&caixa.nome` \
4646             `&String`-borrow of the underlying field",
4647        );
4648
4649        // Aplicacao arm — same discipline on the peer typed-name-graph
4650        // kind. Constructed alongside the supervisor arm so any future
4651        // accessor drift lands on both arms in the same pin.
4652        let mut app = caixa(CaixaKind::Aplicacao);
4653        app.placement = Some(Placement {
4654            estrategia: PlacementStrategy::Replicated,
4655            clusters: vec!["rio".into()],
4656            affinity: None,
4657            shard_key: None,
4658        });
4659        app.membros = vec![Membro {
4660            caixa: "demo".into(),
4661            versao: "^0.1".into(),
4662        }];
4663        let parent_nome_via_accessor = app.nome();
4664        assert_eq!(
4665            parent_nome_via_accessor, "demo",
4666            "the caixa() fixture helper's `:nome` must be \"demo\" on \
4667             the Aplicacao arm too — same accessor-ground-truth as the \
4668             sibling supervisor arm above",
4669        );
4670        let err = layout.verify(&app, &root).unwrap_err();
4671        let LayoutError::AplicacaoViolation { caixa: c_nome, .. } = err else {
4672            panic!("expected AplicacaoViolation for self-referential membro, got {err:?}");
4673        };
4674        assert_eq!(
4675            c_nome, parent_nome_via_accessor,
4676            "the AplicacaoViolation's `caixa` field must equal \
4677             `app.nome()` — the cross-slot self-membership gate's \
4678             `parent_nome` arg must route through the lifted \
4679             [`Caixa::nome`] accessor, not the raw `&caixa.nome` \
4680             `&String`-borrow of the underlying field",
4681        );
4682
4683        // Dep-graph arm — third typed-name-graph kind on the
4684        // `parent_nome` arg boundary. Same discipline as the peer
4685        // supervision-tree and Aplicacao-membership arms above.
4686        // Constructed alongside so any future accessor drift lands on
4687        // all three arms in the same pin. Needs a distinct layout
4688        // fixture from the supervisor / aplicacao arms above because
4689        // the `Biblioteca` kind's code-path existence gate demands the
4690        // canonical `lib/<nome>.lisp` path also `exists`, so the shim
4691        // covers both `caixa.lisp` and `lib/demo.lisp`.
4692        let default_lib = root.join("lib").join("demo.lisp");
4693        let manifest_dep = manifest.clone();
4694        let default_lib_clone = default_lib.clone();
4695        let layout_dep = StandardLayout::new()
4696            .with_path_exists(move |p| p == manifest_dep || p == default_lib_clone);
4697        let mut lib = caixa(CaixaKind::Biblioteca);
4698        lib.deps = vec![Dep::simple("demo", "^0.1")];
4699        let parent_nome_via_accessor = lib.nome();
4700        assert_eq!(
4701            parent_nome_via_accessor, "demo",
4702            "the caixa() fixture helper's `:nome` must be \"demo\" on \
4703             the Biblioteca arm too — same accessor-ground-truth as the \
4704             sibling supervisor + Aplicacao arms above",
4705        );
4706        let err = layout_dep.verify(&lib, &root).unwrap_err();
4707        let LayoutError::DepsViolation { caixa: c_nome, .. } = err else {
4708            panic!("expected DepsViolation for self-referential :deps entry, got {err:?}");
4709        };
4710        assert_eq!(
4711            c_nome, parent_nome_via_accessor,
4712            "the DepsViolation's `caixa` field must equal \
4713             `lib.nome()` — the cross-slot self-dep gate's `parent_nome` \
4714             arg must route through the lifted [`Caixa::nome`] accessor, \
4715             not the raw `&caixa.nome` `&String`-borrow of the underlying \
4716             field",
4717        );
4718    }
4719
4720    #[test]
4721    fn upgrade_against_versao_gate_routes_current_versao_through_lifted_accessor() {
4722        // Composition pin: the cross-slot `:upgrade-from :from` ↔
4723        // `:versao` precedence gate fired from
4724        // `LayoutInvariants::verify` — the
4725        // `crate::upgrade::validate_upgrade_from_against_versao` call —
4726        // must key its `versao` arg off the typed [`Caixa::versao`]
4727        // accessor, not the raw `&caixa.versao` `&String`-borrow of
4728        // the underlying field.
4729        //
4730        // Same "arg-boundary reads through the lifted accessor"
4731        // discipline as the sibling
4732        // [`cross_slot_self_edge_gates_route_parent_nome_through_lifted_accessor`]
4733        // pin above on the `:nome`-arg axis of the three typed-name-
4734        // graph self-edge gates — extended here onto the `:versao`-arg
4735        // axis of the substrate's remaining `LayoutInvariants::verify`
4736        // cross-slot arg-carrying call site. Structurally byte-equal
4737        // today (the accessor is `pub fn versao(&self) -> &str { &self.versao }`,
4738        // so both paths coerce to the same `&str`); the pin catches a
4739        // future silent detour (an accessor rebrand that no longer
4740        // shipped the raw slot verbatim — a per-`:edicao` overlay,
4741        // a promotion of `:versao` to a `CaixaVersion` newtype with a
4742        // canonicalizing accessor, an M4 CR-materializer-side pinning
4743        // through a resolver-annotated `:versao-resolved` slot) that
4744        // would silently split the substrate's own precedence gate
4745        // from every peer consumer already routing `:versao` reads
4746        // through the lifted accessor.
4747        //
4748        // The gate fires an `UpgradeViolation { caixa, issue }` when a
4749        // `:upgrade-from` entry's `:from` is not strictly less than
4750        // the top-level `:versao` — the `issue` string names both the
4751        // offending prior version and the current version verbatim,
4752        // so asserting the substring `caixa.versao()` appears in the
4753        // fired diagnostic pins the accessor-routed current-versao
4754        // projection at the arg boundary. A raw-borrow bypass would
4755        // still surface the same bytes today, but the presence of
4756        // this pin makes any future divergence between the accessor's
4757        // return and the raw slot's contents a build-time failure at
4758        // this call site.
4759        use crate::{UpgradeFromEntry, UpgradeInstruction};
4760        let root = PathBuf::from("/tmp/x");
4761        let manifest = root.join("caixa.lisp");
4762        let servico_path = root.join("servicos").join("demo.computeunit.yaml");
4763        let manifest_clone = manifest.clone();
4764        let servico_clone = servico_path.clone();
4765        let layout = StandardLayout::new()
4766            .with_path_exists(move |p| p == manifest_clone || p == servico_clone);
4767        let mut svc = caixa(CaixaKind::Servico);
4768        svc.versao = "0.1.0".into();
4769        svc.servicos = vec!["servicos/demo.computeunit.yaml".into()];
4770        // `:from` >= current `:versao` — trips the precedence gate the
4771        // `validate_upgrade_from_against_versao` cross-slot call
4772        // enforces. `:load-module` carries no on-disk path so the
4773        // path-existence gate downstream stays inert; the precedence
4774        // gate is what fires. No `:on-state-change` needed because the
4775        // instruction list carries no `:state-change` entry, so the
4776        // sibling `validate_upgrade_from_against_behavior` gate is
4777        // inert too.
4778        svc.upgrade_from = vec![UpgradeFromEntry {
4779            from: "0.2.0".into(),
4780            instructions: vec![UpgradeInstruction::LoadModule {
4781                module: "demo".into(),
4782            }],
4783        }];
4784        let current_versao_via_accessor = svc.versao().to_string();
4785        assert_eq!(
4786            current_versao_via_accessor, "0.1.0",
4787            "the mutated fixture's `:versao` must be observable through \
4788             the accessor before layout verification fires — a drift on \
4789             `Caixa::versao` would surface here as a `!= \"0.1.0\"` \
4790             inequality",
4791        );
4792        let err = layout.verify(&svc, &root).unwrap_err();
4793        let LayoutError::UpgradeViolation {
4794            caixa: c_nome,
4795            issue,
4796        } = err
4797        else {
4798            panic!("expected UpgradeViolation for :from >= :versao, got {err:?}");
4799        };
4800        assert_eq!(c_nome, svc.nome(), "wrap envelope names the caixa");
4801        assert!(
4802            issue.contains(&current_versao_via_accessor),
4803            "the UpgradeViolation's `issue` must quote the current \
4804             `:versao` byte-string verbatim — the cross-slot precedence \
4805             gate's `versao` arg must route through the lifted \
4806             [`Caixa::versao`] accessor, not the raw `&caixa.versao` \
4807             `&String`-borrow of the underlying field. issue: {issue}",
4808        );
4809    }
4810
4811    #[test]
4812    fn layout_violation_envelopes_carry_caixa_nome_through_lifted_accessor() {
4813        // Wrap-envelope drift-detection pin: every per-axis
4814        // `LayoutError::*Violation { caixa, issue }` envelope fired
4815        // from `LayoutInvariants::verify` must key its offending-caixa
4816        // field off the typed [`Caixa::nome`] accessor's
4817        // `.to_string()` extension, not the raw
4818        // `caixa.nome.clone()` `String::clone()` of the underlying
4819        // field. Structurally byte-equal today (each accessor is
4820        // `pub fn nome(&self) -> &str { &self.nome }`, so
4821        // `caixa.nome().to_string()` and `caixa.nome.clone()` produce
4822        // the same bytes); the pin catches a future silent detour
4823        // (an accessor rebrand that no longer shipped the raw slot
4824        // verbatim — a per-cluster alias table pinned through a
4825        // future `:placement`-scoped slot, the M4 CR materializer's
4826        // per-CR namespace-qualified rewrite, a `:nome-suffix`
4827        // overlay the MESH-COMPOSITION §III.2 roadmap acknowledges)
4828        // that would silently split the substrate's own layout
4829        // invariant verifier's diagnostic surface from every peer
4830        // caixa-crate consumer that already routes `:nome` reads
4831        // through the lifted accessor (the caixa-mesh 980c059,
4832        // caixa-helm 22461ef, caixa-flux 162e2e2, caixa-crd 61d3429,
4833        // caixa-feira ef83332 raw-borrow converges).
4834        //
4835        // Exercises a representative variant on each of the three
4836        // wrap-envelope arm shapes the substrate's per-axis fan-out
4837        // carries: (1) `LayoutError::NomeViolation` (the leading arm
4838        // in the `verify` order — the `:nome` axis's DNS-1123 shape
4839        // gate fires immediately after the manifest-existence gate),
4840        // (2) `LayoutError::BinarioWithoutExe` (a tuple-variant on
4841        // the kind-coherence family — different envelope shape than
4842        // the struct-variant `*Violation { caixa, issue }` family
4843        // but the same converge target on the `caixa.nome().to_string()`
4844        // arg), and (3) `LayoutError::ServicoWithoutServicos` (the
4845        // sibling tuple-variant on the same kind-coherence family).
4846        // Together they cover the two `LayoutError` envelope shapes
4847        // (struct-variant + tuple-variant) the layout invariants file
4848        // emits on `:nome`-carrying arms.
4849        //
4850        // Peer of the sibling
4851        // [`cross_slot_self_edge_gates_route_parent_nome_through_lifted_accessor`]
4852        // pin above — extends the "wrap-envelope `caixa:` field
4853        // reads through the lifted accessor" discipline from the
4854        // cross-slot self-edge gates' `parent_nome` arg boundary
4855        // onto the per-axis `LayoutError::*Violation` envelope's
4856        // `caixa:` field boundary.
4857
4858        let root = PathBuf::from("/tmp/x");
4859        let manifest = root.join("caixa.lisp");
4860        let manifest_clone = manifest.clone();
4861        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
4862
4863        // (1) `NomeViolation` on the struct-variant envelope: force a
4864        // DNS-1123-invalid `:nome` (uppercase byte — `is_dns_1123_label`
4865        // rejects) and assert the fired envelope's `caixa:` field
4866        // byte-equals `c.nome().to_string()`.
4867        let mut c = caixa(CaixaKind::Biblioteca);
4868        c.nome = "BAD_NAME".into();
4869        c.bibliotecas = vec!["lib/demo.lisp".into()];
4870        let expected_nome_via_accessor = c.nome().to_string();
4871        assert_eq!(
4872            expected_nome_via_accessor, "BAD_NAME",
4873            "the mutated fixture's `:nome` must be observable through \
4874             the accessor before layout verification fires — a drift \
4875             on `Caixa::nome` would surface here as a `!= \"BAD_NAME\"` \
4876             inequality",
4877        );
4878        let err = layout.verify(&c, &root).unwrap_err();
4879        let LayoutError::NomeViolation { caixa: c_nome, .. } = err else {
4880            panic!("expected NomeViolation for DNS-1123-invalid :nome, got {err:?}");
4881        };
4882        assert_eq!(
4883            c_nome, expected_nome_via_accessor,
4884            "the NomeViolation's `caixa` field must equal \
4885             `c.nome().to_string()` — the wrap envelope's per-axis \
4886             projection must route through the lifted [`Caixa::nome`] \
4887             accessor's `.to_string()` extension, not the raw \
4888             `caixa.nome.clone()` `String::clone()` of the underlying \
4889             field",
4890        );
4891
4892        // (2) `BinarioWithoutExe` on the tuple-variant envelope: a
4893        // Binario-kind caixa with an empty `:exe` list fires the
4894        // kind-coherence gate whose payload is a bare `String`, so the
4895        // pattern is `LayoutError::BinarioWithoutExe(String)` rather
4896        // than the struct-variant `{ caixa, issue }` family. The
4897        // converge target is the same — `caixa.nome().to_string()` — but
4898        // the envelope shape is different, so the pin exercises both.
4899        let mut c = caixa(CaixaKind::Binario);
4900        // `:exe` empty is the trigger — the fixture helper defaults
4901        // it to `vec![]`, so no mutation is needed.
4902        c.nome = "binario-demo".into();
4903        let expected_nome_via_accessor = c.nome().to_string();
4904        assert_eq!(
4905            expected_nome_via_accessor, "binario-demo",
4906            "the mutated fixture's `:nome` must be observable through \
4907             the accessor before layout verification fires",
4908        );
4909        let err = layout.verify(&c, &root).unwrap_err();
4910        let LayoutError::BinarioWithoutExe(c_nome) = err else {
4911            panic!("expected BinarioWithoutExe for empty :exe list on Binario kind, got {err:?}");
4912        };
4913        assert_eq!(
4914            c_nome, expected_nome_via_accessor,
4915            "the BinarioWithoutExe's payload must equal \
4916             `c.nome().to_string()` — the tuple-variant envelope's \
4917             per-axis projection must route through the lifted \
4918             [`Caixa::nome`] accessor's `.to_string()` extension, not \
4919             the raw `caixa.nome.clone()` `String::clone()` of the \
4920             underlying field",
4921        );
4922
4923        // (3) `ServicoWithoutServicos` on the sibling tuple-variant
4924        // envelope: same discipline on the peer kind-coherence
4925        // partition arm. Constructed alongside the Binario arm so any
4926        // future accessor drift lands on both arms in the same pin.
4927        let mut c = caixa(CaixaKind::Servico);
4928        // `:servicos` empty is the trigger — the fixture helper
4929        // defaults it to `vec![]`, so no mutation is needed.
4930        c.nome = "servico-demo".into();
4931        let expected_nome_via_accessor = c.nome().to_string();
4932        assert_eq!(
4933            expected_nome_via_accessor, "servico-demo",
4934            "the mutated fixture's `:nome` must be observable through \
4935             the accessor before layout verification fires",
4936        );
4937        let err = layout.verify(&c, &root).unwrap_err();
4938        let LayoutError::ServicoWithoutServicos(c_nome) = err else {
4939            panic!(
4940                "expected ServicoWithoutServicos for empty :servicos list on Servico kind, \
4941                 got {err:?}"
4942            );
4943        };
4944        assert_eq!(
4945            c_nome, expected_nome_via_accessor,
4946            "the ServicoWithoutServicos's payload must equal \
4947             `c.nome().to_string()` — same converge discipline as the \
4948             sibling `BinarioWithoutExe` tuple-variant arm above",
4949        );
4950    }
4951
4952    #[test]
4953    fn supervisor_must_not_have_bibliotecas() {
4954        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
4955        let root = PathBuf::from("/tmp/x");
4956        let manifest = root.join("caixa.lisp");
4957        let manifest_clone = manifest.clone();
4958        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
4959        let mut c = caixa(CaixaKind::Supervisor);
4960        c.estrategia = Some(RestartStrategy::OneForOne);
4961        c.max_restarts = Some(5);
4962        c.bibliotecas = vec!["lib/code.lisp".into()];
4963        c.children = vec![ChildSpec {
4964            caixa: "worker".into(),
4965            versao: "^0.1".into(),
4966            restart: RestartPolicy::Permanent,
4967        }];
4968        let err = layout.verify(&c, &root).unwrap_err();
4969        assert!(matches!(err, LayoutError::SupervisorOwnsCode(_)));
4970    }
4971
4972    // ── Caixa::validate_restart_window wired into Supervisor verify ─────
4973    //
4974    // Until this wire-up landed `Caixa::validate_restart_window` lived as
4975    // `pub fn` on `Caixa` with full per-arm unit coverage in
4976    // `manifest::tests` (`validate_restart_window_rejects_*` — fractional,
4977    // decimal-shaped integer, half-unit minute, leading sign, unknown
4978    // unit, garbage, empty-after-trim) but no production path called it;
4979    // `feira build` silently accepted malformed `:restart-window` and
4980    // `Caixa::supervisor_view` soft-swallowed the parse failure as
4981    // `restart_window: None` (the canonical "no reset" sentinel), turning
4982    // every authoring footgun into a never-reset supervisor far from the
4983    // source caixa.lisp. The following pins fence the layout-pipeline
4984    // wire-up: every layout verify on a structurally-invalid `:restart-
4985    // window` axis surfaces the per-axis `RestartWindowViolation { caixa,
4986    // issue }` envelope before the typed `SupervisorSpec::validate` gate
4987    // sees the laundered `None`.
4988
4989    fn supervisor_with_window(window: Option<&str>) -> Caixa {
4990        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
4991        let mut c = caixa(CaixaKind::Supervisor);
4992        c.estrategia = Some(RestartStrategy::OneForOne);
4993        c.max_restarts = Some(5);
4994        c.restart_window = window.map(str::to_string);
4995        c.children = vec![ChildSpec {
4996            caixa: "worker".into(),
4997            versao: "^0.1".into(),
4998            restart: RestartPolicy::Permanent,
4999        }];
5000        c
5001    }
5002
5003    #[test]
5004    fn restart_window_violation_on_fractional_seconds() {
5005        // `"1.5s"` is the canonical fractional-seconds drift footgun the
5006        // shared integer-magnitude codec (1c55a2a) rejects: round-trips
5007        // through `render` as `"1500ms"` on first serialize, breaking
5008        // THEORY.md §V.2.7 render-determinism. Before this wire-up
5009        // `supervisor_view` soft-swallowed the parse error as
5010        // `restart_window: None`, masking the drift as a never-reset
5011        // supervisor.
5012        let root = PathBuf::from("/tmp/x");
5013        let manifest = root.join("caixa.lisp");
5014        let manifest_clone = manifest.clone();
5015        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5016        let c = supervisor_with_window(Some("1.5s"));
5017        let err = layout.verify(&c, &root).unwrap_err();
5018        let LayoutError::RestartWindowViolation { caixa, issue } = err else {
5019            panic!("expected LayoutError::RestartWindowViolation, got {err:?}");
5020        };
5021        assert_eq!(caixa, "demo");
5022        assert!(
5023            issue.contains("1.5s"),
5024            "issue must quote the offending raw value: {issue}",
5025        );
5026    }
5027
5028    #[test]
5029    fn restart_window_violation_on_decimal_shaped_integer() {
5030        // `"1.0s"` — decimal-shaped integer the codec also rejects (a
5031        // canonical authoring form is `"1s"`). Sibling of the fractional
5032        // case; same codec arm.
5033        let root = PathBuf::from("/tmp/x");
5034        let manifest = root.join("caixa.lisp");
5035        let manifest_clone = manifest.clone();
5036        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5037        let c = supervisor_with_window(Some("1.0s"));
5038        let err = layout.verify(&c, &root).unwrap_err();
5039        assert!(
5040            matches!(
5041                err,
5042                LayoutError::RestartWindowViolation { ref caixa, ref issue }
5043                    if caixa == "demo" && issue.contains("1.0s")
5044            ),
5045            "got {err:?}",
5046        );
5047    }
5048
5049    #[test]
5050    fn restart_window_violation_on_leading_sign() {
5051        // `"+30s"` / `"-30s"` — leading-sign drift the codec rejects.
5052        // Canonical form is `"30s"`. Pin both signs separately because
5053        // a future relaxation might accept one but not the other.
5054        for raw in ["+30s", "-30s"] {
5055            let root = PathBuf::from("/tmp/x");
5056            let manifest = root.join("caixa.lisp");
5057            let manifest_clone = manifest.clone();
5058            let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5059            let c = supervisor_with_window(Some(raw));
5060            let err = layout.verify(&c, &root).unwrap_err();
5061            assert!(
5062                matches!(
5063                    err,
5064                    LayoutError::RestartWindowViolation { ref caixa, ref issue }
5065                        if caixa == "demo" && issue.contains(raw)
5066                ),
5067                "leading-sign {raw:?} got {err:?}",
5068            );
5069        }
5070    }
5071
5072    #[test]
5073    fn restart_window_violation_on_unknown_unit() {
5074        // `"30x"` — unknown duration unit. The codec admits only
5075        // `ms`/`s`/`m`/`h`.
5076        let root = PathBuf::from("/tmp/x");
5077        let manifest = root.join("caixa.lisp");
5078        let manifest_clone = manifest.clone();
5079        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5080        let c = supervisor_with_window(Some("30x"));
5081        let err = layout.verify(&c, &root).unwrap_err();
5082        assert!(
5083            matches!(
5084                err,
5085                LayoutError::RestartWindowViolation { ref caixa, .. } if caixa == "demo"
5086            ),
5087            "got {err:?}",
5088        );
5089    }
5090
5091    #[test]
5092    fn restart_window_violation_on_garbage() {
5093        // `"abc"` — pure garbage. The codec's parse fails before the
5094        // unit dispatch; the wrap envelope still surfaces the
5095        // self-locating diagnostic at the source.
5096        let root = PathBuf::from("/tmp/x");
5097        let manifest = root.join("caixa.lisp");
5098        let manifest_clone = manifest.clone();
5099        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5100        let c = supervisor_with_window(Some("abc"));
5101        let err = layout.verify(&c, &root).unwrap_err();
5102        assert!(
5103            matches!(err, LayoutError::RestartWindowViolation { ref caixa, .. } if caixa == "demo"),
5104            "got {err:?}",
5105        );
5106    }
5107
5108    #[test]
5109    fn restart_window_violation_on_empty_string() {
5110        // `""` — empty after trim. The shared codec's digit-only gate
5111        // refuses an empty magnitude. Distinguished here from the
5112        // `None` ("omit the slot") canonical authoring shape: an empty
5113        // string is an authored-but-empty slot, never the author's
5114        // intent.
5115        let root = PathBuf::from("/tmp/x");
5116        let manifest = root.join("caixa.lisp");
5117        let manifest_clone = manifest.clone();
5118        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5119        let c = supervisor_with_window(Some(""));
5120        let err = layout.verify(&c, &root).unwrap_err();
5121        assert!(
5122            matches!(err, LayoutError::RestartWindowViolation { ref caixa, .. } if caixa == "demo"),
5123            "got {err:?}",
5124        );
5125    }
5126
5127    #[test]
5128    fn verify_accepts_supervisor_without_restart_window() {
5129        // `None` is the canonical "omit the slot to express no reset"
5130        // shape — never reaches the codec, validates cleanly.
5131        let root = PathBuf::from("/tmp/x");
5132        let manifest = root.join("caixa.lisp");
5133        let manifest_clone = manifest.clone();
5134        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5135        let c = supervisor_with_window(None);
5136        layout.verify(&c, &root).unwrap();
5137    }
5138
5139    #[test]
5140    fn verify_accepts_supervisor_with_canonical_restart_window() {
5141        // Every canonical form the shared codec round-trips losslessly
5142        // must pass — `"500ms"`, `"30s"`, `"60s"`, `"1m"`, `"2m"`,
5143        // `"1h"`. Pin every form so a future tightening of the codec's
5144        // accepted set surfaces here as a test failure.
5145        for form in ["500ms", "30s", "60s", "1m", "2m", "1h"] {
5146            let root = PathBuf::from("/tmp/x");
5147            let manifest = root.join("caixa.lisp");
5148            let manifest_clone = manifest.clone();
5149            let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5150            let c = supervisor_with_window(Some(form));
5151            layout
5152                .verify(&c, &root)
5153                .unwrap_or_else(|e| panic!("canonical {form:?} must validate, got {e:?}"));
5154        }
5155    }
5156
5157    #[test]
5158    fn restart_window_violation_fires_before_supervisor_view_validate() {
5159        // Diagnostic-precedence pin: a Supervisor with a malformed
5160        // `:restart-window` AND a typed-shape defect on the typed view
5161        // (zero `:max-restarts`, which `SupervisorSpec::validate`'s
5162        // `ZeroMaxRestarts` arm rejects) surfaces the raw-string
5163        // diagnostic first — the narrower self-locating gate wins. Until
5164        // this wire-up landed `supervisor_view` would silently launder
5165        // the malformed `:restart-window` to `None` and then the typed
5166        // view's `ZeroMaxRestarts` gate would surface, masking the
5167        // raw-string footgun.
5168        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
5169        let root = PathBuf::from("/tmp/x");
5170        let manifest = root.join("caixa.lisp");
5171        let manifest_clone = manifest.clone();
5172        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5173        let mut c = caixa(CaixaKind::Supervisor);
5174        c.estrategia = Some(RestartStrategy::OneForOne);
5175        c.max_restarts = Some(0);
5176        c.restart_window = Some("1.5s".into());
5177        c.children = vec![ChildSpec {
5178            caixa: "worker".into(),
5179            versao: "^0.1".into(),
5180            restart: RestartPolicy::Permanent,
5181        }];
5182        let err = layout.verify(&c, &root).unwrap_err();
5183        assert!(
5184            matches!(err, LayoutError::RestartWindowViolation { .. }),
5185            "got {err:?} — RestartWindowViolation must fire before SupervisorViolation",
5186        );
5187    }
5188
5189    #[test]
5190    fn supervisor_slots_on_non_supervisor_fires_before_restart_window_violation() {
5191        // Order pin: a non-Supervisor caixa with a malformed
5192        // `:restart-window` surfaces `SupervisorSlotsOnNonSupervisor`
5193        // (the kind-coherence gate at the top of verify) before the
5194        // raw-string parse gate inside the Supervisor branch — because
5195        // `:restart-window` is foreign to non-Supervisor kinds, the
5196        // kind-coherence diagnostic is the load-bearing one. Mirrors
5197        // the existing `nome_violation_on_*` ordering tests that fence
5198        // the precedence between universal and kind-specific gates.
5199        let root = PathBuf::from("/tmp/x");
5200        let manifest = root.join("caixa.lisp");
5201        let default_lib = root.join("lib").join("demo.lisp");
5202        let layout =
5203            StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5204        let mut c = caixa(CaixaKind::Biblioteca);
5205        c.restart_window = Some("1.5s".into());
5206        let err = layout.verify(&c, &root).unwrap_err();
5207        assert!(
5208            matches!(err, LayoutError::SupervisorSlotsOnNonSupervisor { .. }),
5209            "got {err:?} — kind-coherence must fire before RestartWindowViolation",
5210        );
5211    }
5212
5213    #[test]
5214    fn restart_window_violation_diagnostic_carries_offending_value() {
5215        // Diagnostic-shape pin: the wrap envelope's `issue` carries the
5216        // codec's parser-shaped reason verbatim (which names the
5217        // offending raw value), so the author can grep their caixa.lisp
5218        // for `:restart-window "<value>"` and fix in one edit. Mirrors
5219        // `nome_violation_*_carries_offending_*` shape pins.
5220        let root = PathBuf::from("/tmp/x");
5221        let manifest = root.join("caixa.lisp");
5222        let manifest_clone = manifest.clone();
5223        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5224        let c = supervisor_with_window(Some("0.5m"));
5225        let err = layout.verify(&c, &root).unwrap_err();
5226        let LayoutError::RestartWindowViolation { caixa, issue } = err else {
5227            panic!("expected LayoutError::RestartWindowViolation, got {err:?}");
5228        };
5229        assert_eq!(caixa, "demo");
5230        assert!(
5231            issue.contains("0.5m"),
5232            "issue must quote the offending raw value verbatim: {issue}",
5233        );
5234        assert!(
5235            !issue.is_empty(),
5236            "issue must carry the codec's parser-shaped reason",
5237        );
5238    }
5239
5240    // ── Aplicacao layout tests ──────────────────────────────────────────
5241
5242    #[test]
5243    fn aplicacao_must_have_membros() {
5244        use crate::{Membro, Placement, PlacementStrategy};
5245        let root = PathBuf::from("/tmp/x");
5246        let manifest = root.join("caixa.lisp");
5247        let manifest_clone = manifest.clone();
5248        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5249        let mut c = caixa(CaixaKind::Aplicacao);
5250        c.placement = Some(Placement {
5251            estrategia: PlacementStrategy::Replicated,
5252            clusters: vec!["rio".into()],
5253            affinity: None,
5254            shard_key: None,
5255        });
5256        // No membros → fails
5257        let err = layout.verify(&c, &root).unwrap_err();
5258        assert!(matches!(err, LayoutError::AplicacaoViolation { .. }));
5259
5260        // With membros → passes
5261        c.membros = vec![Membro {
5262            caixa: "service-a".into(),
5263            versao: "^0.1".into(),
5264        }];
5265        layout.verify(&c, &root).unwrap();
5266    }
5267
5268    #[test]
5269    fn aplicacao_self_referential_membro_is_violation() {
5270        // An Aplicacao whose `:membros` names its own `:nome` is a
5271        // one-node lacre-closure recursion. The cross-slot gate fires
5272        // at verify time, surfacing as an AplicacaoViolation that
5273        // names the offending aplicacao — not at lacre-resolve time
5274        // far from source. The `caixa()` helper's `:nome` is "demo".
5275        // Peer of `supervisor_self_referential_child_is_violation`
5276        // on the supervision-tree axis.
5277        use crate::{Membro, Placement, PlacementStrategy};
5278        let root = PathBuf::from("/tmp/x");
5279        let manifest = root.join("caixa.lisp");
5280        let manifest_clone = manifest.clone();
5281        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5282        let mut c = caixa(CaixaKind::Aplicacao);
5283        c.placement = Some(Placement {
5284            estrategia: PlacementStrategy::Replicated,
5285            clusters: vec!["rio".into()],
5286            affinity: None,
5287            shard_key: None,
5288        });
5289        c.membros = vec![
5290            Membro {
5291                caixa: "service-a".into(),
5292                versao: "^0.1".into(),
5293            },
5294            Membro {
5295                caixa: "demo".into(),
5296                versao: "^0.1".into(),
5297            },
5298        ];
5299        let err = layout.verify(&c, &root).unwrap_err();
5300        let LayoutError::AplicacaoViolation { caixa, issue } = err else {
5301            panic!("expected AplicacaoViolation for self-referential membro, got {err:?}");
5302        };
5303        assert_eq!(caixa, "demo");
5304        assert!(
5305            issue.contains("demo") && issue.contains("lists itself"),
5306            "issue must name the self-membering aplicacao, got {issue:?}"
5307        );
5308    }
5309
5310    #[test]
5311    fn aplicacao_distinct_membros_pass_self_membership_gate() {
5312        // Positive control: an Aplicacao whose membros are all distinct
5313        // from its own `:nome` verifies cleanly.
5314        use crate::{Membro, Placement, PlacementStrategy};
5315        let root = PathBuf::from("/tmp/x");
5316        let manifest = root.join("caixa.lisp");
5317        let manifest_clone = manifest.clone();
5318        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5319        let mut c = caixa(CaixaKind::Aplicacao);
5320        c.placement = Some(Placement {
5321            estrategia: PlacementStrategy::Replicated,
5322            clusters: vec!["rio".into()],
5323            affinity: None,
5324            shard_key: None,
5325        });
5326        c.membros = vec![
5327            Membro {
5328                caixa: "service-a".into(),
5329                versao: "^0.1".into(),
5330            },
5331            Membro {
5332                caixa: "service-b".into(),
5333                versao: "^0.1".into(),
5334            },
5335        ];
5336        layout.verify(&c, &root).unwrap();
5337    }
5338
5339    #[test]
5340    fn aplicacao_self_membership_fires_after_view_validate() {
5341        // Diagnostic-precedence pin: a self-referential membro alongside
5342        // a duplicate-:caixa shape surfaces the more-fundamental
5343        // `MembroDuplicate` (from `view.validate()`) first; only when the
5344        // per-membros shape diagnostics pass does the cross-slot
5345        // self-membership gate fire. Mirrors the ordering pin
5346        // `supervisor_self_referential_child_is_violation` carries on
5347        // the peer supervision-tree axis (`view.validate()` runs first,
5348        // then the cross-slot gate). Without this ordering a future
5349        // refactor that swaps the two calls would silently mask the
5350        // narrower per-membro defect.
5351        use crate::{Membro, Placement, PlacementStrategy};
5352        let root = PathBuf::from("/tmp/x");
5353        let manifest = root.join("caixa.lisp");
5354        let manifest_clone = manifest.clone();
5355        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5356        let mut c = caixa(CaixaKind::Aplicacao);
5357        c.placement = Some(Placement {
5358            estrategia: PlacementStrategy::Replicated,
5359            clusters: vec!["rio".into()],
5360            affinity: None,
5361            shard_key: None,
5362        });
5363        c.membros = vec![
5364            Membro {
5365                caixa: "service-a".into(),
5366                versao: "^0.1".into(),
5367            },
5368            Membro {
5369                caixa: "service-a".into(),
5370                versao: "^0.2".into(),
5371            },
5372            Membro {
5373                caixa: "demo".into(),
5374                versao: "^0.1".into(),
5375            },
5376        ];
5377        let err = layout.verify(&c, &root).unwrap_err();
5378        let LayoutError::AplicacaoViolation { issue, .. } = err else {
5379            panic!("expected AplicacaoViolation, got {err:?}");
5380        };
5381        // The per-membros duplicate diagnostic (from view.validate())
5382        // surfaces ahead of the cross-slot self-membership gate, so the
5383        // `service-a` duplicate is named — not the `demo` self-reference.
5384        assert!(
5385            issue.contains("service-a") && issue.contains("more than once"),
5386            "duplicate-:caixa diagnostic must surface before self-membership gate, \
5387             got {issue:?}"
5388        );
5389    }
5390
5391    #[test]
5392    fn mesh_slots_on_servico_rejected() {
5393        // The canonical real-world footgun: an author adds :entrada to a
5394        // :kind Servico expecting it to expose ingress. aplicacao_view
5395        // returns None for Servico, so the slot is the manifest's
5396        // "ignored otherwise" — never validated, never rendered. The
5397        // kind-coherence gate rejects it at build time (before the
5398        // :servicos existence loop), naming the offending slot + kind.
5399        use crate::Entrada;
5400        let root = PathBuf::from("/tmp/x");
5401        let manifest = root.join("caixa.lisp");
5402        let manifest_clone = manifest.clone();
5403        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5404        let mut c = caixa(CaixaKind::Servico);
5405        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5406        c.entrada = Some(Entrada {
5407            host: "demo.example.com".into(),
5408            para: "demo".into(),
5409            paths: vec![],
5410            port: 8080,
5411        });
5412        let err = layout.verify(&c, &root).unwrap_err();
5413        match err {
5414            LayoutError::MeshSlotsOnNonAplicacao { caixa, kind, slots } => {
5415                assert_eq!(caixa, "demo");
5416                assert_eq!(kind, CaixaKind::Servico);
5417                assert_eq!(slots, crate::render::M3_AUTHOR_KEY_ENTRADA);
5418            }
5419            other => panic!("expected MeshSlotsOnNonAplicacao, got {other:?}"),
5420        }
5421    }
5422
5423    #[test]
5424    fn mesh_slots_on_non_aplicacao_lists_slots_in_canonical_order() {
5425        // All five mesh slots declared on a Biblioteca → the diagnostic
5426        // enumerates them in canonical declaration order, deterministic
5427        // across runs. The gate fires on declared-ness only (the values
5428        // need not be a *valid* AplicacaoSpec — aplicacao_view is never
5429        // called for a non-Aplicacao kind).
5430        use crate::{Entrada, Membro, MeshPolicy, Placement, PlacementStrategy, WitContract};
5431        let root = PathBuf::from("/tmp/x");
5432        let manifest = root.join("caixa.lisp");
5433        let manifest_clone = manifest.clone();
5434        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5435        let mut c = caixa(CaixaKind::Biblioteca);
5436        c.membros = vec![Membro {
5437            caixa: "a".into(),
5438            versao: "^0.1".into(),
5439        }];
5440        c.contratos = vec![WitContract {
5441            de: "a".into(),
5442            para: "a".into(),
5443            wit: "wasi:http/proxy".into(),
5444            endpoint: Some("/x".into()),
5445            subject: None,
5446            slot: None,
5447        }];
5448        c.politicas = Some(MeshPolicy::default());
5449        c.placement = Some(Placement {
5450            estrategia: PlacementStrategy::Replicated,
5451            clusters: vec!["rio".into()],
5452            affinity: None,
5453            shard_key: None,
5454        });
5455        c.entrada = Some(Entrada {
5456            host: "x.example.com".into(),
5457            para: "a".into(),
5458            paths: vec![],
5459            port: 8080,
5460        });
5461        let err = layout.verify(&c, &root).unwrap_err();
5462        match err {
5463            LayoutError::MeshSlotsOnNonAplicacao { slots, .. } => {
5464                assert_eq!(
5465                    slots,
5466                    format!(
5467                        "{} {} {} {} {}",
5468                        crate::render::M3_AUTHOR_KEY_MEMBROS,
5469                        crate::render::M3_AUTHOR_KEY_CONTRATOS,
5470                        crate::render::M3_AUTHOR_KEY_POLITICAS,
5471                        crate::render::M3_AUTHOR_KEY_PLACEMENT,
5472                        crate::render::M3_AUTHOR_KEY_ENTRADA,
5473                    )
5474                );
5475            }
5476            other => panic!("expected MeshSlotsOnNonAplicacao, got {other:?}"),
5477        }
5478    }
5479
5480    #[test]
5481    fn servico_without_mesh_slots_still_verifies() {
5482        // Pass-after control: a well-formed Servico carrying no mesh
5483        // slots must remain accepted — the gate keys off declared-ness,
5484        // so it must not over-fire on the common case.
5485        let root = PathBuf::from("/tmp/x");
5486        let servico = root.join("servicos/demo.computeunit.yaml");
5487        let manifest = root.join("caixa.lisp");
5488        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == servico);
5489        let mut c = caixa(CaixaKind::Servico);
5490        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5491        layout.verify(&c, &root).unwrap();
5492    }
5493
5494    #[test]
5495    fn supervisor_slots_on_servico_rejected() {
5496        // Mirror of `mesh_slots_on_servico_rejected` on the
5497        // supervisor-tree slot set: an author adds `:children` to a
5498        // `:kind Servico` expecting it to spawn workers. supervisor_view
5499        // returns None for Servico, so the slot is the manifest's
5500        // "ignored otherwise" — never validated, never reconciled. The
5501        // kind-coherence gate rejects it at build time (before the
5502        // :servicos existence loop), naming the offending slot + kind.
5503        use crate::{ChildSpec, RestartPolicy};
5504        let root = PathBuf::from("/tmp/x");
5505        let manifest = root.join("caixa.lisp");
5506        let manifest_clone = manifest.clone();
5507        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5508        let mut c = caixa(CaixaKind::Servico);
5509        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5510        c.children = vec![ChildSpec {
5511            caixa: "worker".into(),
5512            versao: "^0.1".into(),
5513            restart: RestartPolicy::Permanent,
5514        }];
5515        let err = layout.verify(&c, &root).unwrap_err();
5516        match err {
5517            LayoutError::SupervisorSlotsOnNonSupervisor { caixa, kind, slots } => {
5518                assert_eq!(caixa, "demo");
5519                assert_eq!(kind, CaixaKind::Servico);
5520                assert_eq!(slots, crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN);
5521            }
5522            other => panic!("expected SupervisorSlotsOnNonSupervisor, got {other:?}"),
5523        }
5524    }
5525
5526    #[test]
5527    fn supervisor_slots_on_non_supervisor_lists_slots_in_canonical_order() {
5528        // All four supervisor slots declared on a Biblioteca → the
5529        // diagnostic enumerates them in canonical declaration order
5530        // (`:estrategia` → `:max-restarts` → `:restart-window` →
5531        // `:children`), deterministic across runs. The gate fires on
5532        // declared-ness only (the values need not be a *valid*
5533        // SupervisorSpec — supervisor_view is never called for a
5534        // non-Supervisor kind). Mirror of
5535        // `mesh_slots_on_non_aplicacao_lists_slots_in_canonical_order`.
5536        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
5537        let root = PathBuf::from("/tmp/x");
5538        let manifest = root.join("caixa.lisp");
5539        let manifest_clone = manifest.clone();
5540        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5541        let mut c = caixa(CaixaKind::Biblioteca);
5542        c.estrategia = Some(RestartStrategy::OneForOne);
5543        c.max_restarts = Some(5);
5544        c.restart_window = Some("60s".into());
5545        c.children = vec![ChildSpec {
5546            caixa: "worker".into(),
5547            versao: "^0.1".into(),
5548            restart: RestartPolicy::Permanent,
5549        }];
5550        let err = layout.verify(&c, &root).unwrap_err();
5551        match err {
5552            LayoutError::SupervisorSlotsOnNonSupervisor { slots, .. } => {
5553                assert_eq!(slots, ":estrategia :max-restarts :restart-window :children");
5554            }
5555            other => panic!("expected SupervisorSlotsOnNonSupervisor, got {other:?}"),
5556        }
5557    }
5558
5559    #[test]
5560    fn aplicacao_with_supervisor_slots_rejected() {
5561        // Cross-kind pin: an Aplicacao (the other no-code orchestrator
5562        // kind) that declares a supervisor slot is rejected by the
5563        // supervisor-slot gate, just as a Supervisor declaring a mesh
5564        // slot is rejected by the mesh-slot gate — the two kind ↔ slot
5565        // coherence gates are symmetric and mutually exclusive. The
5566        // gate fires before the Aplicacao typed-graph validation, so
5567        // the diagnostic names the foreign supervisor slot rather than
5568        // a downstream AplicacaoViolation.
5569        use crate::{Membro, Placement, PlacementStrategy, RestartStrategy};
5570        let root = PathBuf::from("/tmp/x");
5571        let manifest = root.join("caixa.lisp");
5572        let manifest_clone = manifest.clone();
5573        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5574        let mut c = caixa(CaixaKind::Aplicacao);
5575        c.membros = vec![Membro {
5576            caixa: "service-a".into(),
5577            versao: "^0.1".into(),
5578        }];
5579        c.placement = Some(Placement {
5580            estrategia: PlacementStrategy::Replicated,
5581            clusters: vec!["rio".into()],
5582            affinity: None,
5583            shard_key: None,
5584        });
5585        c.estrategia = Some(RestartStrategy::OneForAll);
5586        let err = layout.verify(&c, &root).unwrap_err();
5587        match err {
5588            LayoutError::SupervisorSlotsOnNonSupervisor { caixa, kind, slots } => {
5589                assert_eq!(caixa, "demo");
5590                assert_eq!(kind, CaixaKind::Aplicacao);
5591                assert_eq!(slots, crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA);
5592            }
5593            other => panic!("expected SupervisorSlotsOnNonSupervisor, got {other:?}"),
5594        }
5595    }
5596
5597    #[test]
5598    fn servico_without_supervisor_slots_still_verifies() {
5599        // Pass-after control: a well-formed Servico carrying no
5600        // supervisor slots must remain accepted — the gate keys off
5601        // declared-ness, so it must not over-fire on the common case.
5602        let root = PathBuf::from("/tmp/x");
5603        let servico = root.join("servicos/demo.computeunit.yaml");
5604        let manifest = root.join("caixa.lisp");
5605        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == servico);
5606        let mut c = caixa(CaixaKind::Servico);
5607        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5608        layout.verify(&c, &root).unwrap();
5609    }
5610
5611    #[test]
5612    fn servico_slots_on_biblioteca_rejected() {
5613        // Mirror of `mesh_slots_on_servico_rejected` /
5614        // `supervisor_slots_on_servico_rejected` on the M2
5615        // Servico-runtime slot set: an author adds `:limits` to a
5616        // `:kind Biblioteca` expecting per-process sandboxing. The
5617        // caixa-helm / caixa-flux renderers gate on `require_kind(_,
5618        // Servico)`, so the slot is the manifest's "ignored otherwise" —
5619        // never rendered into any artifact. The kind-coherence gate
5620        // rejects it at build time (before the M2 validate blocks),
5621        // naming the offending slot + kind.
5622        use crate::LimitsSpec;
5623        let root = PathBuf::from("/tmp/x");
5624        let manifest = root.join("caixa.lisp");
5625        let lib = root.join("lib").join("demo.lisp");
5626        let manifest_clone = manifest.clone();
5627        let layout =
5628            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == lib);
5629        let mut c = caixa(CaixaKind::Biblioteca);
5630        c.limits = Some(LimitsSpec {
5631            fuel: Some(1_000_000),
5632            ..Default::default()
5633        });
5634        let err = layout.verify(&c, &root).unwrap_err();
5635        match err {
5636            LayoutError::ServicoSlotsOnNonServico { caixa, kind, slots } => {
5637                assert_eq!(caixa, "demo");
5638                assert_eq!(kind, CaixaKind::Biblioteca);
5639                assert_eq!(slots, crate::render::M2_AUTHOR_KEY_LIMITS);
5640            }
5641            other => panic!("expected ServicoSlotsOnNonServico, got {other:?}"),
5642        }
5643    }
5644
5645    #[test]
5646    fn servico_slots_on_non_servico_lists_slots_in_canonical_order() {
5647        // All three M2 slots declared on a Biblioteca → the diagnostic
5648        // enumerates them in canonical declaration order (`:limits` →
5649        // `:behavior` → `:upgrade-from`), deterministic across runs. The
5650        // gate fires on declared-ness only (the values need not pass the
5651        // M2 validate blocks — those run only after the kind-coherence
5652        // gate, and never for a non-Servico declared-slot caixa). Mirror
5653        // of the mesh/supervisor `*_lists_slots_in_canonical_order` pins.
5654        use crate::{BehaviorSpec, LimitsSpec, UpgradeFromEntry, UpgradeInstruction};
5655        let root = PathBuf::from("/tmp/x");
5656        let manifest = root.join("caixa.lisp");
5657        let manifest_clone = manifest.clone();
5658        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5659        let mut c = caixa(CaixaKind::Biblioteca);
5660        c.limits = Some(LimitsSpec {
5661            fuel: Some(1_000_000),
5662            ..Default::default()
5663        });
5664        c.behavior = Some(BehaviorSpec {
5665            on_init: Some(PathBuf::from("lib/init.lisp")),
5666            ..Default::default()
5667        });
5668        c.upgrade_from = vec![UpgradeFromEntry {
5669            from: "0.1.0".into(),
5670            instructions: vec![UpgradeInstruction::Restart],
5671        }];
5672        let err = layout.verify(&c, &root).unwrap_err();
5673        match err {
5674            LayoutError::ServicoSlotsOnNonServico { slots, .. } => {
5675                assert_eq!(
5676                    slots,
5677                    format!(
5678                        "{} {} {}",
5679                        crate::render::M2_AUTHOR_KEY_LIMITS,
5680                        crate::render::M2_AUTHOR_KEY_BEHAVIOR,
5681                        crate::render::M2_AUTHOR_KEY_UPGRADE_FROM,
5682                    )
5683                );
5684            }
5685            other => panic!("expected ServicoSlotsOnNonServico, got {other:?}"),
5686        }
5687    }
5688
5689    #[test]
5690    fn aplicacao_with_servico_slots_rejected() {
5691        // Cross-kind pin (mirror of `aplicacao_with_supervisor_slots_rejected`):
5692        // an Aplicacao that declares an M2 Servico-runtime slot is
5693        // rejected by the Servico-slot gate, just as a Supervisor
5694        // declaring a mesh slot is rejected by the mesh-slot gate — the
5695        // three kind ↔ slot coherence gates are symmetric and mutually
5696        // exclusive. The gate fires before the Aplicacao typed-graph
5697        // validation, so the diagnostic names the foreign M2 slot rather
5698        // than a downstream AplicacaoViolation about missing :membros.
5699        use crate::{Membro, Placement, PlacementStrategy, UpgradeFromEntry, UpgradeInstruction};
5700        let root = PathBuf::from("/tmp/x");
5701        let manifest = root.join("caixa.lisp");
5702        let manifest_clone = manifest.clone();
5703        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5704        let mut c = caixa(CaixaKind::Aplicacao);
5705        c.membros = vec![Membro {
5706            caixa: "service-a".into(),
5707            versao: "^0.1".into(),
5708        }];
5709        c.placement = Some(Placement {
5710            estrategia: PlacementStrategy::Replicated,
5711            clusters: vec!["rio".into()],
5712            affinity: None,
5713            shard_key: None,
5714        });
5715        c.upgrade_from = vec![UpgradeFromEntry {
5716            from: "0.1.0".into(),
5717            instructions: vec![UpgradeInstruction::Restart],
5718        }];
5719        let err = layout.verify(&c, &root).unwrap_err();
5720        match err {
5721            LayoutError::ServicoSlotsOnNonServico { caixa, kind, slots } => {
5722                assert_eq!(caixa, "demo");
5723                assert_eq!(kind, CaixaKind::Aplicacao);
5724                assert_eq!(slots, crate::render::M2_AUTHOR_KEY_UPGRADE_FROM);
5725            }
5726            other => panic!("expected ServicoSlotsOnNonServico, got {other:?}"),
5727        }
5728    }
5729
5730    #[test]
5731    fn servico_with_servico_slots_still_verifies() {
5732        // Pass-after control: a well-formed Servico carrying all three M2
5733        // slots must remain accepted — the gate is guarded by `kind !=
5734        // Servico`, so it must not over-fire on the kind these slots
5735        // exist for. Mirror of `servico_without_{mesh,supervisor}_slots_
5736        // still_verifies` on the legitimate-declaration axis.
5737        use crate::{BehaviorSpec, LimitsSpec, UpgradeFromEntry, UpgradeInstruction};
5738        let root = PathBuf::from("/tmp/x");
5739        let manifest = root.join("caixa.lisp");
5740        let svc = root.join("servicos/demo.computeunit.yaml");
5741        let init = root.join("lib/init.lisp");
5742        let layout =
5743            StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc || p == init);
5744        let mut c = caixa(CaixaKind::Servico);
5745        c.versao = "0.2.0".into();
5746        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5747        c.limits = Some(LimitsSpec {
5748            fuel: Some(1_000_000),
5749            ..Default::default()
5750        });
5751        c.behavior = Some(BehaviorSpec {
5752            on_init: Some(PathBuf::from("lib/init.lisp")),
5753            ..Default::default()
5754        });
5755        c.upgrade_from = vec![UpgradeFromEntry {
5756            from: "0.1.0".into(),
5757            instructions: vec![UpgradeInstruction::Restart],
5758        }];
5759        layout.verify(&c, &root).unwrap();
5760    }
5761
5762    #[test]
5763    fn aplicacao_must_not_have_bibliotecas() {
5764        use crate::{Membro, Placement, PlacementStrategy};
5765        let root = PathBuf::from("/tmp/x");
5766        let manifest = root.join("caixa.lisp");
5767        let manifest_clone = manifest.clone();
5768        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5769        let mut c = caixa(CaixaKind::Aplicacao);
5770        c.bibliotecas = vec!["lib/code.lisp".into()];
5771        c.membros = vec![Membro {
5772            caixa: "x".into(),
5773            versao: "^0.1".into(),
5774        }];
5775        c.placement = Some(Placement {
5776            estrategia: PlacementStrategy::Replicated,
5777            clusters: vec!["rio".into()],
5778            affinity: None,
5779            shard_key: None,
5780        });
5781        let err = layout.verify(&c, &root).unwrap_err();
5782        assert!(matches!(err, LayoutError::AplicacaoOwnsCode(_)));
5783    }
5784
5785    #[test]
5786    fn acao_must_not_have_bibliotecas() {
5787        // Mirror of `supervisor_must_not_have_bibliotecas` /
5788        // `aplicacao_must_not_have_bibliotecas` on the third no-code
5789        // kind. `has_code` fires before the `:ci`-presence gates below
5790        // it, so this must surface `AcaoOwnsCode` even though the
5791        // caixa also lacks a `:ci` slot (which would otherwise surface
5792        // as `MissingCi`) — the more-fundamental "this kind runs no
5793        // code at all" diagnostic wins.
5794        let root = PathBuf::from("/tmp/x");
5795        let manifest = root.join("caixa.lisp");
5796        let manifest_clone = manifest.clone();
5797        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5798        let mut c = caixa(CaixaKind::Acao);
5799        c.bibliotecas = vec!["lib/code.lisp".into()];
5800        let err = layout.verify(&c, &root).unwrap_err();
5801        assert!(matches!(err, LayoutError::AcaoOwnsCode(_)));
5802    }
5803
5804    #[test]
5805    fn acao_without_ci_errors() {
5806        // Mirror of `binario_without_exe_errors` on the fifth required-
5807        // slot axis.
5808        let root = PathBuf::from("/tmp/x");
5809        let manifest = root.join("caixa.lisp");
5810        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5811        let err = layout.verify(&caixa(CaixaKind::Acao), &root).unwrap_err();
5812        assert!(matches!(err, LayoutError::MissingCi(_)));
5813    }
5814
5815    #[test]
5816    fn acao_with_ci_passes() {
5817        let root = PathBuf::from("/tmp/x");
5818        let manifest = root.join("caixa.lisp");
5819        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5820        let mut c = caixa(CaixaKind::Acao);
5821        c.ci = Some(canteiro_types::CiRun {
5822            workspace: "pleme-io".into(),
5823            repo: "caixa".into(),
5824            nodes: vec![],
5825        });
5826        layout
5827            .verify(&c, &root)
5828            .expect("an Acao caixa with a declared :ci slot passes layout verify");
5829    }
5830
5831    #[test]
5832    fn ci_on_non_acao_errors() {
5833        // Mirror of `mesh_slots_on_non_aplicacao_lists_slots_in_canonical_order`
5834        // on the Acao-only `:ci` axis — declaring `:ci` on any other
5835        // kind is the same "silently ignored" footgun the sibling
5836        // mesh-/supervisor-/servico-slot gates already close.
5837        let root = PathBuf::from("/tmp/x");
5838        let manifest = root.join("caixa.lisp");
5839        let manifest_clone = manifest.clone();
5840        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5841        let mut c = caixa(CaixaKind::Biblioteca);
5842        c.ci = Some(canteiro_types::CiRun {
5843            workspace: "pleme-io".into(),
5844            repo: "caixa".into(),
5845            nodes: vec![],
5846        });
5847        let err = layout.verify(&c, &root).unwrap_err();
5848        match err {
5849            LayoutError::CiOnNonAcao { caixa, kind } => {
5850                assert_eq!(caixa, "demo");
5851                assert_eq!(kind, CaixaKind::Biblioteca);
5852            }
5853            other => panic!("expected CiOnNonAcao, got {other:?}"),
5854        }
5855    }
5856
5857    // ── ForeignCodeSlot — kind ↔ code-surface coherence ────────────────
5858
5859    #[test]
5860    fn biblioteca_with_exe_rejected() {
5861        // Fail-before-pass-after pin: a `:kind Biblioteca` declaring
5862        // `:exe` is the "I added a CLI to my library" footgun — the nix
5863        // flake renderer for Binario gates on `require_kind(_, Binario)`,
5864        // so on a Biblioteca the `:exe` path is silently dropped past
5865        // the layout's path-existence check (no executable target is
5866        // ever generated). The diagnostic names the offending kind +
5867        // slot verbatim so the author can grep their caixa.lisp for
5868        // `:exe` and fix in one edit (drop the slot or change
5869        // `:kind Biblioteca` → `:kind Binario`).
5870        let root = PathBuf::from("/tmp/x");
5871        let manifest = root.join("caixa.lisp");
5872        let lib = root.join("lib").join("demo.lisp");
5873        let exe_path = root.join("exe").join("tool");
5874        let layout = StandardLayout::new()
5875            .with_path_exists(move |p| p == manifest || p == lib || p == exe_path);
5876        let mut c = caixa(CaixaKind::Biblioteca);
5877        c.exe = vec!["exe/tool".into()];
5878        let err = layout.verify(&c, &root).unwrap_err();
5879        match err {
5880            LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
5881                assert_eq!(caixa, "demo");
5882                assert_eq!(kind, CaixaKind::Biblioteca);
5883                assert_eq!(slots, ":exe");
5884            }
5885            other => panic!("expected ForeignCodeSlot, got {other:?}"),
5886        }
5887    }
5888
5889    #[test]
5890    fn biblioteca_with_servicos_rejected() {
5891        // Symmetric to `biblioteca_with_exe_rejected` on the
5892        // `:servicos` axis: a `:kind Biblioteca` declaring a Servico
5893        // computeunit silently passed validate and the daemon's
5894        // ComputeUnit / lareira chart never materialized (caixa-helm /
5895        // caixa-flux gate emission on `require_kind(_, Servico)`).
5896        let root = PathBuf::from("/tmp/x");
5897        let manifest = root.join("caixa.lisp");
5898        let lib = root.join("lib").join("demo.lisp");
5899        let svc = root.join("servicos").join("demo.computeunit.yaml");
5900        let layout =
5901            StandardLayout::new().with_path_exists(move |p| p == manifest || p == lib || p == svc);
5902        let mut c = caixa(CaixaKind::Biblioteca);
5903        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5904        let err = layout.verify(&c, &root).unwrap_err();
5905        match err {
5906            LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
5907                assert_eq!(caixa, "demo");
5908                assert_eq!(kind, CaixaKind::Biblioteca);
5909                assert_eq!(slots, ":servicos");
5910            }
5911            other => panic!("expected ForeignCodeSlot, got {other:?}"),
5912        }
5913    }
5914
5915    #[test]
5916    fn biblioteca_with_exe_and_servicos_lists_slots_in_canonical_order() {
5917        // Both foreign code slots declared on a Biblioteca → the
5918        // diagnostic enumerates them in canonical declaration order
5919        // (`:exe` → `:servicos`), deterministic across runs. Mirrors the
5920        // mesh/supervisor/servico-slot `*_lists_slots_in_canonical_order`
5921        // pins on the peer kind ↔ slot algebra axes; drift in the
5922        // [`Caixa::declared_foreign_code_slots`] iteration order surfaces
5923        // here.
5924        let root = PathBuf::from("/tmp/x");
5925        let manifest = root.join("caixa.lisp");
5926        let manifest_clone = manifest.clone();
5927        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
5928        let mut c = caixa(CaixaKind::Biblioteca);
5929        c.exe = vec!["exe/tool".into()];
5930        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5931        let err = layout.verify(&c, &root).unwrap_err();
5932        match err {
5933            LayoutError::ForeignCodeSlot { slots, .. } => {
5934                assert_eq!(slots, ":exe :servicos");
5935            }
5936            other => panic!("expected ForeignCodeSlot, got {other:?}"),
5937        }
5938    }
5939
5940    #[test]
5941    fn binario_with_servicos_rejected() {
5942        // The peer footgun on the Binario kind: declaring a Servico
5943        // computeunit on a `:kind Binario` caixa. The caixa-helm /
5944        // caixa-flux renderers gate on `require_kind(_, Servico)`, so
5945        // the `:servicos` slot vanishes past the layout's path-
5946        // existence check — no ComputeUnit, no Helm chart. `:exe` stays
5947        // valid (Binario's native code surface), so the kind-coherence
5948        // diagnostic targets only `:servicos`.
5949        let root = PathBuf::from("/tmp/x");
5950        let manifest = root.join("caixa.lisp");
5951        let exe_path = root.join("exe").join("tool");
5952        let svc = root.join("servicos").join("demo.computeunit.yaml");
5953        let layout = StandardLayout::new()
5954            .with_path_exists(move |p| p == manifest || p == exe_path || p == svc);
5955        let mut c = caixa(CaixaKind::Binario);
5956        c.exe = vec!["exe/tool".into()];
5957        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5958        let err = layout.verify(&c, &root).unwrap_err();
5959        match err {
5960            LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
5961                assert_eq!(caixa, "demo");
5962                assert_eq!(kind, CaixaKind::Binario);
5963                assert_eq!(slots, ":servicos");
5964            }
5965            other => panic!("expected ForeignCodeSlot, got {other:?}"),
5966        }
5967    }
5968
5969    #[test]
5970    fn servico_with_exe_rejected() {
5971        // Symmetric to `binario_with_servicos_rejected` on the other
5972        // code-running peer: a `:kind Servico` declaring an `:exe` is
5973        // the "I added a host-side CLI to my wasm component" footgun —
5974        // the nix flake's Binario target gates on `require_kind(_,
5975        // Binario)`, so the `:exe` path vanishes past the layout's
5976        // path-existence check.
5977        let root = PathBuf::from("/tmp/x");
5978        let manifest = root.join("caixa.lisp");
5979        let svc = root.join("servicos").join("demo.computeunit.yaml");
5980        let exe_path = root.join("exe").join("tool");
5981        let layout = StandardLayout::new()
5982            .with_path_exists(move |p| p == manifest || p == svc || p == exe_path);
5983        let mut c = caixa(CaixaKind::Servico);
5984        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5985        c.exe = vec!["exe/tool".into()];
5986        let err = layout.verify(&c, &root).unwrap_err();
5987        match err {
5988            LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
5989                assert_eq!(caixa, "demo");
5990                assert_eq!(kind, CaixaKind::Servico);
5991                assert_eq!(slots, ":exe");
5992            }
5993            other => panic!("expected ForeignCodeSlot, got {other:?}"),
5994        }
5995    }
5996
5997    #[test]
5998    fn binario_without_servicos_still_verifies() {
5999        // Pass-after control: a well-formed Binario carrying only its
6000        // native `:exe` surface must remain accepted — the gate keys off
6001        // declared-ness of the *foreign* slots, so it must not over-fire
6002        // on the legitimate same-kind case. Mirror of
6003        // `servico_with_servico_slots_still_verifies` on the peer axis.
6004        let root = PathBuf::from("/tmp/x");
6005        let manifest = root.join("caixa.lisp");
6006        let exe_path = root.join("exe").join("tool");
6007        let layout =
6008            StandardLayout::new().with_path_exists(move |p| p == manifest || p == exe_path);
6009        let mut c = caixa(CaixaKind::Binario);
6010        c.exe = vec!["exe/tool".into()];
6011        layout.verify(&c, &root).unwrap();
6012    }
6013
6014    #[test]
6015    fn biblioteca_with_only_bibliotecas_still_verifies() {
6016        // Pass-after control: a well-formed Biblioteca carrying only
6017        // its native `:bibliotecas` surface (or the default
6018        // `lib/<nome>.lisp`) must remain accepted. The gate keys off
6019        // declared-ness of `:exe` + `:servicos` only — `:bibliotecas`
6020        // is deliberately excluded from the foreign-set on every
6021        // code-running kind (`declared_foreign_code_slots` doc), so a
6022        // Biblioteca with the canonical lib surface alone passes.
6023        let root = PathBuf::from("/tmp/x");
6024        let manifest = root.join("caixa.lisp");
6025        let lib = root.join("lib").join("demo.lisp");
6026        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == lib);
6027        layout
6028            .verify(&caixa(CaixaKind::Biblioteca), &root)
6029            .expect("Biblioteca with default lib must verify");
6030    }
6031
6032    #[test]
6033    fn binario_with_bibliotecas_helper_still_verifies() {
6034        // Pass-after control on the deliberate `:bibliotecas`-as-helper
6035        // shape: a `:kind Binario` may legitimately bundle a `lib/`
6036        // helper its nix flake build consumes (the same shape a
6037        // `:kind Servico` may bundle for its wasm-component source).
6038        // The foreign-code-slot gate must NOT fire on `:bibliotecas` for
6039        // either code-running kind; pinned here so a future tightening
6040        // that adds `:bibliotecas` to the foreign set on Binario /
6041        // Servico surfaces as a test failure rather than as a silent
6042        // over-reach.
6043        let root = PathBuf::from("/tmp/x");
6044        let manifest = root.join("caixa.lisp");
6045        let exe_path = root.join("exe").join("tool");
6046        let lib = root.join("lib").join("helper.lisp");
6047        let layout = StandardLayout::new()
6048            .with_path_exists(move |p| p == manifest || p == exe_path || p == lib);
6049        let mut c = caixa(CaixaKind::Binario);
6050        c.exe = vec!["exe/tool".into()];
6051        c.bibliotecas = vec!["lib/helper.lisp".into()];
6052        layout.verify(&c, &root).unwrap();
6053    }
6054
6055    #[test]
6056    fn supervisor_with_exe_still_surfaces_owns_code() {
6057        // Diagnostic-precedence pin: a `:kind Supervisor` declaring
6058        // `:exe` is *both* "Supervisor with code" and "foreign code
6059        // slot". The more-fundamental `SupervisorOwnsCode` must win
6060        // (Supervisor doesn't run code at all — the foreign-slot
6061        // diagnostic would mislead the author toward changing `:kind`
6062        // when the underlying defect is that supervisors orchestrate
6063        // children, not code). Guards the call order in `verify`
6064        // against silent reordering.
6065        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6066        let root = PathBuf::from("/tmp/x");
6067        let manifest = root.join("caixa.lisp");
6068        let manifest_clone = manifest.clone();
6069        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6070        let mut c = caixa(CaixaKind::Supervisor);
6071        c.estrategia = Some(RestartStrategy::OneForOne);
6072        c.max_restarts = Some(5);
6073        c.exe = vec!["exe/tool".into()];
6074        c.children = vec![ChildSpec {
6075            caixa: "worker".into(),
6076            versao: "^0.1".into(),
6077            restart: RestartPolicy::Permanent,
6078        }];
6079        let err = layout.verify(&c, &root).unwrap_err();
6080        assert!(
6081            matches!(err, LayoutError::SupervisorOwnsCode(_)),
6082            "Supervisor-with-:exe must surface as SupervisorOwnsCode (the more-fundamental \
6083             no-code-at-all diagnostic), got {err:?}"
6084        );
6085    }
6086
6087    #[test]
6088    fn declared_foreign_code_slots_returns_canonical_order() {
6089        // Unit-level pin for the lifted method: the canonical iteration
6090        // order is `:exe` → `:servicos`, independent of which subset is
6091        // populated. Empty input + each single-slot subset + the full
6092        // pair are all checked so a future axis added to the method
6093        // (a hypothetical fifth code-surface slot) is one extension
6094        // point + one assertion update here, not a coordinated rewrite
6095        // across the layout-test sites that reach for the canonical
6096        // order.
6097        let mut c = caixa(CaixaKind::Biblioteca);
6098        assert!(c.declared_foreign_code_slots().is_empty());
6099        c.exe = vec!["exe/tool".into()];
6100        assert_eq!(c.declared_foreign_code_slots(), vec![":exe"]);
6101        c.exe.clear();
6102        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6103        assert_eq!(c.declared_foreign_code_slots(), vec![":servicos"]);
6104        c.exe = vec!["exe/tool".into()];
6105        assert_eq!(c.declared_foreign_code_slots(), vec![":exe", ":servicos"]);
6106    }
6107
6108    #[test]
6109    fn aplicacao_with_unknown_contrato_member_fails() {
6110        use crate::{Membro, Placement, PlacementStrategy, WitContract};
6111        let root = PathBuf::from("/tmp/x");
6112        let manifest = root.join("caixa.lisp");
6113        let manifest_clone = manifest.clone();
6114        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6115        let mut c = caixa(CaixaKind::Aplicacao);
6116        c.membros = vec![Membro {
6117            caixa: "service-a".into(),
6118            versao: "^0.1".into(),
6119        }];
6120        c.contratos = vec![WitContract {
6121            de: "service-a".into(),
6122            para: "phantom".into(),
6123            wit: "wasi:http/proxy".into(),
6124            endpoint: Some("/x".into()),
6125            subject: None,
6126            slot: None,
6127        }];
6128        c.placement = Some(Placement {
6129            estrategia: PlacementStrategy::Replicated,
6130            clusters: vec!["rio".into()],
6131            affinity: None,
6132            shard_key: None,
6133        });
6134        let err = layout.verify(&c, &root).unwrap_err();
6135        assert!(matches!(err, LayoutError::AplicacaoViolation { .. }));
6136    }
6137
6138    #[test]
6139    fn limits_zero_axis_surfaces_as_layout_violation() {
6140        use crate::LimitsSpec;
6141        let root = PathBuf::from("/tmp/x");
6142        let manifest = root.join("caixa.lisp");
6143        let svc = root.join("servicos/demo.computeunit.yaml");
6144        let mut c = caixa(CaixaKind::Servico);
6145        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6146        c.limits = Some(LimitsSpec {
6147            fuel: Some(0),
6148            ..Default::default()
6149        });
6150        let manifest_clone = manifest.clone();
6151        let svc_clone = svc.clone();
6152        let layout =
6153            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
6154        let err = layout.verify(&c, &root).unwrap_err();
6155        let LayoutError::LimitsViolation { caixa, issue } = err else {
6156            panic!("expected LimitsViolation, got {err:?}");
6157        };
6158        assert_eq!(caixa, "demo");
6159        assert!(issue.contains(":fuel"), "issue must name the axis: {issue}");
6160    }
6161
6162    #[test]
6163    fn limits_well_formed_passes_layout() {
6164        use crate::LimitsSpec;
6165        use std::time::Duration;
6166        let root = PathBuf::from("/tmp/x");
6167        let manifest = root.join("caixa.lisp");
6168        let svc = root.join("servicos/demo.computeunit.yaml");
6169        let mut c = caixa(CaixaKind::Servico);
6170        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6171        c.limits = Some(LimitsSpec {
6172            memory: Some(64 * 1024 * 1024),
6173            fuel: Some(1_000_000),
6174            wall_clock: Some(Duration::from_secs(30)),
6175            cpu: Some(500),
6176        });
6177        let manifest_clone = manifest.clone();
6178        let svc_clone = svc.clone();
6179        let layout =
6180            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
6181        layout.verify(&c, &root).unwrap();
6182    }
6183
6184    #[test]
6185    fn supervisor_with_valid_children_passes() {
6186        use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6187        let root = PathBuf::from("/tmp/x");
6188        let manifest = root.join("caixa.lisp");
6189        let manifest_clone = manifest.clone();
6190        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6191        let mut c = caixa(CaixaKind::Supervisor);
6192        c.estrategia = Some(RestartStrategy::OneForOne);
6193        c.max_restarts = Some(5);
6194        c.children = vec![
6195            ChildSpec {
6196                caixa: "worker".into(),
6197                versao: "^0.1".into(),
6198                restart: RestartPolicy::Permanent,
6199            },
6200            ChildSpec {
6201                caixa: "cache".into(),
6202                versao: "^0.1".into(),
6203                restart: RestartPolicy::Transient,
6204            },
6205        ];
6206        layout.verify(&c, &root).unwrap();
6207    }
6208
6209    // ── :upgrade-from entry validation pipes through layout ─────────────
6210
6211    #[test]
6212    fn upgrade_invalid_module_surfaces_as_layout_violation() {
6213        // End-to-end pin that
6214        // [`crate::UpgradeFromEntry::validate`] runs *inside*
6215        // `LayoutInvariants::verify` and surfaces value-shape
6216        // violations through the new `UpgradeViolation` arm
6217        // (parallel to `BehaviorViolation`, `LimitsViolation`,
6218        // `SupervisorViolation`, `AplicacaoViolation`). Until this
6219        // wiring landed the entry validator was unreachable from any
6220        // build-pipeline caller — an `:upgrade-from
6221        // ((:from "0.1.0" :instructions ((:load-module "Hello")))` (uppercase
6222        // module name the K8s apiserver would reject on the per-
6223        // ComputeUnit `metadata.name` axis) silently passed
6224        // `feira lint` / `feira build` and surfaced only at wasm-engine
6225        // hot-upgrade time as a per-backend "module not found" /
6226        // `code:load_module/1` `badarg` runtime error, far from the
6227        // source caixa.lisp. Pinning the wiring here so a future
6228        // refactor that drops the `entry.validate()` call surfaces as
6229        // a build-pipeline regression at this test, not as a runtime
6230        // surprise per consumer.
6231        use crate::{UpgradeFromEntry, UpgradeInstruction};
6232        use std::path::PathBuf;
6233        let root = PathBuf::from("/tmp/x");
6234        let manifest = root.join("caixa.lisp");
6235        let svc = root.join("servicos/demo.computeunit.yaml");
6236        let mut c = caixa(CaixaKind::Servico);
6237        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6238        c.upgrade_from = vec![UpgradeFromEntry {
6239            from: "0.1.0".into(),
6240            instructions: vec![UpgradeInstruction::LoadModule {
6241                module: "Hello".into(), // uppercase — not DNS-1123
6242            }],
6243        }];
6244        let manifest_clone = manifest.clone();
6245        let svc_clone = svc.clone();
6246        let layout =
6247            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
6248        let err = layout.verify(&c, &root).unwrap_err();
6249        let LayoutError::UpgradeViolation { caixa, issue } = err else {
6250            panic!("expected UpgradeViolation, got {err:?}");
6251        };
6252        assert_eq!(caixa, "demo");
6253        assert!(
6254            issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_LOAD_MODULE),
6255            "issue must name the lisp-form of the offending instruction: {issue}"
6256        );
6257        assert!(
6258            issue.contains("Hello"),
6259            "issue must name the offending :module verbatim: {issue}"
6260        );
6261    }
6262
6263    #[test]
6264    fn upgrade_empty_module_surfaces_as_layout_violation() {
6265        // Companion to the DNS-1123 footgun above on the narrower
6266        // empty arm. Every Module-bearing variant's empty value
6267        // reaches the layout pipeline through the kind-tagged
6268        // `ModuleEmpty` diagnostic naming its lisp-form.
6269        use crate::{UpgradeFromEntry, UpgradeInstruction};
6270        use std::path::PathBuf;
6271        let root = PathBuf::from("/tmp/x");
6272        let manifest = root.join("caixa.lisp");
6273        let svc = root.join("servicos/demo.computeunit.yaml");
6274        let mut c = caixa(CaixaKind::Servico);
6275        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6276        c.upgrade_from = vec![UpgradeFromEntry {
6277            from: "0.1.0".into(),
6278            instructions: vec![UpgradeInstruction::SoftPurge {
6279                module: String::new(),
6280            }],
6281        }];
6282        let manifest_clone = manifest.clone();
6283        let svc_clone = svc.clone();
6284        let layout =
6285            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
6286        let err = layout.verify(&c, &root).unwrap_err();
6287        let LayoutError::UpgradeViolation { caixa, issue } = err else {
6288            panic!("expected UpgradeViolation, got {err:?}");
6289        };
6290        assert_eq!(caixa, "demo");
6291        assert!(
6292            issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_SOFT_PURGE),
6293            "issue must name the lisp-form of the empty instruction: {issue}"
6294        );
6295    }
6296
6297    #[test]
6298    fn upgrade_invalid_state_change_script_surfaces_as_layout_violation() {
6299        // Pins that the b0c8389 script value-shape gates
6300        // (AbsoluteScript / ParentEscapeScript) — previously
6301        // unreachable from any build-pipeline caller — now fire
6302        // through the same `UpgradeViolation` arm before the path-
6303        // existence pass would otherwise emit the less-helpful
6304        // "missing upgrade-script" (or, worse, *succeed* against
6305        // /etc/passwd, proving the sandbox bypass — same defect
6306        // the b0c8389 BehaviorSpec wiring closed on the peer M2
6307        // slot).
6308        use crate::{UpgradeFromEntry, UpgradeInstruction};
6309        use std::path::PathBuf;
6310        let root = PathBuf::from("/tmp/x");
6311        let manifest = root.join("caixa.lisp");
6312        let svc = root.join("servicos/demo.computeunit.yaml");
6313        let etc_passwd = PathBuf::from("/etc/passwd");
6314        let mut c = caixa(CaixaKind::Servico);
6315        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6316        c.upgrade_from = vec![UpgradeFromEntry {
6317            from: "0.1.0".into(),
6318            instructions: vec![UpgradeInstruction::StateChange {
6319                script: PathBuf::from("/etc/passwd"),
6320            }],
6321        }];
6322        let manifest_clone = manifest.clone();
6323        let svc_clone = svc.clone();
6324        let etc_passwd_clone = etc_passwd.clone();
6325        // Critically: /etc/passwd "exists" in our mock — without the
6326        // value-shape pre-check, the existence loop would *succeed*
6327        // and the path-traversal exit from the project sandbox would
6328        // pass `feira build` silently.
6329        let layout = StandardLayout::new().with_path_exists(move |p| {
6330            p == manifest_clone || p == svc_clone || p == etc_passwd_clone
6331        });
6332        let err = layout.verify(&c, &root).unwrap_err();
6333        let LayoutError::UpgradeViolation { caixa, issue } = err else {
6334            panic!("expected UpgradeViolation, got {err:?}");
6335        };
6336        assert_eq!(caixa, "demo");
6337        assert!(
6338            issue.contains("absolute") || issue.contains("Absolute"),
6339            "issue must name the violation kind (absolute): {issue}"
6340        );
6341    }
6342
6343    #[test]
6344    fn upgrade_well_formed_passes_layout() {
6345        // Positive control — every documented authoring shape
6346        // (`:load-module`, `:state-change` with a relative path,
6347        // `:soft-purge`, `:purge`, sole `:restart`) passes the wired
6348        // gate. The typed sequence (`:load-module` → `:state-change`
6349        // → `:soft-purge` → `:purge`) lives in one entry; the sole
6350        // `:restart` fallback lives in a *separate* entry on a
6351        // different `:from` (the within-entry restart-exclusivity
6352        // gate added in this commit rejects mixing the fallback with
6353        // the typed sequence — per the UpgradeInstruction::Restart
6354        // doc, `:restart` is terminal and any other instructions in
6355        // the same entry are dead code). Drift here = a future
6356        // tighten that rejects any canonical shape surfaces as a
6357        // regression at this layout-level pin, not piecemeal across
6358        // per-renderer call sites.
6359        //
6360        // `:soft-purge` and `:purge` target *distinct* old-version
6361        // modules (`hello-rio-old` and `hello-rio-oldest`) so the
6362        // within-entry cleanup-singularity gate
6363        // (`UpgradeError::DuplicateCleanup`) passes — that gate
6364        // rejects more than one cleanup per module per entry (one
6365        // semantic per old version; mixing drain + discard on one
6366        // module is the soft-then-hard fallback footgun the author
6367        // shouldn't write because the operator handles cleanup
6368        // failure escalation itself). The two distinct names cover
6369        // the legitimate "drain a recent old, hard-discard an
6370        // older-still" shape — both authoring forms remain load-
6371        // bearing in this positive-control enumeration.
6372        use crate::{BehaviorSpec, UpgradeFromEntry, UpgradeInstruction};
6373        use std::path::PathBuf;
6374        let root = PathBuf::from("/tmp/x");
6375        let manifest = root.join("caixa.lisp");
6376        let svc = root.join("servicos/demo.computeunit.yaml");
6377        let migration = root.join("lib/migrations/v01-to-v02.lisp");
6378        let on_state_change = root.join("lib/migrations.lisp");
6379        let mut c = caixa(CaixaKind::Servico);
6380        // `:versao` past both entries' `:from` so the cross-slot
6381        // precedence gate (`FromNotBeforeVersao`) lets this canonical
6382        // authoring shape through to the positive-control assertion.
6383        c.versao = "0.2.0".into();
6384        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6385        // `:on-state-change` declared alongside the `(:state-change …)`
6386        // instruction below — the cross-slot composition gate
6387        // (`validate_upgrade_from_against_behavior`) rejects a
6388        // `:state-change` without the callback, so the canonical
6389        // authoring shape this positive control pins now includes the
6390        // runtime delivery hook (the `gen_server:code_change/3` analog
6391        // that the per-version script is invoked through during hot
6392        // upgrade per the upgrade.rs module doc "Composes with"
6393        // promise).
6394        c.behavior = Some(BehaviorSpec {
6395            on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
6396            ..Default::default()
6397        });
6398        c.upgrade_from = vec![
6399            UpgradeFromEntry {
6400                from: "0.1.0".into(),
6401                instructions: vec![
6402                    UpgradeInstruction::LoadModule {
6403                        module: "hello-rio".into(),
6404                    },
6405                    UpgradeInstruction::StateChange {
6406                        script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
6407                    },
6408                    UpgradeInstruction::SoftPurge {
6409                        module: "hello-rio-old".into(),
6410                    },
6411                    UpgradeInstruction::Purge {
6412                        module: "hello-rio-oldest".into(),
6413                    },
6414                ],
6415            },
6416            UpgradeFromEntry {
6417                from: "0.0.9".into(),
6418                instructions: vec![UpgradeInstruction::Restart],
6419            },
6420        ];
6421        let manifest_clone = manifest.clone();
6422        let svc_clone = svc.clone();
6423        let migration_clone = migration.clone();
6424        let on_state_change_clone = on_state_change.clone();
6425        let layout = StandardLayout::new().with_path_exists(move |p| {
6426            p == manifest_clone
6427                || p == svc_clone
6428                || p == migration_clone
6429                || p == on_state_change_clone
6430        });
6431        layout.verify(&c, &root).unwrap();
6432    }
6433
6434    #[test]
6435    fn upgrade_from_restart_mixed_surfaces_as_upgrade_violation() {
6436        // Wiring pin: the within-entry `(:restart)`-exclusivity gate
6437        // (`UpgradeFromEntry::validate_restart_exclusive`) lands on
6438        // the same `LayoutError::UpgradeViolation` axis the per-entry
6439        // shape gate (26da2c7), the cross-entry duplicate-`:from`
6440        // gate (7c6aef2), and the cross-slot `:from < :versao`
6441        // precedence gate (de7ab1a) already do. A caixa.lisp whose
6442        // `:upgrade-from` entry mixes `(:restart)` with a typed
6443        // instruction surfaces at `feira build` time naming the
6444        // offending caixa + the entry's `:from` rather than silently
6445        // passing into the wasm-operator with semantically dead code
6446        // in the operator's dispatch table. Mirrors
6447        // `upgrade_from_duplicate_surfaces_as_upgrade_violation` on
6448        // the peer cross-entry gate.
6449        use crate::{UpgradeFromEntry, UpgradeInstruction};
6450        let root = PathBuf::from("/tmp/x");
6451        let manifest = root.join("caixa.lisp");
6452        let svc = root.join("servicos/demo.computeunit.yaml");
6453        let mut c = caixa(CaixaKind::Servico);
6454        c.versao = "0.2.0".into();
6455        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6456        c.upgrade_from = vec![UpgradeFromEntry {
6457            from: "0.1.0".into(),
6458            instructions: vec![
6459                UpgradeInstruction::LoadModule {
6460                    module: "hello-rio".into(),
6461                },
6462                UpgradeInstruction::Restart,
6463            ],
6464        }];
6465        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
6466        let err = layout.verify(&c, &root).unwrap_err();
6467        let LayoutError::UpgradeViolation { caixa, issue } = err else {
6468            panic!("expected LayoutError::UpgradeViolation for restart-mixed entry, got {err:?}");
6469        };
6470        assert_eq!(caixa, "demo");
6471        assert!(
6472            issue.contains("0.1.0"),
6473            "UpgradeViolation issue must name the offending entry's `:from` verbatim, got \
6474             {issue:?}"
6475        );
6476        assert!(
6477            issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_RESTART),
6478            "UpgradeViolation issue must name the `:restart` axis verbatim, got {issue:?}"
6479        );
6480        assert!(
6481            issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_LOAD_MODULE),
6482            "UpgradeViolation issue must name the non-:restart peer instruction's lisp-form \
6483             verbatim, got {issue:?}"
6484        );
6485    }
6486
6487    #[test]
6488    fn upgrade_from_restart_duplicated_surfaces_as_upgrade_violation() {
6489        // Companion arm: the duplicate-`(:restart)` mode of
6490        // `RestartNotExclusive` (no typed peers, just multiple
6491        // `Restart` variants) surfaces through the same wiring as the
6492        // mixed-with-typed mode above. The diagnostic still names the
6493        // offending entry's `:from` verbatim even when `other_kinds`
6494        // is empty.
6495        use crate::{UpgradeFromEntry, UpgradeInstruction};
6496        let root = PathBuf::from("/tmp/x");
6497        let manifest = root.join("caixa.lisp");
6498        let svc = root.join("servicos/demo.computeunit.yaml");
6499        let mut c = caixa(CaixaKind::Servico);
6500        c.versao = "0.2.0".into();
6501        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6502        c.upgrade_from = vec![UpgradeFromEntry {
6503            from: "0.1.0".into(),
6504            instructions: vec![UpgradeInstruction::Restart, UpgradeInstruction::Restart],
6505        }];
6506        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
6507        let err = layout.verify(&c, &root).unwrap_err();
6508        let LayoutError::UpgradeViolation { caixa, issue } = err else {
6509            panic!(
6510                "expected LayoutError::UpgradeViolation for duplicate-restart entry, got \
6511                 {err:?}"
6512            );
6513        };
6514        assert_eq!(caixa, "demo");
6515        assert!(
6516            issue.contains("0.1.0"),
6517            "UpgradeViolation issue must name the offending entry's `:from` verbatim, got \
6518             {issue:?}"
6519        );
6520        assert!(
6521            issue.contains("(:restart)")
6522                || issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_RESTART),
6523            "UpgradeViolation issue must name the `:restart` axis verbatim, got {issue:?}"
6524        );
6525    }
6526
6527    #[test]
6528    fn upgrade_from_invalid_surfaces_as_layout_violation() {
6529        // The `:from` semver gate (`UpgradeError::FromInvalid`)
6530        // was likewise unreachable before this wiring landed — a
6531        // typo-shaped `:from "v0.1.0"` (git-tag-shape leaking into
6532        // the semver slot) silently passed `feira build` and
6533        // surfaced only when the operator's hot-upgrade decision
6534        // engine tried to match against the version key it couldn't
6535        // parse. Now wired through `UpgradeViolation` with the
6536        // peer-shaped `{ from, reason }` payload — the
6537        // parser-shaped `reason` flows through `Display` so the
6538        // wrapped issue string carries both the offending value
6539        // *and* the SemVer-2 parser's wording (peer with the
6540        // `VersaoInvalid` / `MembroVersaoInvalid` envelopes on the
6541        // sibling SemVer-2 axes).
6542        use crate::{UpgradeFromEntry, UpgradeInstruction};
6543        use std::path::PathBuf;
6544        let root = PathBuf::from("/tmp/x");
6545        let manifest = root.join("caixa.lisp");
6546        let svc = root.join("servicos/demo.computeunit.yaml");
6547        let mut c = caixa(CaixaKind::Servico);
6548        c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6549        c.upgrade_from = vec![UpgradeFromEntry {
6550            from: "v0.1.0".into(), // git-tag-shape, not semver
6551            instructions: vec![UpgradeInstruction::Restart],
6552        }];
6553        let manifest_clone = manifest.clone();
6554        let svc_clone = svc.clone();
6555        let layout =
6556            StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
6557        let err = layout.verify(&c, &root).unwrap_err();
6558        let LayoutError::UpgradeViolation { caixa, issue } = err else {
6559            panic!("expected UpgradeViolation, got {err:?}");
6560        };
6561        assert_eq!(caixa, "demo");
6562        assert!(
6563            issue.contains("v0.1.0"),
6564            "UpgradeViolation issue must name the offending :from value verbatim, got {issue:?}"
6565        );
6566        assert!(
6567            issue.contains(":from"),
6568            "UpgradeViolation issue must name the :from slot verbatim, got {issue:?}"
6569        );
6570        // Pin the parser-shaped reason flow-through: the renamed
6571        // `FromInvalid { from, reason }` carries the SemVer-2 parser's
6572        // wording verbatim, and the [`UpgradeError`] Display routes it
6573        // into the wrapped `issue` string so the layout envelope
6574        // surfaces both the offending value *and* the parser's
6575        // diagnosis. Mirrors the peer flow-through on
6576        // `ManifestError::VersaoInvalid` (top-level `:versao`) and
6577        // `AplicacaoError::MembroVersaoInvalid` (`:membros :versao`).
6578        assert!(
6579            issue.contains("SemVer-2"),
6580            "UpgradeViolation issue must carry the parser-shaped reason (\"SemVer-2\"), got {issue:?}"
6581        );
6582    }
6583
6584    #[test]
6585    fn missing_lib_gate_routes_through_kind_requires_lib_and_caixa_nome() {
6586        // Fail-before-pass-after pin on the two-part converge landed
6587        // at layout.rs:844-847:
6588        //   (a) `caixa.kind().is_biblioteca()` →
6589        //       `caixa.kind().requires_lib()` — routes the biblioteca
6590        //       required-slot gate onto the same `requires_*()`
6591        //       predicate family the three sibling required-slot
6592        //       gates (`requires_exe()` at :856, `requires_servicos()`
6593        //       at :860, `requires_ci()` at :874) already key off.
6594        //       All four gates in the block now share one convention;
6595        //       a future kind that gains its own required-slot gate
6596        //       (an M4/M5 typed arm the CAIXA-SDLC §I six-kind roster
6597        //       may grow) reaches for the same predicate family and
6598        //       inherits the accessor discipline for free.
6599        //   (b) raw `caixa.nome` → `caixa.nome()` — routes the
6600        //       `expected` path composition through the typed
6601        //       [`crate::Caixa::nome`] accessor, closing the last
6602        //       unlifted raw `caixa.nome` production field-access
6603        //       site in `caixa-core/src/layout.rs` (every peer
6604        //       diagnostic in the file already routes through
6605        //       `caixa.nome().to_string()`).
6606        //
6607        // The behavioral pin: for a Biblioteca kind with no fallback
6608        // `lib/<nome>.lisp` file, MissingLib fires and its `expected`
6609        // path composes through `Caixa::nome()`; for every other
6610        // kind, MissingLib does NOT fire (the gate short-circuits on
6611        // kinds where `requires_lib()` returns false), even when the
6612        // fallback file is likewise absent. A future regression that
6613        // reroutes the gate off `requires_lib()` (e.g. onto
6614        // `is_biblioteca()` again, or onto a hand-authored
6615        // `matches!(caixa.kind(), CaixaKind::Biblioteca)`) that
6616        // *happens* to agree byte-for-byte on today's arm-set trips
6617        // this test the moment a future kind's `requires_lib()`
6618        // returns true for a non-`Biblioteca` arm (or the sibling
6619        // required-slot gates diverge from the same convention).
6620        let root = PathBuf::from("/tmp/x");
6621        let manifest = root.join("caixa.lisp");
6622        let manifest_only = manifest.clone();
6623        let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_only);
6624
6625        // Biblioteca kind + no lib fallback → MissingLib fires with
6626        // the expected path composed through `Caixa::nome()`.
6627        let bib = caixa(CaixaKind::Biblioteca);
6628        assert!(
6629            bib.kind().requires_lib(),
6630            "requires_lib() must return true for Biblioteca — the four-required-\
6631             slot-gate family's routing depends on this arm's assignment"
6632        );
6633        let err = layout.verify(&bib, &root).unwrap_err();
6634        let LayoutError::MissingLib {
6635            caixa: cname,
6636            expected,
6637        } = err
6638        else {
6639            panic!("expected MissingLib for Biblioteca kind with no lib fallback, got {err:?}");
6640        };
6641        assert_eq!(
6642            cname,
6643            bib.nome(),
6644            "MissingLib `caixa:` carrier must byte-equal Caixa::nome()"
6645        );
6646        assert_eq!(
6647            expected,
6648            root.join(crate::render::LAYOUT_DIR_LIB)
6649                .join(format!("{}.lisp", bib.nome())),
6650            "MissingLib `expected:` path must compose through Caixa::nome() \
6651             verbatim — a raw-field-access regression would silently drift \
6652             the composed path on any future `:nome` axis extension \
6653             (namespace-qualified rewrite, per-cluster alias overlay)"
6654        );
6655
6656        // Non-Biblioteca kinds → the MissingLib gate short-circuits.
6657        // Different kinds fail on their own required-slot gate
6658        // (BinarioWithoutExe, ServicoWithoutServicos, MissingCi) or
6659        // on downstream M2/M3 invariants; none of them may surface as
6660        // MissingLib, because `requires_lib()` returns false for each.
6661        for kind in [
6662            CaixaKind::Binario,
6663            CaixaKind::Servico,
6664            CaixaKind::Supervisor,
6665            CaixaKind::Aplicacao,
6666            CaixaKind::Acao,
6667        ] {
6668            assert!(
6669                !kind.requires_lib(),
6670                "requires_lib() must return false for {kind:?} — the \
6671                 four-required-slot-gate family's arm assignment pins \
6672                 exactly one kind (Biblioteca) as the arm that requires \
6673                 a `lib/` entry"
6674            );
6675            let c = caixa(kind);
6676            let result = layout.verify(&c, &root);
6677            assert!(
6678                !matches!(result, Err(LayoutError::MissingLib { .. })),
6679                "MissingLib gate at layout.rs:844 must short-circuit for \
6680                 kinds where requires_lib() returns false; unexpectedly \
6681                 fired for {kind:?}: {result:?}"
6682            );
6683        }
6684    }
6685}