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