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