Skip to main content

caixa_core/
layout.rs

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